Basic site editor

This commit is contained in:
Simon Martens
2026-01-14 18:52:57 +01:00
parent 3732b128db
commit f724cdf975
549 changed files with 324463 additions and 80 deletions

View File

@@ -13,18 +13,11 @@
<div class="flex flex-col justify-end gap-y-4 pr-4">
<div class="inputwrapper !mb-0">
<div class="inputlabelrow">
<label for="page-editor-select" class="inputlabel">Seite auswählen</label>
</div>
<select
id="page-editor-select"
name="key"
class="inputinput"
hx-get="/redaktion/seiten/form/"
hx-trigger="change"
hx-target="#page-editor-form"
hx-swap="outerHTML"
hx-indicator="body">
class="inputinput px-3 py-2 bg-white border border-slate-300 shadow-sm focus:border-slate-500 focus:ring-2 focus:ring-slate-400/30"
onchange="window.location.href = '/redaktion/seiten/?key=' + encodeURIComponent(this.value);">
{{- if $model.pages -}}
{{- range $page := $model.pages -}}
<option value="{{ $page.Key }}" {{ if eq $page.Key $model.selected_key }}selected{{ end }}>{{ $page.Title }}</option>
@@ -41,3 +34,115 @@
<div class="container-normal mx-auto mt-4 !px-0">
{{ template "_page_form" $model }}
</div>
<script>
(function () {
if (window.pageEditorTinyMCEInit) {
return;
}
window.pageEditorTinyMCEInit = true;
const selector = "textarea.page-html-editor";
const formId = "page-editor-form";
function initEditors(root) {
if (!window.tinymce) return;
cleanupEditors(document);
const container = root || document;
const targets = Array.from(container.querySelectorAll(selector));
if (targets.length === 0) return;
targets.forEach((textarea) => {
if (!textarea.id) {
textarea.id = "page-html-editor-" + Math.random().toString(36).slice(2);
}
const existing = tinymce.get(textarea.id);
if (existing) {
existing.remove();
}
});
targets.reduce((chain, textarea) => {
return chain.then(() => {
if (tinymce.get(textarea.id)) return Promise.resolve();
textarea.style.visibility = "hidden";
return tinymce.init({
target: textarea,
base_url: "/assets/vendor/tinymce",
suffix: ".min",
license_key: "gpl",
menubar: false,
branding: false,
plugins: "advlist lists link code autoresize",
toolbar: "undo redo | blocks | bold italic | bullist numlist | outdent indent | link | code removeformat",
block_formats: "Absatz=p;Überschrift 2=h2;Überschrift 3=h3;Zitat=blockquote;Vorformatiert=pre",
forced_root_block: "p",
autoresize_bottom_margin: 16,
content_style: "body { font-family: inherit; font-size: 1rem; } p { margin: 0 0 0.75rem; }",
init_instance_callback: function (editor) {
if (editor && editor.targetElm) {
editor.targetElm.style.visibility = "";
}
},
});
});
}, Promise.resolve());
}
function cleanupEditors(root) {
if (!window.tinymce) return;
tinymce.remove();
}
function waitForTinyMCE(callback) {
if (window.tinymce) {
callback();
return;
}
let retries = 0;
const timer = setInterval(() => {
if (window.tinymce || retries > 40) {
clearInterval(timer);
if (window.tinymce) callback();
return;
}
retries += 1;
}, 50);
}
waitForTinyMCE(() => initEditors(document));
window.PageEditorCleanup = function () {
cleanupEditors(document);
};
window.PageEditorInit = function () {
const form = document.getElementById(formId);
waitForTinyMCE(() => {
requestAnimationFrame(() => {
setTimeout(() => initEditors(form || document), 0);
});
});
};
const observer = new MutationObserver((mutations) => {
let shouldInit = false;
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (!(node instanceof Element)) return;
if (node.id === formId) {
shouldInit = true;
return;
}
if (node.querySelector && node.querySelector(selector)) {
shouldInit = true;
}
});
});
if (shouldInit) {
window.PageEditorInit();
}
});
observer.observe(document.body, { childList: true, subtree: true });
})();
</script>

View File

@@ -57,75 +57,18 @@
</div>
</div>
<trix-toolbar id="page-html-toolbar-{{ $index }}">
<div class="trix-toolbar-container">
<span class="trix-toolbar-group">
<button type="button" class="trix-toolbar-button" data-trix-attribute="bold" data-trix-key="b" title="Fett">
<i class="ri-bold"></i>
</button>
<button type="button" class="trix-toolbar-button" data-trix-attribute="italic" data-trix-key="i" title="Kursiv">
<i class="ri-italic"></i>
</button>
<button type="button" class="trix-toolbar-button" data-trix-attribute="strike" title="Gestrichen">
<i class="ri-strikethrough"></i>
</button>
<button type="button" class="trix-toolbar-button" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" title="Link">
<i class="ri-links-line"></i>
</button>
</span>
<span class="trix-toolbar-group">
<button type="button" class="trix-toolbar-button" data-trix-attribute="heading1" title="Überschrift">
<i class="ri-h-1"></i>
</button>
<button type="button" class="trix-toolbar-button" data-trix-attribute="quote" title="Zitat">
<i class="ri-double-quotes-l"></i>
</button>
<button type="button" class="trix-toolbar-button" data-trix-attribute="bullet" title="Liste">
<i class="ri-list-unordered"></i>
</button>
<button type="button" class="trix-toolbar-button" data-trix-attribute="number" title="Aufzählung">
<i class="ri-list-ordered"></i>
</button>
<button type="button" class="trix-toolbar-button" data-trix-action="decreaseNestingLevel" title="Einzug verkleinern">
<i class="ri-indent-decrease"></i>
</button>
<button type="button" class="trix-toolbar-button" data-trix-action="increaseNestingLevel" title="Einzug vergrößern">
<i class="ri-indent-increase"></i>
</button>
</span>
<span class="trix-toolbar-group">
<button type="button" class="trix-toolbar-button" data-trix-action="undo" data-trix-key="z" title="Rückgängig">
<i class="ri-arrow-go-back-line"></i>
</button>
<button type="button" class="trix-toolbar-button" data-trix-action="redo" data-trix-key="shift+z" title="Wiederholen">
<i class="ri-arrow-go-forward-line"></i>
</button>
</span>
</div>
<div class="trix-dialogs" data-trix-dialogs>
<div class="trix-dialog trix-dialog--link" data-trix-dialog="href" data-trix-dialog-attribute="href">
<div class="trix-dialog__link-fields flex flex-row">
<input type="url" name="href" class="trix-input trix-input--dialog" placeholder="URL eingeben…" aria-label="URL" required data-trix-input>
<div class="trix-button-group flex-row">
<input type="button" class="trix-button trix-button--dialog" value="Link" data-trix-method="setAttribute">
<input type="button" class="trix-button trix-button--dialog" value="Unlink" data-trix-method="removeAttribute">
</div>
</div>
</div>
</div>
</trix-toolbar>
<textarea hidden id="page-html-{{ $index }}" name="html[{{ $section.Key }}]" autocomplete="off">{{- $section.HTML -}}</textarea>
<trix-editor input="page-html-{{ $index }}" toolbar="page-html-toolbar-{{ $index }}"></trix-editor>
<textarea
id="page-html-{{ $index }}"
name="html[{{ $section.Key }}]"
class="inputinput page-html-editor"
rows="12"
autocomplete="off">{{- $section.HTML -}}</textarea>
</div>
{{- end -}}
{{- end -}}
<div class="flex justify-end mt-6">
<button type="submit" class="btn bg-slate-800 text-white px-4 py-2 rounded-xs hover:bg-slate-900">
<button type="submit" class="submitbutton w-40 flex items-center gap-2 justify-center">
<i class="ri-save-line"></i> Speichern
</button>
</div>

View File

@@ -1 +1,2 @@
<title>Seiteneditor</title>
<script src="/assets/vendor/tinymce/tinymce.min.js" defer></script>