@kubex/zinc 1.1.95 → 1.1.97
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/custom-elements.json +193 -428
- package/dist/vscode.html-custom-data.json +1 -1
- package/dist/web-types.json +3 -3
- package/dist/zn.d.ts +113 -224
- package/dist/zn.min.js +343 -384
- package/docs/pages/components/flow-builder.md +17 -1
- package/docs/pages/components/page-builder.md +21 -92
- package/package.json +1 -1
- package/src/components/data-table/data-table.component.ts +13 -8
- package/src/components/data-table/data-table.scss +1 -1
- package/src/components/flow-builder/flow-builder.component.ts +16 -1
- package/src/components/flow-builder/flow-builder.test.ts +206 -1
- package/src/components/flow-builder/flow-geometry.test.ts +102 -0
- package/src/components/flow-builder/flow.types.ts +82 -15
- package/src/components/flow-builder/modules/flow-canvas/flow-canvas.component.ts +163 -108
- package/src/components/flow-builder/modules/flow-step/flow-step.component.ts +2 -0
- package/src/components/page-builder/page-builder.component.ts +229 -429
- package/src/components/page-builder/page-builder.scss +10 -64
- package/src/components/page-builder/page-builder.test.ts +162 -614
- package/src/components/page-builder/page.types.ts +7 -75
- package/src/components/remarkd-editor/remarkd-editor.component.ts +37 -11
- package/src/components/remarkd-editor/remarkd-editor.test.ts +79 -0
- package/src/components/page-builder/page-tree.test.ts +0 -483
- package/src/components/page-builder/page-tree.ts +0 -329
|
@@ -1,36 +1,13 @@
|
|
|
1
|
+
import { type CSSResultGroup, html, type PropertyValues, unsafeCSS } from 'lit';
|
|
1
2
|
import {
|
|
2
|
-
cloneWithNewIds,
|
|
3
|
-
containerCells,
|
|
4
|
-
containerColumns,
|
|
5
|
-
containerDepth,
|
|
6
|
-
containerGrow,
|
|
7
|
-
containerHeight,
|
|
8
|
-
containerWidths,
|
|
9
|
-
extractSection,
|
|
10
|
-
findSection,
|
|
11
|
-
insertIntoCell,
|
|
12
|
-
normaliseCells,
|
|
13
|
-
normaliseGrowth,
|
|
14
|
-
normaliseSections,
|
|
15
|
-
patchSection,
|
|
16
|
-
recolumnCells,
|
|
17
|
-
trimTrailingEmptyCells,
|
|
18
|
-
} from './page-tree';
|
|
19
|
-
import { type CSSResultGroup, html, type PropertyValues, type TemplateResult, unsafeCSS } from 'lit';
|
|
20
|
-
import {
|
|
21
|
-
DEFAULT_WIDTHS,
|
|
22
|
-
defaultLayout,
|
|
23
3
|
emptyPageState,
|
|
24
4
|
generateSectionId,
|
|
25
|
-
isContainer,
|
|
26
|
-
MAX_COLUMNS,
|
|
27
|
-
MAX_CONTAINER_LEVELS,
|
|
28
|
-
MAX_WIDTH,
|
|
29
5
|
PAGE_SECTION_MIME,
|
|
30
6
|
PAGE_TYPE_MIME,
|
|
31
7
|
type PageSection,
|
|
32
8
|
type PageSectionType,
|
|
33
9
|
type PageState,
|
|
10
|
+
sectionChildren,
|
|
34
11
|
sectionSummary,
|
|
35
12
|
} from './page.types';
|
|
36
13
|
import { FormControlController, validValidityState } from '../../internal/form';
|
|
@@ -45,11 +22,14 @@ import ZnIcon from '../icon';
|
|
|
45
22
|
import ZnInput from '../input';
|
|
46
23
|
import ZnPagePaletteItem from './modules/page-palette-item';
|
|
47
24
|
import ZnPageSectionCard from './modules/page-section-card';
|
|
48
|
-
import ZnToggle from '../toggle';
|
|
49
25
|
|
|
50
26
|
import styles from './page-builder.scss';
|
|
51
27
|
|
|
52
28
|
const HISTORY_LIMIT = 50;
|
|
29
|
+
/** Sections beyond this are dropped (with a warning) when external state is applied. */
|
|
30
|
+
const MAX_SECTIONS = 500;
|
|
31
|
+
/** Per-container children beyond this are dropped when external state is applied. */
|
|
32
|
+
const MAX_CHILDREN = 24;
|
|
53
33
|
/** Builder width below which the palette auto-collapses — keep in sync with the @container query in page-builder.scss. */
|
|
54
34
|
const NARROW_WIDTH = 768;
|
|
55
35
|
|
|
@@ -68,14 +48,6 @@ function timeAgo(ms: number): string {
|
|
|
68
48
|
return `${Math.floor(h / 24)}d ago`;
|
|
69
49
|
}
|
|
70
50
|
|
|
71
|
-
/** Where a section lives inside a container cell. */
|
|
72
|
-
interface CellOwner {
|
|
73
|
-
containerId: string;
|
|
74
|
-
cellIndex: number;
|
|
75
|
-
index: number;
|
|
76
|
-
columns: number;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
51
|
/**
|
|
80
52
|
* @summary A config-driven page composer: a palette of predefined section types, a linear
|
|
81
53
|
* canvas of section cards, and an inspector for editing each section's content.
|
|
@@ -93,14 +65,10 @@ interface CellOwner {
|
|
|
93
65
|
* @event zn-page-selection-change - Emitted when the selected section changes. `event.detail.sectionId`.
|
|
94
66
|
*
|
|
95
67
|
* @slot config - `<template type="…">` declarations; never displayed. Each template's attributes
|
|
96
|
-
* (type, label, icon, icon-library, color, category, description,
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* a growable instance. `accepts` is a comma-separated list of the type keys its cells allow —
|
|
101
|
-
* omitted on a `container` type, any type is allowed subject to the nesting cap. `slots` is
|
|
102
|
-
* @deprecated: it declares a fixed-slot container of `DEFAULT_WIDTHS` columns pinned to that many
|
|
103
|
-
* cells, and keeps the old any-non-container-type rule when `accepts` is omitted.
|
|
68
|
+
* (type, label, icon, icon-library, color, category, description, slots, accepts) declare a
|
|
69
|
+
* palette entry and its content declares the inspector form for that type. `slots` makes the
|
|
70
|
+
* section a container with that many child slots; `accepts` is a comma-separated list of the
|
|
71
|
+
* type keys its slots allow.
|
|
104
72
|
* @slot header-left - Actions shown on the left of the header bar.
|
|
105
73
|
* @slot header-right - Actions shown on the right of the header bar.
|
|
106
74
|
*
|
|
@@ -121,7 +89,6 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
121
89
|
'zn-input': ZnInput,
|
|
122
90
|
'zn-page-palette-item': ZnPagePaletteItem,
|
|
123
91
|
'zn-page-section-card': ZnPageSectionCard,
|
|
124
|
-
'zn-toggle': ZnToggle,
|
|
125
92
|
};
|
|
126
93
|
|
|
127
94
|
private readonly formControlController = new FormControlController(this);
|
|
@@ -180,10 +147,10 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
180
147
|
/** Index of the drop zone whose "+" type picker is open, if any. */
|
|
181
148
|
@state() private _pickerIndex: number | null = null;
|
|
182
149
|
@state() private _dragOverIndex: number | null = null;
|
|
183
|
-
/** The
|
|
184
|
-
@state() private
|
|
185
|
-
/** The
|
|
186
|
-
@state() private
|
|
150
|
+
/** The container slot a drag is currently over, if any. */
|
|
151
|
+
@state() private _slotDragOver: { containerId: string; index: number } | null = null;
|
|
152
|
+
/** The container slot whose "+" type picker is open, if any. */
|
|
153
|
+
@state() private _slotPicker: { containerId: string; index: number } | null = null;
|
|
187
154
|
/** The stamped config form for the selected section; rebuilt on selection change. */
|
|
188
155
|
@state() private _form: HTMLDivElement | null = null;
|
|
189
156
|
|
|
@@ -441,18 +408,6 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
441
408
|
const type = el.getAttribute('type');
|
|
442
409
|
if (!type) return null;
|
|
443
410
|
const slots = parseInt(el.getAttribute('slots') ?? '', 10);
|
|
444
|
-
// `container` is the flag; `columns`/`widths`/`grow` only apply when it's present, so a
|
|
445
|
-
// legacy `slots`-only declaration is untouched by the new fields.
|
|
446
|
-
const container = el.hasAttribute('container');
|
|
447
|
-
let defaultWidths: number[] | undefined;
|
|
448
|
-
if (container) {
|
|
449
|
-
const widths = (el.getAttribute('widths') ?? '')
|
|
450
|
-
.trim().split(/[\s,]+/).filter(Boolean).map(Number).filter(n => !Number.isNaN(n));
|
|
451
|
-
const columns = parseInt(el.getAttribute('columns') ?? '', 10);
|
|
452
|
-
defaultWidths = widths.length
|
|
453
|
-
? widths
|
|
454
|
-
: columns > 0 ? Array.from({ length: columns }, () => 1) : [...DEFAULT_WIDTHS];
|
|
455
|
-
}
|
|
456
411
|
return {
|
|
457
412
|
type,
|
|
458
413
|
label: el.getAttribute('label') ?? type,
|
|
@@ -464,9 +419,6 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
464
419
|
configTemplate: el,
|
|
465
420
|
slots: slots > 0 ? slots : undefined,
|
|
466
421
|
accepts: el.getAttribute('accepts')?.split(',').map(s => s.trim()).filter(Boolean),
|
|
467
|
-
container: container ? true : undefined,
|
|
468
|
-
defaultWidths,
|
|
469
|
-
defaultGrow: container ? el.hasAttribute('grow') : undefined,
|
|
470
422
|
};
|
|
471
423
|
}
|
|
472
424
|
|
|
@@ -484,15 +436,41 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
484
436
|
|
|
485
437
|
/** Normalises and installs an externally provided state; resets selection. */
|
|
486
438
|
private _applyExternalState(next: PageState) {
|
|
487
|
-
const
|
|
488
|
-
|
|
439
|
+
const seen = new Set<string>();
|
|
440
|
+
let clippedChildren = false;
|
|
441
|
+
// Nesting is one level deep by design — grandchildren in malformed input are
|
|
442
|
+
// dropped, which also bounds the recursion. Duplicate ids get regenerated so
|
|
443
|
+
// selection/patching can never target two sections at once, and children are
|
|
444
|
+
// capped (slots are single-digit by design; MAX_SECTIONS alone would still
|
|
445
|
+
// admit one section with a huge children array).
|
|
446
|
+
const normalise = (s: PageSection, depth: number): PageSection => {
|
|
447
|
+
const id = !s.id || seen.has(s.id) ? generateSectionId() : s.id;
|
|
448
|
+
seen.add(id);
|
|
449
|
+
if (depth === 0 && (s.children?.length ?? 0) > MAX_CHILDREN) clippedChildren = true;
|
|
450
|
+
return {
|
|
451
|
+
id,
|
|
452
|
+
type: s.type,
|
|
453
|
+
label: s.label,
|
|
454
|
+
data: structuredClone(s.data ?? {}),
|
|
455
|
+
...(s.children && depth === 0
|
|
456
|
+
? { children: s.children.slice(0, MAX_CHILDREN).map(c => (c && typeof c.type === 'string' ? normalise(c, depth + 1) : null)) }
|
|
457
|
+
: {}),
|
|
458
|
+
};
|
|
459
|
+
};
|
|
460
|
+
const incoming = (next?.sections ?? []).filter(s => s && typeof s.type === 'string');
|
|
461
|
+
if (incoming.length > MAX_SECTIONS) {
|
|
462
|
+
console.warn(`<zn-page-builder> config has ${incoming.length} sections; keeping the first ${MAX_SECTIONS}`);
|
|
463
|
+
}
|
|
464
|
+
const sections = incoming.slice(0, MAX_SECTIONS).map(s => normalise(s, 0));
|
|
465
|
+
if (clippedChildren) {
|
|
466
|
+
console.warn(`<zn-page-builder> some sections had more than ${MAX_CHILDREN} children; extras were dropped`);
|
|
467
|
+
}
|
|
489
468
|
this._history = [];
|
|
490
469
|
this._redoStack = [];
|
|
491
470
|
this._state = { sections: this._requireFirst(sections) };
|
|
492
471
|
this.defaultValue = JSON.stringify(this._state);
|
|
493
472
|
this._selectedId = null;
|
|
494
473
|
this._pickerIndex = null;
|
|
495
|
-
this._cellPicker = null;
|
|
496
474
|
this._offerRestoreIfNewer();
|
|
497
475
|
}
|
|
498
476
|
|
|
@@ -525,33 +503,69 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
525
503
|
private _requireFirst(sections: PageSection[]): PageSection[] {
|
|
526
504
|
if (!this.requiredFirst || sections[0]?.type === this.requiredFirst) return sections;
|
|
527
505
|
const at = sections.findIndex(s => s.type === this.requiredFirst);
|
|
528
|
-
if (at === -1) return [
|
|
506
|
+
if (at === -1) return [{ id: generateSectionId(), type: this.requiredFirst, data: {} }, ...sections];
|
|
529
507
|
const hoisted = [...sections];
|
|
530
508
|
hoisted.unshift(...hoisted.splice(at, 1));
|
|
531
509
|
return hoisted;
|
|
532
510
|
}
|
|
533
511
|
|
|
534
|
-
/**
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
512
|
+
/** Finds a section by id, searching top-level sections and slotted children. */
|
|
513
|
+
private _findSection(id: string | null): PageSection | undefined {
|
|
514
|
+
if (!id) return undefined;
|
|
515
|
+
for (const s of this._state.sections) {
|
|
516
|
+
if (s.id === id) return s;
|
|
517
|
+
const child = s.children?.find(c => c?.id === id);
|
|
518
|
+
if (child) return child;
|
|
519
|
+
}
|
|
520
|
+
return undefined;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** New sections array with the section patched wherever it lives (top level or slot). */
|
|
524
|
+
private _patchSection(id: string, patch: (s: PageSection) => PageSection): PageSection[] {
|
|
525
|
+
return this._state.sections.map(s => {
|
|
526
|
+
if (s.id === id) return patch(s);
|
|
527
|
+
if (s.children?.some(c => c?.id === id)) {
|
|
528
|
+
return { ...s, children: s.children.map(c => (c?.id === id ? patch(c) : c)) };
|
|
529
|
+
}
|
|
530
|
+
return s;
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** Detaches a section wherever it lives: removed from the top level, or its slot nulled. */
|
|
535
|
+
private _extract(id: string): [PageSection | undefined, PageSection[]] {
|
|
536
|
+
let removed: PageSection | undefined;
|
|
537
|
+
const sections: PageSection[] = [];
|
|
538
|
+
for (const s of this._state.sections) {
|
|
539
|
+
if (s.id === id) {
|
|
540
|
+
removed = s;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const slot = s.children?.findIndex(c => c?.id === id) ?? -1;
|
|
544
|
+
if (slot !== -1) {
|
|
545
|
+
removed = s.children![slot] ?? undefined;
|
|
546
|
+
sections.push({ ...s, children: s.children!.map((c, i) => (i === slot ? null : c)) });
|
|
547
|
+
} else {
|
|
548
|
+
sections.push(s);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return [removed, sections];
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Installs a new state from a user edit and notifies listeners. */
|
|
541
555
|
private _commit(next: PageState) {
|
|
542
|
-
this._state =
|
|
556
|
+
this._state = next;
|
|
543
557
|
this.emit('zn-page-change', { detail: { state: this.state } });
|
|
544
558
|
}
|
|
545
559
|
|
|
546
560
|
private _selectedSection(): PageSection | undefined {
|
|
547
|
-
return
|
|
561
|
+
return this._findSection(this._selectedId);
|
|
548
562
|
}
|
|
549
563
|
|
|
550
564
|
private _select(id: string | null) {
|
|
551
565
|
if (this._selectedId === id) return;
|
|
552
566
|
this._selectedId = id;
|
|
553
567
|
this._pickerIndex = null;
|
|
554
|
-
this.
|
|
568
|
+
this._slotPicker = null;
|
|
555
569
|
this.emit('zn-page-selection-change', { detail: { sectionId: id } });
|
|
556
570
|
}
|
|
557
571
|
|
|
@@ -583,22 +597,11 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
583
597
|
|
|
584
598
|
// --- Section mutations ------------------------------------------------------
|
|
585
599
|
|
|
586
|
-
/** A fresh section of a registered type, seeded with layout/cells when it's a container. */
|
|
587
|
-
private _newSection(type: string): PageSection {
|
|
588
|
-
const sectionType = this.registry.get(type);
|
|
589
|
-
const section: PageSection = { id: generateSectionId(), type, data: {} };
|
|
590
|
-
if (isContainer(sectionType)) {
|
|
591
|
-
section.layout = defaultLayout(sectionType);
|
|
592
|
-
section.cells = containerCells(section, sectionType);
|
|
593
|
-
}
|
|
594
|
-
return section;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
600
|
/** Adds a section of a registered type at `index` (default: end). Returns null for unknown types. */
|
|
598
601
|
addSection(type: string, index?: number): PageSection | null {
|
|
599
602
|
if (!this.registry.has(type)) return null;
|
|
600
603
|
this._pushHistory();
|
|
601
|
-
const section =
|
|
604
|
+
const section: PageSection = { id: generateSectionId(), type, data: {} };
|
|
602
605
|
const sections = [...this._state.sections];
|
|
603
606
|
sections.splice(Math.max(index ?? sections.length, this._firstFreeIndex), 0, section);
|
|
604
607
|
this._commit({ sections });
|
|
@@ -606,15 +609,33 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
606
609
|
return section;
|
|
607
610
|
}
|
|
608
611
|
|
|
612
|
+
/** Adds a new section of a registered type into a container's slot. Returns null if not allowed. */
|
|
613
|
+
addSectionToSlot(type: string, containerId: string, slotIndex: number): PageSection | null {
|
|
614
|
+
const sectionType = this.registry.get(type);
|
|
615
|
+
const container = this._findSection(containerId);
|
|
616
|
+
const containerType = container ? this.registry.get(container.type) : undefined;
|
|
617
|
+
if (!sectionType || sectionType.slots || !container || !containerType?.slots) return null;
|
|
618
|
+
if (slotIndex < 0 || slotIndex >= containerType.slots) return null;
|
|
619
|
+
if (containerType.accepts && !containerType.accepts.includes(type)) return null;
|
|
620
|
+
const children = sectionChildren(container, containerType);
|
|
621
|
+
if (children[slotIndex]) return null;
|
|
622
|
+
const section: PageSection = { id: generateSectionId(), type, data: {} };
|
|
623
|
+
children[slotIndex] = section;
|
|
624
|
+
this._pushHistory();
|
|
625
|
+
this._commit({ sections: this._patchSection(containerId, s => ({ ...s, children })) });
|
|
626
|
+
this._select(section.id);
|
|
627
|
+
return section;
|
|
628
|
+
}
|
|
629
|
+
|
|
609
630
|
private _removeSection(id: string) {
|
|
610
631
|
if (this._isPinned(id)) return;
|
|
611
|
-
const [removed, sections] =
|
|
632
|
+
const [removed, sections] = this._extract(id);
|
|
612
633
|
if (!removed) return;
|
|
613
634
|
this._pushHistory();
|
|
614
|
-
// Clear selection for the removed section AND anything inside it
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
635
|
+
// Clear selection for the removed section AND anything inside it.
|
|
636
|
+
if (this._selectedId === id || removed.children?.some(c => c?.id === this._selectedId)) {
|
|
637
|
+
this._select(null);
|
|
638
|
+
}
|
|
618
639
|
this._commit({ sections });
|
|
619
640
|
}
|
|
620
641
|
|
|
@@ -622,51 +643,35 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
622
643
|
const index = this._state.sections.findIndex(s => s.id === id);
|
|
623
644
|
if (index !== -1) {
|
|
624
645
|
this._pushHistory();
|
|
625
|
-
const copy =
|
|
646
|
+
const copy = structuredClone(this._state.sections[index]);
|
|
647
|
+
copy.id = generateSectionId();
|
|
648
|
+
copy.children = copy.children?.map(c => (c ? { ...c, id: generateSectionId() } : null));
|
|
626
649
|
const sections = [...this._state.sections];
|
|
627
650
|
sections.splice(index + 1, 0, copy);
|
|
628
651
|
this._commit({ sections });
|
|
629
652
|
this._select(copy.id);
|
|
630
653
|
return;
|
|
631
654
|
}
|
|
632
|
-
// A
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
)
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
const cells = section.cells ?? [];
|
|
650
|
-
for (let cellIndex = 0; cellIndex < cells.length; cellIndex++) {
|
|
651
|
-
const index = cells[cellIndex].findIndex(child => child.id === id);
|
|
652
|
-
if (index !== -1) {
|
|
653
|
-
return {
|
|
654
|
-
containerId: section.id,
|
|
655
|
-
cellIndex,
|
|
656
|
-
index,
|
|
657
|
-
columns: containerColumns(section, this.registry.get(section.type)),
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
|
-
const deeper = search(cells[cellIndex]);
|
|
661
|
-
if (deeper) return deeper;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
return undefined;
|
|
665
|
-
};
|
|
666
|
-
return search(this._state.sections);
|
|
655
|
+
// A slotted child duplicates into its container's next empty slot, if any.
|
|
656
|
+
for (const s of this._state.sections) {
|
|
657
|
+
const slot = s.children?.findIndex(c => c?.id === id) ?? -1;
|
|
658
|
+
if (slot === -1) continue;
|
|
659
|
+
const type = this.registry.get(s.type);
|
|
660
|
+
if (!type?.slots) return;
|
|
661
|
+
const children = sectionChildren(s, type);
|
|
662
|
+
const empty = children.findIndex(c => !c);
|
|
663
|
+
if (empty === -1) return;
|
|
664
|
+
const copy = structuredClone(children[slot]!);
|
|
665
|
+
copy.id = generateSectionId();
|
|
666
|
+
children[empty] = copy;
|
|
667
|
+
this._pushHistory();
|
|
668
|
+
this._commit({ sections: this._patchSection(s.id, x => ({ ...x, children })) });
|
|
669
|
+
this._select(copy.id);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
667
672
|
}
|
|
668
673
|
|
|
669
|
-
/** Moves a section (top-level or
|
|
674
|
+
/** Moves a section (top-level or slotted) to a top-level position. */
|
|
670
675
|
private _moveSection(id: string, index: number) {
|
|
671
676
|
if (this._isPinned(id)) return;
|
|
672
677
|
index = Math.max(index, this._firstFreeIndex);
|
|
@@ -681,22 +686,60 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
681
686
|
this._commit({ sections });
|
|
682
687
|
return;
|
|
683
688
|
}
|
|
684
|
-
const [moved, sections] =
|
|
689
|
+
const [moved, sections] = this._extract(id);
|
|
685
690
|
if (!moved) return;
|
|
686
691
|
this._pushHistory();
|
|
687
692
|
sections.splice(index, 0, moved);
|
|
688
693
|
this._commit({ sections });
|
|
689
694
|
}
|
|
690
695
|
|
|
691
|
-
|
|
696
|
+
/**
|
|
697
|
+
* Moves a section into a container's slot. Dropping onto an occupied slot swaps
|
|
698
|
+
* the two children (slot-to-slot reordering); top-level sections and containers
|
|
699
|
+
* only enter empty slots / never enter slots respectively.
|
|
700
|
+
*/
|
|
701
|
+
private _moveToSlot(id: string, containerId: string, slotIndex: number) {
|
|
702
|
+
const moved = this._findSection(id);
|
|
703
|
+
const container = this._findSection(containerId);
|
|
704
|
+
const containerType = container ? this.registry.get(container.type) : undefined;
|
|
705
|
+
if (!moved || !container || !containerType?.slots || id === containerId) return;
|
|
706
|
+
if (this._isPinned(id)) return; // the pinned section stays at the top of the page
|
|
707
|
+
if (this.registry.get(moved.type)?.slots) return; // no containers inside slots
|
|
708
|
+
if (containerType.accepts && !containerType.accepts.includes(moved.type)) return;
|
|
709
|
+
if (slotIndex < 0 || slotIndex >= containerType.slots) return;
|
|
710
|
+
|
|
711
|
+
const target = container.children?.[slotIndex] ?? null;
|
|
712
|
+
if (target?.id === id) return;
|
|
713
|
+
const fromTop = this._state.sections.some(s => s.id === id);
|
|
714
|
+
if (target && fromTop) return; // top-level sections only drop on empty slots
|
|
715
|
+
|
|
716
|
+
this._pushHistory();
|
|
717
|
+
const sections = structuredClone(this._state.sections);
|
|
718
|
+
const containerRef = sections.find(s => s.id === containerId)!;
|
|
719
|
+
containerRef.children = Array.from({ length: containerType.slots }, (_, i) => containerRef.children?.[i] ?? null);
|
|
720
|
+
const movedCopy = structuredClone(moved);
|
|
721
|
+
|
|
722
|
+
if (fromTop) {
|
|
723
|
+
sections.splice(sections.findIndex(s => s.id === id), 1);
|
|
724
|
+
} else {
|
|
725
|
+
// Swap: the occupant (or null) takes the source slot.
|
|
726
|
+
for (const s of sections) {
|
|
727
|
+
const slot = s.children?.findIndex(c => c?.id === id) ?? -1;
|
|
728
|
+
if (slot !== -1) {
|
|
729
|
+
s.children![slot] = target ? structuredClone(target) : null;
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
containerRef.children[slotIndex] = movedCopy;
|
|
735
|
+
this._commit({ sections });
|
|
736
|
+
}
|
|
692
737
|
|
|
693
|
-
|
|
694
|
-
private _draggingId: string | null = null;
|
|
738
|
+
// --- Canvas drag & drop -----------------------------------------------------
|
|
695
739
|
|
|
696
740
|
private _onCardDragStart(e: DragEvent, id: string) {
|
|
697
741
|
if (!e.dataTransfer) return;
|
|
698
742
|
e.stopPropagation(); // a child card's drag must not also start its container's
|
|
699
|
-
this._draggingId = id;
|
|
700
743
|
e.dataTransfer.setData(PAGE_SECTION_MIME, id);
|
|
701
744
|
e.dataTransfer.effectAllowed = 'move';
|
|
702
745
|
}
|
|
@@ -707,6 +750,24 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
707
750
|
return types.includes(PAGE_TYPE_MIME) || types.includes(PAGE_SECTION_MIME);
|
|
708
751
|
}
|
|
709
752
|
|
|
753
|
+
private _onSlotDragOver(e: DragEvent, containerId: string, index: number) {
|
|
754
|
+
if (!this._isPageDrag(e)) return;
|
|
755
|
+
e.preventDefault();
|
|
756
|
+
e.stopPropagation();
|
|
757
|
+
this._dragOverIndex = null;
|
|
758
|
+
this._slotDragOver = { containerId, index };
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
private _onSlotDrop(e: DragEvent, containerId: string, index: number) {
|
|
762
|
+
e.preventDefault();
|
|
763
|
+
e.stopPropagation();
|
|
764
|
+
this._slotDragOver = null;
|
|
765
|
+
const typeKey = e.dataTransfer?.getData(PAGE_TYPE_MIME);
|
|
766
|
+
const sectionId = e.dataTransfer?.getData(PAGE_SECTION_MIME);
|
|
767
|
+
if (typeKey) this.addSectionToSlot(typeKey, containerId, index);
|
|
768
|
+
else if (sectionId) this._moveToSlot(sectionId, containerId, index);
|
|
769
|
+
}
|
|
770
|
+
|
|
710
771
|
private _onZoneDragOver(e: DragEvent, index: number) {
|
|
711
772
|
if (!this._isPageDrag(e)) return;
|
|
712
773
|
e.preventDefault();
|
|
@@ -736,124 +797,6 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
736
797
|
this._onZoneDrop(e, this._state.sections.length);
|
|
737
798
|
};
|
|
738
799
|
|
|
739
|
-
// --- Container cell drag & drop ----------------------------------------------
|
|
740
|
-
|
|
741
|
-
/**
|
|
742
|
-
* Whether `typeKey`'s registered type is allowed into this container by its
|
|
743
|
-
* `accepts`/`slots=` rule — the nesting depth cap is a separate concern,
|
|
744
|
-
* checked by each caller against its own notion of how tall the dropped
|
|
745
|
-
* subtree is.
|
|
746
|
-
*/
|
|
747
|
-
private _acceptsType(container: PageSection, typeKey: string): boolean {
|
|
748
|
-
const containerType = this.registry.get(container.type);
|
|
749
|
-
const dragged = this.registry.get(typeKey);
|
|
750
|
-
if (!dragged || !isContainer(containerType)) return false;
|
|
751
|
-
if (containerType!.accepts) return containerType!.accepts.includes(typeKey);
|
|
752
|
-
// The deprecated `slots=` alias keeps its stricter non-containers-only rule.
|
|
753
|
-
if (!containerType!.container) return !isContainer(dragged);
|
|
754
|
-
return true;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
/**
|
|
758
|
-
* Whether `typeKey` may be dropped into this container's cells from the
|
|
759
|
-
* palette. A newly created section always has height 1, so the depth cap
|
|
760
|
-
* only needs the target's own depth.
|
|
761
|
-
*/
|
|
762
|
-
private _acceptsInCell(container: PageSection, typeKey: string): boolean {
|
|
763
|
-
const dragged = this.registry.get(typeKey);
|
|
764
|
-
// The nesting depth cap is enforced independently of `accepts` — an explicit
|
|
765
|
-
// allow-list must not be able to punch through MAX_CONTAINER_LEVELS.
|
|
766
|
-
if (dragged && isContainer(dragged) && containerDepth(this._state.sections, container.id) >= MAX_CONTAINER_LEVELS) {
|
|
767
|
-
return false;
|
|
768
|
-
}
|
|
769
|
-
return this._acceptsType(container, typeKey);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
/**
|
|
773
|
-
* Whether a dragged, already-placed section may land in this container's
|
|
774
|
-
* cells. Unlike a palette drop, the moved subtree can already be several
|
|
775
|
-
* containers tall (it may itself hold a nested container), so the cap has to
|
|
776
|
-
* account for that height, not just the target's depth: dropping `moved` at
|
|
777
|
-
* a new depth of `containerDepth(container) + 1` puts its own deepest
|
|
778
|
-
* descendant at `containerDepth(container) + containerHeight(moved)`.
|
|
779
|
-
*/
|
|
780
|
-
private _canMoveIntoCell(container: PageSection, sectionId: string): boolean {
|
|
781
|
-
if (this._isPinned(sectionId)) return false;
|
|
782
|
-
if (sectionId === container.id) return false;
|
|
783
|
-
const moved = findSection(this._state.sections, sectionId);
|
|
784
|
-
if (!moved) return false;
|
|
785
|
-
// Refuse dropping a container into its own subtree.
|
|
786
|
-
if (findSection([moved], container.id)) return false;
|
|
787
|
-
if (containerDepth(this._state.sections, container.id) + containerHeight(moved) > MAX_CONTAINER_LEVELS) {
|
|
788
|
-
return false;
|
|
789
|
-
}
|
|
790
|
-
return this._acceptsType(container, moved.type);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
private _onCellDragOver(e: DragEvent, containerId: string, cellIndex: number, insertIndex: number) {
|
|
794
|
-
if (!this._isPageDrag(e)) return;
|
|
795
|
-
const container = findSection(this._state.sections, containerId);
|
|
796
|
-
if (!container) return;
|
|
797
|
-
// Claim the event before deciding accept/refuse — a nested cell is the deepest
|
|
798
|
-
// target and must not let a refusal bubble up into an ancestor cell that accepts.
|
|
799
|
-
e.stopPropagation();
|
|
800
|
-
const types = e.dataTransfer ? Array.from(e.dataTransfer.types) : [];
|
|
801
|
-
// A section drag is validated here from its own tracked id, since dataTransfer
|
|
802
|
-
// values are unreadable during a real dragover (protected mode).
|
|
803
|
-
if (types.includes(PAGE_SECTION_MIME) && this._draggingId && !this._canMoveIntoCell(container, this._draggingId)) {
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
|
-
// A type-key drag's payload is unreadable during a real dragover too, so this
|
|
807
|
-
// only enforces `accepts`/depth when the payload happens to be readable; the
|
|
808
|
-
// definitive check is on drop.
|
|
809
|
-
const typeKey = types.includes(PAGE_TYPE_MIME) ? e.dataTransfer?.getData(PAGE_TYPE_MIME) : '';
|
|
810
|
-
if (typeKey && !this._acceptsInCell(container, typeKey)) return;
|
|
811
|
-
e.preventDefault();
|
|
812
|
-
this._dragOverIndex = null;
|
|
813
|
-
this._cellDragOver = { containerId, cellIndex, insertIndex };
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
private _onCellDrop(e: DragEvent, containerId: string, cellIndex: number, insertIndex: number) {
|
|
817
|
-
e.preventDefault();
|
|
818
|
-
e.stopPropagation();
|
|
819
|
-
this._cellDragOver = null;
|
|
820
|
-
const typeKey = e.dataTransfer?.getData(PAGE_TYPE_MIME);
|
|
821
|
-
const sectionId = e.dataTransfer?.getData(PAGE_SECTION_MIME);
|
|
822
|
-
if (typeKey) this.addSectionToCell(typeKey, containerId, cellIndex, insertIndex);
|
|
823
|
-
else if (sectionId) this._moveToCell(sectionId, containerId, cellIndex, insertIndex);
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
/** Adds a new section of a registered type into a container cell. */
|
|
827
|
-
addSectionToCell(type: string, containerId: string, cellIndex: number, insertIndex = 0): PageSection | null {
|
|
828
|
-
const container = findSection(this._state.sections, containerId);
|
|
829
|
-
if (!container || !this._acceptsInCell(container, type)) return null;
|
|
830
|
-
const section = this._newSection(type);
|
|
831
|
-
this._pushHistory();
|
|
832
|
-
this._commit({
|
|
833
|
-
sections: insertIntoCell(
|
|
834
|
-
this._state.sections, containerId, cellIndex, insertIndex, section,
|
|
835
|
-
containerColumns(container, this.registry.get(container.type))
|
|
836
|
-
),
|
|
837
|
-
});
|
|
838
|
-
this._select(section.id);
|
|
839
|
-
return section;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
private _moveToCell(id: string, containerId: string, cellIndex: number, insertIndex: number) {
|
|
843
|
-
const container = findSection(this._state.sections, containerId);
|
|
844
|
-
if (!container || !this._canMoveIntoCell(container, id)) return;
|
|
845
|
-
const columns = containerColumns(container, this.registry.get(container.type));
|
|
846
|
-
const [moved, without] = extractSection(this._state.sections, id);
|
|
847
|
-
if (!moved) return;
|
|
848
|
-
this._pushHistory();
|
|
849
|
-
this._commit({ sections: insertIntoCell(without, containerId, cellIndex, insertIndex, moved, columns) });
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
/** Types offerable in a container's cells. */
|
|
853
|
-
private _cellTypes(container: PageSection): PageSectionType[] {
|
|
854
|
-
return this.registry.all().filter(t => this._acceptsInCell(container, t.type));
|
|
855
|
-
}
|
|
856
|
-
|
|
857
800
|
private _onCardKeydown(e: KeyboardEvent, id: string) {
|
|
858
801
|
if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
859
802
|
e.preventDefault();
|
|
@@ -1021,92 +964,59 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
1021
964
|
}
|
|
1022
965
|
|
|
1023
966
|
private _renderCard(section: PageSection, index: number) {
|
|
1024
|
-
|
|
967
|
+
const type = this.registry.get(section.type);
|
|
968
|
+
const card = this._renderSectionCard(section, {
|
|
1025
969
|
over: e => this._onZoneDragOver(e, index + 1),
|
|
1026
970
|
drop: e => this._onZoneDrop(e, index + 1),
|
|
1027
971
|
});
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
/**
|
|
1031
|
-
* A card, or — for a container — its card plus its own cell grid. Shared by the
|
|
1032
|
-
* top-level section list and container-cell stacks, so nesting renders at every level.
|
|
1033
|
-
*/
|
|
1034
|
-
private _renderNode(
|
|
1035
|
-
section: PageSection,
|
|
1036
|
-
drop: { over: (e: DragEvent) => void; drop: (e: DragEvent) => void },
|
|
1037
|
-
extraClass = ''
|
|
1038
|
-
): TemplateResult {
|
|
1039
|
-
const type = this.registry.get(section.type);
|
|
1040
|
-
const card = this._renderSectionCard(section, drop, extraClass);
|
|
1041
|
-
if (!isContainer(type)) return card;
|
|
972
|
+
if (!type?.slots) return card;
|
|
1042
973
|
return html`
|
|
1043
974
|
<div class="container">
|
|
1044
975
|
${card}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
/** A container's cell grid: one track per width, one stack per cell. */
|
|
1050
|
-
private _renderCells(container: PageSection, type: PageSectionType | undefined): TemplateResult {
|
|
1051
|
-
const widths = containerWidths(container, type);
|
|
1052
|
-
const cells = containerCells(container, type);
|
|
1053
|
-
const grow = containerGrow(container, type);
|
|
1054
|
-
// A growable container never persists a trailing empty row, so the extra
|
|
1055
|
-
// row of drop targets is added here at render time.
|
|
1056
|
-
const rendered = grow ? [...cells, ...Array.from({ length: widths.length }, () => [] as PageSection[])] : cells;
|
|
1057
|
-
return html`
|
|
1058
|
-
<div class="cells" style="grid-template-columns:${widths.map(w => `${w}fr`).join(' ')}">
|
|
1059
|
-
${rendered.map((stack, cellIndex) => this._renderCell(container, stack, cellIndex))}
|
|
976
|
+
<div class="slots">
|
|
977
|
+
${sectionChildren(section, type).map((child, i) => this._renderSlot(section, child, i))}
|
|
978
|
+
</div>
|
|
1060
979
|
</div>`;
|
|
1061
980
|
}
|
|
1062
981
|
|
|
1063
|
-
private
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
982
|
+
private _renderSlot(container: PageSection, child: PageSection | null, index: number) {
|
|
983
|
+
if (child) {
|
|
984
|
+
return this._renderSectionCard(child, {
|
|
985
|
+
over: e => this._onSlotDragOver(e, container.id, index),
|
|
986
|
+
drop: e => this._onSlotDrop(e, container.id, index),
|
|
987
|
+
}, 'slot__card');
|
|
988
|
+
}
|
|
989
|
+
const active = this._slotDragOver?.containerId === container.id && this._slotDragOver.index === index;
|
|
990
|
+
const pickerOpen = this._slotPicker?.containerId === container.id && this._slotPicker.index === index;
|
|
1068
991
|
return html`
|
|
1069
992
|
<div
|
|
1070
|
-
class="
|
|
1071
|
-
@dragover="${(e: DragEvent) => this.
|
|
993
|
+
class="slot slot--empty ${active ? 'slot--active' : ''}"
|
|
994
|
+
@dragover="${(e: DragEvent) => this._onSlotDragOver(e, container.id, index)}"
|
|
1072
995
|
@dragleave="${() => {
|
|
1073
|
-
if (this.
|
|
1074
|
-
this.
|
|
996
|
+
if (this._slotDragOver?.containerId === container.id && this._slotDragOver.index === index) {
|
|
997
|
+
this._slotDragOver = null;
|
|
1075
998
|
}
|
|
1076
999
|
}}"
|
|
1077
|
-
@drop="${(e: DragEvent) => this.
|
|
1000
|
+
@drop="${(e: DragEvent) => this._onSlotDrop(e, container.id, index)}"
|
|
1078
1001
|
@click="${(e: Event) => {
|
|
1079
1002
|
e.stopPropagation();
|
|
1080
|
-
this.
|
|
1003
|
+
this._slotPicker = pickerOpen ? null : { containerId: container.id, index };
|
|
1081
1004
|
}}">
|
|
1082
|
-
|
|
1083
|
-
${this._renderCellStrip(container, cellIndex, i)}
|
|
1084
|
-
${this._renderNode(child, {
|
|
1085
|
-
over: e => this._onCellDragOver(e, container.id, cellIndex, i),
|
|
1086
|
-
drop: e => this._onCellDrop(e, container.id, cellIndex, i),
|
|
1087
|
-
}, 'cell__card')}
|
|
1088
|
-
`)}
|
|
1089
|
-
${this._renderCellStrip(container, cellIndex, stack.length)}
|
|
1090
|
-
${stack.length === 0 ? html`<zn-icon src="add" size="16"></zn-icon>` : ''}
|
|
1005
|
+
<zn-icon src="add" size="16"></zn-icon>
|
|
1091
1006
|
${pickerOpen
|
|
1092
|
-
? this._renderTypePicker(this.
|
|
1007
|
+
? this._renderTypePicker(this._slotTypes(container), t => this.addSectionToSlot(t, container.id, index))
|
|
1093
1008
|
: ''}
|
|
1094
1009
|
</div>`;
|
|
1095
1010
|
}
|
|
1096
1011
|
|
|
1097
|
-
/**
|
|
1098
|
-
private
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
&&
|
|
1102
|
-
return html`
|
|
1103
|
-
<div
|
|
1104
|
-
class="cell__strip ${active ? 'cell__strip--active' : ''}"
|
|
1105
|
-
@dragover="${(e: DragEvent) => this._onCellDragOver(e, container.id, cellIndex, insertIndex)}"
|
|
1106
|
-
@drop="${(e: DragEvent) => this._onCellDrop(e, container.id, cellIndex, insertIndex)}"></div>`;
|
|
1012
|
+
/** Types allowed in a container's slots: non-containers, filtered by its accepts list. */
|
|
1013
|
+
private _slotTypes(container: PageSection): PageSectionType[] {
|
|
1014
|
+
const containerType = this.registry.get(container.type);
|
|
1015
|
+
return this.registry.all().filter(t =>
|
|
1016
|
+
!t.slots && (!containerType?.accepts || containerType.accepts.includes(t.type)));
|
|
1107
1017
|
}
|
|
1108
1018
|
|
|
1109
|
-
/** The one type-picker template both drop zones and
|
|
1019
|
+
/** The one type-picker template both drop zones and slot cells render. */
|
|
1110
1020
|
private _renderTypePicker(types: PageSectionType[], pick: (type: string) => void) {
|
|
1111
1021
|
return html`
|
|
1112
1022
|
<div class="picker" @click="${(e: Event) => e.stopPropagation()}">
|
|
@@ -1116,6 +1026,7 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
1116
1026
|
class="picker__item"
|
|
1117
1027
|
@click="${() => {
|
|
1118
1028
|
this._pickerIndex = null;
|
|
1029
|
+
this._slotPicker = null;
|
|
1119
1030
|
pick(type.type);
|
|
1120
1031
|
}}">
|
|
1121
1032
|
<zn-icon src="${type.icon ?? 'widgets'}" library="${ifDefined(type.iconLibrary)}" size="14"></zn-icon>
|
|
@@ -1223,121 +1134,12 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
1223
1134
|
|
|
1224
1135
|
private _updateSectionData(id: string, patch: Record<string, unknown>) {
|
|
1225
1136
|
this._pushHistory();
|
|
1226
|
-
this._commit({ sections:
|
|
1137
|
+
this._commit({ sections: this._patchSection(id, s => ({ ...s, data: { ...s.data, ...patch } })) });
|
|
1227
1138
|
}
|
|
1228
1139
|
|
|
1229
1140
|
private _renameSection(id: string, label: string) {
|
|
1230
1141
|
this._pushHistory();
|
|
1231
|
-
this._commit({ sections:
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
/** Replaces a container's layout, keeping cells consistent with it. */
|
|
1235
|
-
private _setLayout(
|
|
1236
|
-
id: string,
|
|
1237
|
-
next: (current: { widths: number[]; grow: boolean; cells: PageSection[][] }) => {
|
|
1238
|
-
widths: number[];
|
|
1239
|
-
grow: boolean;
|
|
1240
|
-
cells: PageSection[][];
|
|
1241
|
-
}
|
|
1242
|
-
) {
|
|
1243
|
-
const container = findSection(this._state.sections, id);
|
|
1244
|
-
if (!container) return;
|
|
1245
|
-
const type = this.registry.get(container.type);
|
|
1246
|
-
const current = {
|
|
1247
|
-
widths: containerWidths(container, type),
|
|
1248
|
-
grow: containerGrow(container, type),
|
|
1249
|
-
cells: containerCells(container, type),
|
|
1250
|
-
};
|
|
1251
|
-
const { widths, grow, cells } = next(current);
|
|
1252
|
-
this._pushHistory();
|
|
1253
|
-
this._commit({
|
|
1254
|
-
sections: patchSection(this._state.sections, id, section => ({
|
|
1255
|
-
...section,
|
|
1256
|
-
layout: { widths, grow },
|
|
1257
|
-
cells: normaliseCells(cells, widths.length, grow),
|
|
1258
|
-
})),
|
|
1259
|
-
});
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
private _setColumns(id: string, columns: number) {
|
|
1263
|
-
this._setLayout(id, current => {
|
|
1264
|
-
const count = Math.min(Math.max(Math.floor(columns) || 1, 1), MAX_COLUMNS);
|
|
1265
|
-
const widths = Array.from({ length: count }, (_, i) => current.widths[i] ?? 1);
|
|
1266
|
-
return { widths, grow: current.grow, cells: recolumnCells(current.cells, count) };
|
|
1267
|
-
});
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
private _setWidth(id: string, column: number, width: number) {
|
|
1271
|
-
const container = findSection(this._state.sections, id);
|
|
1272
|
-
if (!container) return;
|
|
1273
|
-
const columns = containerWidths(container, this.registry.get(container.type)).length;
|
|
1274
|
-
if (column < 0 || column >= columns) return;
|
|
1275
|
-
this._setLayout(id, current => {
|
|
1276
|
-
const widths = [...current.widths];
|
|
1277
|
-
widths[column] = Math.min(Math.max(Math.floor(width) || 1, 1), MAX_WIDTH);
|
|
1278
|
-
return { ...current, widths };
|
|
1279
|
-
});
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
private _setGrow(id: string, grow: boolean) {
|
|
1283
|
-
this._setLayout(id, current => ({ ...current, grow }));
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
/** Pads with empty rows, or trims trailing empty rows down to the last occupied one. */
|
|
1287
|
-
private _setRows(id: string, rows: number) {
|
|
1288
|
-
this._setLayout(id, current => {
|
|
1289
|
-
const columns = current.widths.length;
|
|
1290
|
-
const wanted = Math.max(Math.floor(rows) || 1, 1);
|
|
1291
|
-
const target = wanted * columns;
|
|
1292
|
-
const cells = trimTrailingEmptyCells(current.cells);
|
|
1293
|
-
while (cells.length < target) cells.push([]);
|
|
1294
|
-
return { ...current, cells };
|
|
1295
|
-
});
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
private _renderLayoutGroup(section: PageSection, type: PageSectionType | undefined) {
|
|
1299
|
-
if (!isContainer(type)) return '';
|
|
1300
|
-
const widths = containerWidths(section, type);
|
|
1301
|
-
const grow = containerGrow(section, type);
|
|
1302
|
-
const rows = containerCells(section, type).length / widths.length;
|
|
1303
|
-
return html`
|
|
1304
|
-
<div class="layout-group" @click="${(e: Event) => e.stopPropagation()}">
|
|
1305
|
-
<div class="layout-group__title">Layout</div>
|
|
1306
|
-
<zn-input
|
|
1307
|
-
class="layout-group__columns"
|
|
1308
|
-
type="number"
|
|
1309
|
-
label="Columns"
|
|
1310
|
-
min="1"
|
|
1311
|
-
max="${MAX_COLUMNS}"
|
|
1312
|
-
.value="${String(widths.length)}"
|
|
1313
|
-
@zn-change="${(e: Event) => this._setColumns(section.id, Number((e.target as ZnInput).value))}"></zn-input>
|
|
1314
|
-
<div class="layout-group__widths">
|
|
1315
|
-
<span class="layout-group__label">Widths</span>
|
|
1316
|
-
<div class="layout-group__weights">
|
|
1317
|
-
${widths.map((width, column) => html`
|
|
1318
|
-
<zn-input
|
|
1319
|
-
type="number"
|
|
1320
|
-
min="1"
|
|
1321
|
-
max="${MAX_WIDTH}"
|
|
1322
|
-
aria-label="Column ${column + 1} width"
|
|
1323
|
-
.value="${String(width)}"
|
|
1324
|
-
@zn-change="${(e: Event) => this._setWidth(section.id, column, Number((e.target as ZnInput).value))}"></zn-input>`)}
|
|
1325
|
-
</div>
|
|
1326
|
-
</div>
|
|
1327
|
-
<zn-toggle
|
|
1328
|
-
label="Keep adding rows"
|
|
1329
|
-
label-position="left"
|
|
1330
|
-
description="Offers a new row as the last one fills."
|
|
1331
|
-
?checked="${grow}"
|
|
1332
|
-
@zn-input="${(e: Event) => this._setGrow(section.id, Boolean((e.target as HTMLInputElement).checked))}"></zn-toggle>
|
|
1333
|
-
${grow ? '' : html`
|
|
1334
|
-
<zn-input
|
|
1335
|
-
type="number"
|
|
1336
|
-
label="Rows"
|
|
1337
|
-
min="1"
|
|
1338
|
-
.value="${String(rows)}"
|
|
1339
|
-
@zn-change="${(e: Event) => this._setRows(section.id, Number((e.target as ZnInput).value))}"></zn-input>`}
|
|
1340
|
-
</div>`;
|
|
1142
|
+
this._commit({ sections: this._patchSection(id, s => ({ ...s, label: label || undefined })) });
|
|
1341
1143
|
}
|
|
1342
1144
|
|
|
1343
1145
|
private _renderInspector() {
|
|
@@ -1376,7 +1178,6 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
1376
1178
|
@zn-change="${this._onInspectorInput}"
|
|
1377
1179
|
@input="${this._onInspectorInput}"
|
|
1378
1180
|
@zn-input="${this._onInspectorInput}">
|
|
1379
|
-
${this._renderLayoutGroup(section, type)}
|
|
1380
1181
|
<zn-input
|
|
1381
1182
|
class="inspector__rename"
|
|
1382
1183
|
label="Section name"
|
|
@@ -1399,8 +1200,7 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
1399
1200
|
class="builder ${this._selectedId ? 'builder--inspecting' : ''} ${this.paletteCollapsed ? 'builder--palette-collapsed' : ''} ${this.inspectorCollapsed ? 'builder--inspector-collapsed' : ''}"
|
|
1400
1201
|
@dragend="${() => {
|
|
1401
1202
|
this._dragOverIndex = null;
|
|
1402
|
-
this.
|
|
1403
|
-
this._draggingId = null;
|
|
1203
|
+
this._slotDragOver = null;
|
|
1404
1204
|
}}">
|
|
1405
1205
|
<header part="header" class="header" ?hidden="${!hasHeader}">
|
|
1406
1206
|
<div class="header__group">
|