Fassung 1.5.1: der Werkzeugkasten kennt den Zusammenhang
Umuts Einwand: Zeile und Spalte standen oben in der Auswahl, als waeren sie Seitenanfaenge. Sie gehoeren in den Inhalt - erst eine Sektion, dann eine Zeile darin. Das war nicht nur eine Anzeigefrage: eine Zeile nahm bisher jedes Kind an, eine Ueberschrift konnte neben den Spalten landen, und eine Spalte durfte ueberall stehen. Die Struktur, die der Editor in seinem eigenen Hinweistext beschreibt, galt fuer die Schaltflaechen nicht. Uebernommen ist Flatsomes Regelmodell (Elements.php): ALLOW - eine Zeile nimmt nur Spalten; REQUIRE - eine Spalte lebt nur in einer Zeile. Dazu ein Zusatz, den Flatsome nicht hat: ROOT_NEEDS_CONTAINER - eine Zeile ist kein Seitenanfang. In jedem Behaelter bleibt sie erlaubt, auch in einem Tab. Daraus die Bedienung: - Werkzeugkasten nach Aufbau gruppiert statt flacher Liste aus 31 Typen. - Was nicht hingehoert, ist gesperrt und sagt beim Ueberfahren warum. Eine Regel, die man erklaert bekommt, lernt man; eine Schaltflaeche, die nur nicht mehr reagiert, drueckt man weiter. - Was moeglich ist, sagt wohin es kommt. - Die Zeile wird nach Spaltenzahl angeboten und bringt ihre Spalten mit; der separate Zeilenbereich ist entfallen. - insertionTarget beantwortet "wohin kaeme das?", ohne etwas einzufuegen. Knopf, Einfuegepunkt, Ziehen und die Sperrlogik nutzen dieselbe Funktion - vorher entschied insertBlock allein, und insertRow hatte eine zweite Kopie derselben Platzierungslogik. Ein Element, das der gewaehlte Behaelter nicht nimmt, wird nicht gesperrt, sondern steigt auf, bis ein Elternteil es annimmt. Nebenbei behoben: postnav fehlte seit Schema v18 in labels und icons, der Strukturbaum zeigte dafuer "Unbekannt" und ein Fragezeichen. Geprueft: npm test 27/27, phpunit 446/446, npm run test:editor vollstaendig mit neuem Abschnitt und ohne axe-Verstoss. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
22e4ada830
commit
0e2334d30f
8 files changed
+397
-74
No files matched your search
+86
-15
@@ -1,4 +1,4 @@
|
||||
import { EditorHistory, labels, icons, blockLabel, blockIcon, isPluginBlock, setStyle, effectiveStyle, locate, clone, isContainer, canContain, insertBlock, insertBlockAt, moveBlockTo, duplicateBlock, moveBlock, devices, effectiveDesign, setDesign, blockPath, insertPreset, insertRow, setEffect, insertMedia, upgradeDocument } from './editor-model.js';
|
||||
import { EditorHistory, labels, icons, blockLabel, blockIcon, isPluginBlock, setStyle, effectiveStyle, locate, clone, isContainer, canContain, insertBlock, insertBlockAt, insertionTarget, moveBlockTo, duplicateBlock, moveBlock, devices, effectiveDesign, setDesign, blockPath, insertPreset, insertRow, insertRowAt, setEffect, insertMedia, upgradeDocument, CATEGORIES, ROW_COLUMNS, whyNot, dropList } from './editor-model.js';
|
||||
import { openMedia } from './media.js';
|
||||
import { createDesignPanel } from './design-panel.js';
|
||||
import { createRichText } from './rich-text.js';
|
||||
@@ -65,8 +65,7 @@ function controls() {
|
||||
$('#nest').disabled ||= current.index === 0 || !canContain(current.siblings[current.index - 1], current.block);
|
||||
$('#outdent').disabled ||= !current.parent;
|
||||
}
|
||||
$('#library').querySelectorAll('button').forEach(button => button.disabled = !ready || busy);
|
||||
$('#rows').querySelectorAll('button').forEach(button => button.disabled = !ready || busy);
|
||||
updateLibrary();
|
||||
$('#presets').querySelectorAll('button').forEach(button => button.disabled = !ready || busy);
|
||||
$('#properties').querySelectorAll('input,textarea,select,button').forEach(input => input.disabled = busy);
|
||||
document.querySelectorAll('[data-panel],[data-viewport],#tree-search,#expand-all,#collapse-all').forEach(input => input.disabled = !ready || busy);
|
||||
@@ -718,6 +717,7 @@ function addElement(type) {
|
||||
/** Marks where the next added element goes, and says so instead of leaving it invisible. */
|
||||
function setPendingInsert(target) {
|
||||
pendingInsert = target;
|
||||
updateLibrary();
|
||||
$('#toolbox').classList.toggle('awaiting-insert', Boolean(target));
|
||||
$('#insert-hint').hidden = !target;
|
||||
if (target) {
|
||||
@@ -726,17 +726,84 @@ function setPendingInsert(target) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// The toolbox.
|
||||
//
|
||||
// It is built here rather than listed in the template because it has to answer a question that
|
||||
// only the current selection can answer: what fits where the next element would go. A flat list
|
||||
// of every type offered a row on an empty page and a column next to nothing - the structure the
|
||||
// editor describes in its own hint was not the structure the buttons followed.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
function buildLibrary() {
|
||||
const library = $('#library');
|
||||
library.replaceChildren();
|
||||
for (const category of CATEGORIES) {
|
||||
const group = document.createElement('div'); group.className = 'library-group';
|
||||
const heading = document.createElement('h4'); heading.className = 'library-group-title'; heading.textContent = category.name;
|
||||
group.append(heading);
|
||||
if (category.hint) {
|
||||
const hint = document.createElement('p'); hint.className = 'library-group-hint'; hint.textContent = category.hint;
|
||||
group.append(hint);
|
||||
}
|
||||
const list = document.createElement('div'); list.className = 'library-buttons';
|
||||
for (const type of category.types) {
|
||||
// A row without columns is an empty box, so it is offered by column count instead - the
|
||||
// shape people actually want when they reach for a row.
|
||||
for (const columns of 'row' === type ? ROW_COLUMNS : [null]) {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button'; button.draggable = true;
|
||||
button.dataset.add = type;
|
||||
if (columns) button.dataset.row = String(columns);
|
||||
const glyph = document.createElement('span'); glyph.setAttribute('aria-hidden', 'true');
|
||||
glyph.textContent = columns ? '▥'.repeat(Math.min(columns, 4)) : icons[type];
|
||||
if (columns) glyph.className = 'library-row-glyph';
|
||||
const text = document.createElement('span');
|
||||
text.textContent = columns ? `${labels[type]}, ${columns} Spalten` : labels[type];
|
||||
button.append(glyph, text);
|
||||
list.append(button);
|
||||
}
|
||||
}
|
||||
group.append(list);
|
||||
library.append(list.children.length ? group : document.createComment('leer'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Greys out what cannot go where the next element would land, and says where it will land when
|
||||
* it can. The answer comes from the same function the click follows, so the two cannot disagree.
|
||||
*/
|
||||
function updateLibrary() {
|
||||
const ready = Boolean(history);
|
||||
for (const button of $('#library').querySelectorAll('button')) {
|
||||
const type = button.dataset.add;
|
||||
if (!ready || busy) { button.disabled = true; button.removeAttribute('title'); continue; }
|
||||
const target = pendingInsert
|
||||
? (dropList(history.document, pendingInsert.parentId, { type }) ? pendingInsert : null)
|
||||
: insertionTarget(history.document, selected, type);
|
||||
button.disabled = !target;
|
||||
if (!target) { button.title = whyNot(type); continue; }
|
||||
const parent = target.parentId ? locate(history.document.blocks, target.parentId)?.block : null;
|
||||
button.title = `Wird in ${parent ? blockLabel(parent) : 'die Seite'} eingefügt.`;
|
||||
}
|
||||
}
|
||||
|
||||
$('#library').addEventListener('click', event => {
|
||||
const type = event.target.closest('[data-add]')?.dataset.add;
|
||||
if (type) addElement(type);
|
||||
const button = event.target.closest('[data-add]');
|
||||
if (!button || button.disabled) return;
|
||||
if (button.dataset.row) {
|
||||
if (busy || !history) return;
|
||||
const at = pendingInsert;
|
||||
setPendingInsert(null);
|
||||
const id = history.change(doc => at
|
||||
? insertRowAt(doc, button.dataset.row, at.parentId, at.index)
|
||||
: insertRow(doc, button.dataset.row, selected));
|
||||
if (id) { selected = id; propertyPanel = 'design'; changed(); }
|
||||
return;
|
||||
}
|
||||
addElement(button.dataset.add);
|
||||
});
|
||||
$('#cancel-insert').addEventListener('click', () => setPendingInsert(null));
|
||||
// A row with columns, in one click: the piece people actually look for when they want a row.
|
||||
$('#rows').addEventListener('click', event => {
|
||||
const columns = event.target.closest('[data-row]')?.dataset.row;
|
||||
if (!columns || busy || !history) return;
|
||||
selected = history.change(doc => insertRow(doc, columns, selected)); propertyPanel = 'design'; changed();
|
||||
});
|
||||
$('#presets').addEventListener('click', event => {
|
||||
const name = event.target.closest('[data-preset]')?.dataset.preset;
|
||||
if (!name || busy || !history) return;
|
||||
@@ -834,7 +901,9 @@ overlay = createCanvasOverlay({
|
||||
onDrop: (payload, plan) => {
|
||||
if (busy || !history) return;
|
||||
if (payload.kind === 'add') {
|
||||
const id = history.change(doc => insertBlockAt(doc, payload.type, plan.parentId, plan.index));
|
||||
const id = history.change(doc => payload.columns
|
||||
? insertRowAt(doc, payload.columns, plan.parentId, plan.index)
|
||||
: insertBlockAt(doc, payload.type, plan.parentId, plan.index));
|
||||
if (id) { selected = id; changed(); }
|
||||
return;
|
||||
}
|
||||
@@ -847,12 +916,13 @@ overlay = createCanvasOverlay({
|
||||
// toolbox remains usable with the keyboard alone.
|
||||
$('#library').addEventListener('dragstart', event => {
|
||||
const button = event.target.closest('[data-add]');
|
||||
if (!button || busy || !history) { event.preventDefault(); return; }
|
||||
if (!button || button.disabled || busy || !history) { event.preventDefault(); return; }
|
||||
const type = button.dataset.add;
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
event.dataTransfer.setData('text/plain', button.dataset.add);
|
||||
event.dataTransfer.setData('text/plain', type);
|
||||
// A stand-in carrying only the type: the drop rules ask nothing else of a block, and the real
|
||||
// one does not exist until it lands.
|
||||
overlay.startDrag({ kind: 'add', type: button.dataset.add, block: { type: button.dataset.add } });
|
||||
overlay.startDrag({ kind: 'add', type, columns: button.dataset.row ?? null, block: { type } });
|
||||
});
|
||||
$('#library').addEventListener('dragend', () => overlay.endDrag());
|
||||
|
||||
@@ -907,4 +977,5 @@ window.addEventListener('keydown', event => {
|
||||
if (key === 'z' || key === 'y') { event.preventDefault(); (event.shiftKey || key === 'y') ? history.redo() : history.undo(); changed(); }
|
||||
});
|
||||
window.addEventListener('beforeunload', event => { if (history?.dirty || busy) { event.preventDefault(); event.returnValue = ''; } });
|
||||
buildLibrary();
|
||||
load();
|
||||
Reference in new issue
Block a user