Mariam-metho / templates /admin_edit_article.html
kuro223's picture
o
8bdef7b
{% extends "base.html" %}
{% block title %}Éditer Article{% endblock %}
{% block extra_head %}
<link rel="stylesheet" type="text/css" href="https://unpkg.com/trix@2.0.8/dist/trix.css">
<style>
/* Admin Editor Styles */
.admin-editor {
max-width: 1000px;
margin: 0 auto;
padding: var(--spacing-lg);
background: var(--bg-card);
border-radius: 8px;
box-shadow: var(--shadow-md);
}
.editor-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--spacing-lg);
flex-wrap: wrap;
gap: var(--spacing-md);
}
.editor-title {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
background: linear-gradient(135deg, var(--primary-1), var(--accent-1));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.form-group {
margin-bottom: var(--spacing-md);
}
.form-group label {
display: block;
margin-bottom: var(--spacing-xs);
color: var(--text);
font-weight: 500;
}
.form-control {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--bg);
color: var(--text);
transition: all var(--transition-fast);
}
.form-control:focus {
outline: none;
border-color: var(--primary-1);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
trix-editor {
min-height: 300px;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--bg);
color: var(--text);
padding: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
trix-editor:focus {
outline: none;
border-color: var(--primary-1);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.preview-section {
margin-top: var(--spacing-xl);
padding: var(--spacing-lg);
background: var(--bg-alt);
border-radius: 8px;
}
.preview-header {
margin-bottom: var(--spacing-md);
padding-bottom: var(--spacing-sm);
border-bottom: 1px solid var(--border-color);
}
.preview-content {
background: var(--bg);
padding: var(--spacing-lg);
border-radius: 8px;
box-shadow: var(--shadow-sm);
}
/* Custom file input */
.file-input-wrapper {
position: relative;
}
.file-input-wrapper input[type="file"] {
opacity: 0;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.file-input-trigger {
display: block;
padding: 0.75rem 1rem;
border: 2px dashed var(--border-color);
border-radius: 8px;
text-align: center;
color: var(--text-light);
transition: all var(--transition-fast);
}
.file-input-wrapper:hover .file-input-trigger {
border-color: var(--primary-1);
color: var(--primary-1);
}
</style>
{% endblock %}
{% block content %}
<div class="admin-editor">
<div class="editor-header">
<h1 class="editor-title">Éditer Article</h1>
<a href="{{ url_for('admin_articles') }}" class="btn btn-secondary">← Retour aux articles</a>
</div>
<form method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="title">Titre:</label>
<input type="text" class="form-control" id="title" name="title" value="{{ article.title }}" required>
</div>
<div class="form-group">
<label for="category_id">Catégorie:</label>
<select class="form-control" id="category_id" name="category_id" required>
{% for category in article.category.subject.categories %}
<option value="{{ category.id }}" {% if category.id == article.category_id %}selected{% endif %}>{{ category.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="icon">Image:</label>
<div class="file-input-wrapper">
<div class="file-input-trigger">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="upload-icon">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
<span>Glissez une image ou cliquez pour sélectionner</span>
</div>
<input type="file" id="icon" name="icon" accept="image/*">
</div>
</div>
<div class="form-group">
<label for="youtube_url">Lien YouTube (optionnel) :</label>
<input type="url" class="form-control" id="youtube_url" name="youtube_url"
placeholder="https://www.youtube.com/watch?v=..."
value="{{ article.youtube_url or '' }}">
</div>
<div class="form-group">
<label for="content">Contenu:</label>
<input id="content" type="hidden" name="content" value="{{ article.content | safe }}">
<trix-editor input="content"></trix-editor>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Enregistrer les modifications</button>
</div>
</form>
<div class="preview-section">
<div class="preview-header">
<h3>Aperçu en direct</h3>
</div>
<div id="editor-preview" class="preview-content"></div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script type="text/javascript" src="https://unpkg.com/trix@2.0.8/dist/trix.umd.min.js"></script>
<script>
// Configure Trix before init
document.addEventListener('trix-before-initialize', function() {
if (window.Trix && Trix.config && Trix.config.dompurify) {
Trix.config.dompurify.ADD_TAGS = ["iframe", "video", "source", "table", "tr", "td", "th", "img"];
Trix.config.dompurify.ADD_ATTR = ["src", "controls", "width", "height", "class", "style", "allowfullscreen"];
}
Trix.config.toolbar.getDefaultHTML = function() {
return `
<div class="trix-button-row">
<span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" title="Gras">Gras</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" title="Italique">Italique</button>
<button type="button" class="trix-button" data-trix-action="x-heading" title="Titre">H</button>
<button type="button" class="trix-button" data-trix-action="x-quote" title="Citation">"</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" title="Lien">Lien</button>
</span>
<span class="trix-button-group">
<button type="button" class="trix-button" data-trix-attribute="textAlign" data-trix-value="left" title="Aligner à gauche">←</button>
<button type="button" class="trix-button" data-trix-attribute="textAlign" data-trix-value="center" title="Centrer">⫸</button>
<button type="button" class="trix-button" data-trix-attribute="textAlign" data-trix-value="right" title="Aligner à droite">→</button>
</span>
</div>`;
};
});
function renderShortcodesForPreview(html) {
if (!html) return '';
html = html.replace(/\[youtube:([^\]]+)\]/g, function(_, url) {
try {
var parsed = new URL(url.trim());
var id = null;
if (parsed.hostname.includes('youtu.be')) id = parsed.pathname.slice(1);
else if (parsed.hostname.includes('youtube')) id = parsed.searchParams.get('v') || parsed.pathname.split('/').pop();
if (!id) return '<pre>' + url + '</pre>';
return '<div class="video-player"><iframe src="https://www.youtube.com/embed/' + id + '" allowfullscreen></iframe></div>';
} catch (e) {
return '<pre>' + url + '</pre>'
}
});
html = html.replace(/\[image:([^\]|]+)(?:\|([^\]]+))?\]/g, function(_, url, opts) {
var classes = '';
var style = '';
if (opts) {
opts.split(/[|,]/).forEach(function(o) {
o = o.trim();
if (/^(left|right|center)$/.test(o)) {
if (o === 'left') classes = 'float-left';
if (o === 'right') classes = 'float-right';
if (o === 'center') style = 'display:block;margin-left:auto;margin-right:auto';
} else if (/^width=/.test(o)) {
style += (style ? ';' : '') + 'width:' + o.split('=')[1];
} else if (/^height=/.test(o)) {
style += (style ? ';' : '') + 'height:' + o.split('=')[1];
}
});
}
return '<img src="' + url.trim() + '" class="' + classes + '" style="' + style + '"/>';
});
return html;
}
function uploadFileToServer(fileBlob, filename, attachment) {
var formData = new FormData();
formData.append('file', fileBlob, filename);
fetch('/upload', {
method: 'POST',
body: formData
}).then(function(r) {
return r.json()
}).then(function(data) {
if (data.url) attachment.setAttributes({
url: data.url
});
}).catch(function(e) {
console.error('Upload failed', e)
});
}
// File upload and image resize handling
document.addEventListener('trix-attachment-add', function(event) {
var attachment = event.attachment;
var file = attachment.file;
if (!file) return;
if (!file.type.startsWith('image/')) {
uploadFileToServer(file, file.name, attachment);
return;
}
// Create modal for image resize
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
`;
const modalContent = document.createElement('div');
modalContent.style.cssText = `
background: var(--bg);
padding: 2rem;
border-radius: var(--card-radius);
box-shadow: var(--shadow-lg);
max-width: 400px;
width: 90%;
`;
modalContent.innerHTML = `
<h3 style="margin-top:0">Redimensionner l'image ?</h3>
<p>Entrez la largeur souhaitée :</p>
<input type="text" placeholder="800px ou 80%" style="width:100%;padding:0.5rem;margin:1rem 0;border-radius:var(--btn-radius);border:1px solid var(--border-color)">
<div style="display:flex;gap:1rem;justify-content:flex-end">
<button class="btn btn-secondary cancel">Annuler</button>
<button class="btn btn-primary confirm">Confirmer</button>
</div>
`;
modal.appendChild(modalContent);
document.body.appendChild(modal);
const input = modalContent.querySelector('input');
const cancelBtn = modalContent.querySelector('.cancel');
const confirmBtn = modalContent.querySelector('.confirm');
cancelBtn.onclick = () => {
document.body.removeChild(modal);
};
confirmBtn.onclick = () => {
const choice = input.value;
document.body.removeChild(modal);
if (!choice) {
uploadFileToServer(file, file.name, attachment);
return;
}
const reader = new FileReader();
reader.onload = function(e) {
const img = new Image();
img.onload = function() {
const origW = img.naturalWidth,
origH = img.naturalHeight;
let targetW = origW;
const c = choice.toString().trim();
if (c.endsWith('%')) {
const p = parseFloat(c.replace('%', ''));
if (!isNaN(p)) targetW = Math.round(origW * p / 100);
} else {
const px = parseInt(c, 10);
if (!isNaN(px)) targetW = px;
}
if (targetW <= 0 || targetW === origW) {
uploadFileToServer(file, file.name, attachment);
return;
}
const scale = targetW / origW,
targetH = Math.round(origH * scale);
const canvas = document.createElement('canvas');
canvas.width = targetW;
canvas.height = targetH;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, targetW, targetH);
canvas.toBlob(function(blob) {
uploadFileToServer(blob, file.name, attachment);
}, file.type || 'image/jpeg', 0.92);
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
};
});
// Live preview updates
function updatePreview() {
var contentInput = document.querySelector('input[name="content"]');
var preview = document.getElementById('editor-preview');
if (preview && contentInput) {
preview.innerHTML = renderShortcodesForPreview(contentInput.value);
}
}
document.addEventListener('trix-change', updatePreview);
document.addEventListener('DOMContentLoaded', updatePreview);
// File input visual feedback
document.querySelector('.file-input-wrapper input[type="file"]').addEventListener('change', function(e) {
const trigger = this.parentElement.querySelector('.file-input-trigger');
if (this.files.length > 0) {
trigger.textContent = `Fichier sélectionné : ${this.files[0].name}`;
}
});
// Keyboard shortcuts for shortcodes
document.addEventListener('keydown', function(e) {
// Ctrl+Y for YouTube shortcode
if (e.ctrlKey && e.key === 'y') {
e.preventDefault();
const editor = document.querySelector('trix-editor');
if (editor && editor.editor) {
const position = editor.editor.getSelectedRange();
editor.editor.setSelectedRange(position);
editor.editor.insertString('[youtube:]');
// Position cursor inside the brackets
const newPosition = position[0] + '[youtube:'.length;
editor.editor.setSelectedRange([newPosition, newPosition]);
}
}
// Ctrl+I for responsive image shortcode
if (e.ctrlKey && e.key === 'i') {
e.preventDefault();
const editor = document.querySelector('trix-editor');
if (editor && editor.editor) {
const position = editor.editor.getSelectedRange();
editor.editor.setSelectedRange(position);
editor.editor.insertString('[image:URL|width=100%]');
// Select "URL" for easy replacement
const urlStart = position[0] + '[image:'.length;
const urlEnd = urlStart + 3; // "URL".length
editor.editor.setSelectedRange([urlStart, urlEnd]);
}
}
});
</script>
{% endblock %}