talas-group/talas-wiki/templates/edit.html

335 lines
14 KiB
HTML
Raw Normal View History

{{define "title"}}Edition: {{.Page.Title}} — Talas Wiki{{end}}
{{define "pagePath"}}{{encodeURL .Page.URLPath}}{{end}}
{{define "content"}}
<div class="breadcrumb">
<a href="/">/</a>
{{range breadcrumbs .Page.URLPath}}
/ <a href="/wiki/{{encodeURL .path}}">{{.name}}</a>
{{end}}
/ <span class="edit-marker">edition</span>
</div>
<div class="page-header">
<h1>Edition: {{.Page.Title}}</h1>
<div class="page-actions"><a href="/wiki/{{encodeURL .Page.URLPath}}" class="btn-cancel">annuler</a></div>
</div>
<div id="draft-banner" class="draft-banner" style="display:none">
<span class="draft-banner-text">Brouillon non sauvegarde trouve.</span>
<button class="draft-banner-btn" onclick="restoreDraft()">Restaurer</button>
<button class="draft-banner-btn" onclick="dismissDraft()">Ignorer</button>
</div>
<div class="editor-toolbar">
<div class="toolbar-buttons">
<button type="button" onclick="wrap('**','**')" title="Ctrl+B"><b>B</b></button>
<button type="button" onclick="wrap('*','*')" title="Ctrl+I"><i>I</i></button>
<button type="button" onclick="prefix('## ')" title="Heading">H</button>
<button type="button" onclick="wrapLink()" title="Ctrl+K">Lien</button>
<button type="button" onclick="prefix('- ')">Liste</button>
<button type="button" onclick="wrap('`','`')">Code</button>
<button type="button" onclick="insertTable()">Table</button>
</div>
<button type="button" class="toolbar-btn" onclick="togglePreview()" id="preview-toggle">preview</button>
<label class="toolbar-upload"><input type="file" id="upload-input" style="display:none" accept="image/*,.pdf">upload</label>
<span class="toolbar-hint">Ctrl+S sauvegarder | [[ autocomplete</span>
</div>
<form method="POST" action="/save/{{encodeURL .Page.URLPath}}" class="edit-form" id="edit-form">
<div class="editor-split" id="editor-split">
<div style="position:relative;flex:1">
<textarea name="content" class="edit-textarea" id="editor" spellcheck="false">{{.Content}}</textarea>
<div id="wl-autocomplete" class="wl-autocomplete"></div>
</div>
<div class="preview-pane" id="preview-pane" style="display:none">
<div class="preview-content page-content" id="preview-content"></div>
</div>
</div>
<div class="edit-actions">
<button type="submit" class="btn-save">sauvegarder</button>
<a href="/wiki/{{encodeURL .Page.URLPath}}" class="btn-cancel">annuler</a>
</div>
</form>
{{end}}
{{define "scripts"}}
<script>
var editor = document.getElementById('editor');
var previewPane = document.getElementById('preview-pane');
var previewContent = document.getElementById('preview-content');
var editorSplit = document.getElementById('editor-split');
var previewVisible = false;
var previewTimer = null;
var pagePath = '{{.Page.URLPath}}';
var draftKey = 'draft:' + pagePath;
// === Preview ===
function togglePreview() {
previewVisible = !previewVisible;
previewPane.style.display = previewVisible ? 'block' : 'none';
editorSplit.classList.toggle('split-active', previewVisible);
document.getElementById('preview-toggle').textContent = previewVisible ? 'masquer' : 'preview';
if (previewVisible) updatePreview();
}
function updatePreview() {
if (!previewVisible) return;
clearTimeout(previewTimer);
previewTimer = setTimeout(function() {
fetch('/api/preview?content=' + encodeURIComponent(editor.value))
.then(function(r) { return r.text(); })
.then(function(html) { previewContent.innerHTML = html; hljs.highlightAll(); });
}, 300);
}
editor.addEventListener('input', function() { updatePreview(); saveDraft(); });
// === Toolbar ===
function wrap(before, after) {
var s = editor.selectionStart, e = editor.selectionEnd;
var sel = editor.value.substring(s, e) || 'texte';
editor.value = editor.value.substring(0, s) + before + sel + after + editor.value.substring(e);
editor.selectionStart = s + before.length;
editor.selectionEnd = s + before.length + sel.length;
editor.focus();
}
function prefix(pre) {
var s = editor.selectionStart;
var lineStart = editor.value.lastIndexOf('\n', s - 1) + 1;
editor.value = editor.value.substring(0, lineStart) + pre + editor.value.substring(lineStart);
editor.selectionStart = editor.selectionEnd = s + pre.length;
editor.focus();
}
function wrapLink() {
var s = editor.selectionStart, e = editor.selectionEnd;
var sel = editor.value.substring(s, e) || 'texte';
var ins = '[' + sel + '](url)';
editor.value = editor.value.substring(0, s) + ins + editor.value.substring(e);
editor.selectionStart = s + sel.length + 3;
editor.selectionEnd = s + sel.length + 6;
editor.focus();
}
function insertTable() {
var s = editor.selectionStart;
var tbl = '\n| Colonne 1 | Colonne 2 |\n|-----------|----------|\n| | |\n';
editor.value = editor.value.substring(0, s) + tbl + editor.value.substring(s);
editor.focus();
}
// === Keyboard shortcuts ===
editor.addEventListener('keydown', function(e) {
if (e.key === 'Tab') { e.preventDefault(); var s = this.selectionStart; this.value = this.value.substring(0, s) + '\t' + this.value.substring(this.selectionEnd); this.selectionStart = this.selectionEnd = s + 1; }
if (e.ctrlKey && e.key === 's') { e.preventDefault(); document.getElementById('edit-form').submit(); }
if (e.ctrlKey && e.key === 'b') { e.preventDefault(); wrap('**', '**'); }
if (e.ctrlKey && e.key === 'i') { e.preventDefault(); wrap('*', '*'); }
if (e.ctrlKey && e.key === 'k') { e.preventDefault(); wrapLink(); }
});
// === Wikilink autocomplete ===
var wlBox = document.getElementById('wl-autocomplete');
var wlTimer = null;
editor.addEventListener('input', function() {
clearTimeout(wlTimer);
var pos = this.selectionStart;
var text = this.value.substring(0, pos);
var openIdx = text.lastIndexOf('[[');
var closeIdx = text.lastIndexOf(']]');
if (openIdx < 0 || closeIdx > openIdx) { wlBox.style.display = 'none'; return; }
var query = text.substring(openIdx + 2);
if (query.length < 1) { wlBox.style.display = 'none'; return; }
wlTimer = setTimeout(function() {
fetch('/api/suggest?q=' + encodeURIComponent(query))
.then(function(r) { return r.json(); })
.then(function(items) {
if (!items.length) { wlBox.style.display = 'none'; return; }
wlBox.innerHTML = items.map(function(item) {
return '<a class="suggestion" data-path="' + item.path + '" href="#"><span class="sug-title">' + item.title + '</span><span class="sug-domain">' + item.domain + '</span></a>';
}).join('');
wlBox.style.display = 'block';
});
}, 150);
});
wlBox.addEventListener('click', function(e) {
e.preventDefault();
var link = e.target.closest('.suggestion');
if (!link) return;
var path = link.dataset.path;
var pos = editor.selectionStart;
var text = editor.value.substring(0, pos);
var openIdx = text.lastIndexOf('[[');
editor.value = editor.value.substring(0, openIdx) + '[[' + path + ']]' + editor.value.substring(pos);
editor.selectionStart = editor.selectionEnd = openIdx + path.length + 4;
wlBox.style.display = 'none';
editor.focus();
});
editor.addEventListener('keydown', function(e) {
if (wlBox.style.display === 'block' && e.key === 'Escape') { wlBox.style.display = 'none'; e.stopPropagation(); }
if (wlBox.style.display === 'block' && e.key === 'Enter') {
var first = wlBox.querySelector('.suggestion');
if (first) { e.preventDefault(); first.click(); }
}
});
// === Drag & Drop upload ===
editor.addEventListener('dragover', function(e) { e.preventDefault(); this.classList.add('editor-dragover'); });
editor.addEventListener('dragleave', function() { this.classList.remove('editor-dragover'); });
editor.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('editor-dragover');
var files = e.dataTransfer.files;
for (var i = 0; i < files.length; i++) {
(function(file) {
var form = new FormData();
form.append('file', file);
form.append('dir', '{{.Page.Domain}}');
fetch('/upload', {method: 'POST', body: form})
.then(function(r) { return r.json(); })
.then(function(data) {
var pos = editor.selectionStart;
editor.value = editor.value.substring(0, pos) + '\n' + data.markdown + '\n' + editor.value.substring(pos);
updatePreview();
});
})(files[i]);
}
});
// === Clipboard image paste ===
editor.addEventListener('paste', function(e) {
var items = (e.clipboardData || {}).items;
if (!items) return;
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
e.preventDefault();
var file = items[i].getAsFile();
var form = new FormData();
form.append('file', file, 'paste-' + Date.now() + '.png');
form.append('dir', '{{.Page.Domain}}');
fetch('/upload', {method: 'POST', body: form})
.then(function(r) { return r.json(); })
.then(function(data) {
var pos = editor.selectionStart;
editor.value = editor.value.substring(0, pos) + '\n' + data.markdown + '\n' + editor.value.substring(pos);
updatePreview();
});
break;
}
}
});
// === Auto-close markdown pairs ===
editor.addEventListener('keydown', function(e) {
var pairs = {'(': ')', '[': ']', '{': '}', '`': '`', '*': '*', '_': '_'};
if (pairs[e.key] && this.selectionStart === this.selectionEnd) {
// Don't auto-close if next char is alphanumeric
var next = this.value[this.selectionStart] || '';
if (/\w/.test(next)) return;
}
if (pairs[e.key] && this.selectionStart !== this.selectionEnd) {
e.preventDefault();
var s = this.selectionStart, end = this.selectionEnd;
var sel = this.value.substring(s, end);
this.value = this.value.substring(0, s) + e.key + sel + pairs[e.key] + this.value.substring(end);
this.selectionStart = s + 1;
this.selectionEnd = end + 1;
}
});
// === Find & Replace ===
document.addEventListener('keydown', function(e) {
if ((e.ctrlKey || e.metaKey) && e.key === 'h' && document.activeElement === editor) {
e.preventDefault();
var find = prompt('Chercher:');
if (!find) return;
var replace = prompt('Remplacer par:');
if (replace === null) return;
var count = (editor.value.match(new RegExp(find.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
if (count > 0 && confirm('Remplacer ' + count + ' occurrence(s) ?')) {
editor.value = editor.value.split(find).join(replace);
updatePreview();
}
}
});
// === Snippets ===
var snippets = {
'/date': new Date().toISOString().substring(0,10),
'/time': new Date().toLocaleTimeString('fr-FR', {hour:'2-digit',minute:'2-digit'}),
'/todo': '- [ ] ',
'/hr': '\n---\n',
'/h1': '# ', '/h2': '## ', '/h3': '### ',
'/table': '| Col1 | Col2 |\n|------|------|\n| | |\n',
'/link': '[[]]',
'/img': '![alt](url)',
};
editor.addEventListener('input', function() {
var pos = this.selectionStart;
var text = this.value.substring(Math.max(0, pos - 10), pos);
for (var key in snippets) {
if (text.endsWith(key)) {
var before = this.value.substring(0, pos - key.length);
var after = this.value.substring(pos);
this.value = before + snippets[key] + after;
this.selectionStart = this.selectionEnd = before.length + snippets[key].length;
break;
}
}
});
// === Diff before save ===
var originalContent = editor.value;
document.getElementById('edit-form').addEventListener('submit', function(e) {
if (editor.value === originalContent) {
if (!confirm('Aucune modification detectee. Sauvegarder quand meme ?')) {
e.preventDefault();
}
}
});
// === Auto-save localStorage ===
function saveDraft() {
try { localStorage.setItem(draftKey, JSON.stringify({content: editor.value, ts: Date.now()})); } catch(e) {}
}
function restoreDraft() {
try {
var d = JSON.parse(localStorage.getItem(draftKey));
if (d && d.content) { editor.value = d.content; updatePreview(); }
} catch(e) {}
document.getElementById('draft-banner').style.display = 'none';
}
function dismissDraft() {
localStorage.removeItem(draftKey);
document.getElementById('draft-banner').style.display = 'none';
}
// Check for draft on load
(function() {
try {
var d = JSON.parse(localStorage.getItem(draftKey));
if (d && d.content && d.content !== editor.value && d.ts) {
document.getElementById('draft-banner').style.display = 'flex';
}
} catch(e) {}
})();
// Auto-save interval
setInterval(saveDraft, 5000);
// Clear draft on successful submit
document.getElementById('edit-form').addEventListener('submit', function() {
localStorage.removeItem(draftKey);
});
// === Upload button ===
document.getElementById('upload-input').addEventListener('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var form = new FormData();
form.append('file', file);
form.append('dir', '{{.Page.Domain}}');
fetch('/upload', {method: 'POST', body: form})
.then(function(r) { return r.json(); })
.then(function(data) {
var pos = editor.selectionStart;
editor.value = editor.value.substring(0, pos) + '\n' + data.markdown + '\n' + editor.value.substring(pos);
updatePreview();
});
});
</script>
{{end}}