Fassung 1.6.2: sichtbare leere Layouts und direktes Befuellen im Editor
CMS quality / php (8.2) (push) Canceled after 0s
CMS quality / php (8.4) (push) Canceled after 0s
CMS quality / browser (push) Canceled after 0s
CMS quality / mysql (push) Canceled after 0s

This commit is contained in:
kantarci committed 2026-09-14 09:22:23 +02:00
1 parent 149e1fe520
commit 6a044e59e9
14 files changed
+314 -21

No files matched your search

+56 -8
View File
@@ -4,6 +4,7 @@ import { createDesignPanel } from './design-panel.js';
import { createRichText } from './rich-text.js';
import { createCanvasOverlay } from './canvas-overlay.js';
import { preview, rowPreview } from './library-previews.js';
import { decorateContainers } from './preview-containers.js';
// Mirrors DocumentValidator::ICONS; the server rejects anything outside this list.
const ICONS = ['app', 'web', 'design', 'network', 'hardware', 'shop', 'support', 'shield', 'clock', 'star', 'phone', 'mail', 'pin', 'check'];
@@ -24,6 +25,7 @@ let device = 'desktop', propertyPanel = 'content', designTokens, designSchema, d
let previewScale = 1, overlay = null, pendingInsert = null;
const collapsed = new Set();
let renderedProperties = null;
let revealAfterPreview = null;
function notify(message = '', recover = false) {
$('#message').textContent = message;
@@ -114,6 +116,7 @@ function renderTree() {
function choose(id, scroll = true) {
if (busy) return;
if (id !== selected && pendingInsert) setPendingInsert(null);
selected = locate(history.document.blocks, id) ? id : null;
blockPath(history.document.blocks, selected).slice(0, -1).forEach(block => collapsed.delete(block.id));
if (!selected) propertyPanel = 'content';
@@ -595,7 +598,13 @@ async function updatePreview() {
const doc = frame.contentDocument;
if (!doc || doc === altesDokument || !doc.getElementById('kant-styles')) return;
angewandt = true;
const style = doc.createElement('link'); style.rel = 'stylesheet'; style.href = '/assets/preview.css'; doc.head.append(style);
const style = doc.createElement('link'); style.rel = 'stylesheet'; style.href = `/assets/preview.css?v=${new URL(import.meta.url).searchParams.get('v') ?? 'dev'}`;
// Register before insertion: cached loads (or errors) can precede DOMContentLoaded.
const styleReady = new Promise(resolve => {
style.addEventListener('load', () => resolve(true), { once: true });
style.addEventListener('error', () => resolve(false), { once: true });
});
doc.head.append(style);
// The preview delivers the generated stylesheet as inert template text (see page.html.twig).
// Applying it as a constructed stylesheet is not a source in the sense of the policy, so the
// preview shows exactly what the published page shows, without a single blocked style.
@@ -607,6 +616,22 @@ async function updatePreview() {
}
doc.addEventListener('click', event => {
event.preventDefault();
const insert = event.target.closest('[data-editor-target]');
if (insert && !busy && history) {
stopInlineEdit();
const id = insert.dataset.editorTarget;
if (!locate(history.document.blocks, id)) return;
choose(id, false);
if (insert.hasAttribute('data-editor-row')) {
setPendingInsert(null);
selected = history.change(content => insertRowAt(content, Number(insert.dataset.editorRow), id, 0));
propertyPanel = 'content'; changed();
} else if (locate(history.document.blocks, id).block.type === 'row') {
selected = history.change(content => insertBlockAt(content, 'column', id, 0));
changed();
} else setPendingInsert({ parentId: id, index: 0 });
return;
}
if (inlineEdit) return;
const element = event.target.closest('[id]');
if (element && locate(history.document.blocks, element.id)) choose(element.id, false);
@@ -618,12 +643,23 @@ async function updatePreview() {
if (block && INLINE_TEXT[block.type]) { event.preventDefault(); choose(element.id, false); startInlineEdit(element.id); }
});
doc.addEventListener('submit', event => event.preventDefault());
// Every refresh is a new document, so the layer above it has to bind to it again.
overlay?.attach();
frame.contentWindow.scrollTo(0, top); highlight();
// Erst jetzt ansehen lassen: bis hierher zeigte das Fenster eine Seite ohne Gestaltung.
frame.style.visibility = '';
$('#preview-status').textContent = 'Entwurf · Klicken wählt aus, Doppelklick bearbeitet Text, Elemente lassen sich hierher ziehen.';
// The head may arrive before the blocks. Decorate only once the complete DOM exists.
const finish = () => {
if (sequence !== previewSequence || doc !== frame.contentDocument) return;
decorateContainers(doc, history.document);
overlay?.attach();
styleReady.then(loaded => {
if (sequence !== previewSequence || doc !== frame.contentDocument) return;
frame.contentWindow.scrollTo(0, top);
highlight(revealAfterPreview === selected); revealAfterPreview = null;
markInsertTarget();
frame.style.visibility = '';
$('#preview-status').textContent = loaded
? 'Entwurf · Klicken wählt aus, Doppelklick bearbeitet Text, Elemente lassen sich hierher ziehen.'
: 'Editor-Hilfsflächen konnten nicht geladen werden. Bitte den Editor neu laden.';
});
};
if (doc.readyState === 'loading') doc.addEventListener('DOMContentLoaded', finish, { once: true }); else finish();
};
// Solange die Gestaltung fehlt, bleibt die Fläche leer statt falsch.
frame.style.visibility = 'hidden';
@@ -654,6 +690,7 @@ async function updatePreview() {
function changed() {
if (!locate(history.document.blocks, selected)) selected = null;
if (selected && !$('#preview').contentDocument?.getElementById(selected)) revealAfterPreview = selected;
renderTree(); renderProperties(); controls(); schedulePreview();
}
@@ -727,12 +764,23 @@ function setPendingInsert(target) {
updateLibrary();
$('#toolbox').classList.toggle('awaiting-insert', Boolean(target));
$('#insert-hint').hidden = !target;
markInsertTarget();
if (target) {
if ($('#toolbox').hidden) $('#toggle-library').click();
$('#toolbox').scrollIntoView({ block: 'nearest' });
$('#library-search').value = ''; filterLibrary();
const name = target.parentId ? $('#preview').contentDocument?.getElementById(target.parentId)?.dataset.editorLabel || blockLabel(locate(history.document.blocks, target.parentId)?.block) : 'Seite';
$('#insert-hint').firstChild.textContent = `Einfügen in ${name}. Wähle jetzt ein Element. `;
$('#library-search').focus({ preventScroll: true });
$('#library-search').scrollIntoView({ block: 'nearest' });
}
}
function markInsertTarget() {
const doc = $('#preview').contentDocument;
doc?.querySelectorAll('.editor-insert-target').forEach(node => node.classList.remove('editor-insert-target'));
if (pendingInsert?.parentId) doc?.getElementById(pendingInsert.parentId)?.classList.add('editor-insert-target');
}
// ---------------------------------------------------------------------------------------------
// The toolbox.
//