@kubex/zinc 1.1.94 → 1.1.96
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 +653 -89
- package/dist/vscode.html-custom-data.json +73 -18
- package/dist/web-types.json +145 -35
- package/dist/zn.d.ts +204 -39
- package/dist/zn.min.css +1 -1
- package/dist/zn.min.js +374 -308
- package/docs/pages/components/flow-builder.md +17 -1
- package/docs/pages/components/page-builder.md +31 -0
- package/docs/pages/components/slash-menu.md +132 -6
- package/docs/pages/components/textarea.md +16 -0
- package/package.json +1 -1
- package/scss/_root.scss +7 -1
- package/src/components/button/button.scss +5 -2
- 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/inline-edit/inline-edit.component.ts +6 -1
- package/src/components/input/input.component.ts +12 -2
- package/src/components/page/page.scss +7 -2
- package/src/components/page-builder/modules/page-section-card/page-section-card.component.ts +16 -9
- package/src/components/page-builder/modules/page-section-card/page-section-card.scss +13 -0
- package/src/components/page-builder/modules/page-section-card/page-section-card.test.ts +9 -0
- package/src/components/page-builder/page-builder.component.ts +62 -4
- package/src/components/page-builder/page-builder.test.ts +94 -0
- package/src/components/remarkd-editor/remarkd-editor.component.ts +227 -12
- package/src/components/remarkd-editor/remarkd-editor.scss +81 -0
- package/src/components/remarkd-editor/remarkd-editor.test.ts +258 -0
- package/src/components/settings-container/settings-container.scss +2 -1
- package/src/components/slash-item/slash-item.component.ts +1 -1
- package/src/components/slash-menu/slash-menu-items.ts +48 -0
- package/src/components/slash-menu/slash-menu.component.ts +134 -27
- package/src/components/slash-menu/slash-menu.scss +90 -12
- package/src/components/slash-menu/slash-menu.test.ts +107 -0
- package/src/components/textarea/textarea.component.ts +12 -2
- package/src/components/textarea/textarea.test.ts +2 -2
- package/src/components/translations/translations.component.ts +5 -1
|
@@ -117,12 +117,15 @@ export default class ZnInlineEdit extends ZincElement implements ZincFormControl
|
|
|
117
117
|
/** The characters that open the slash menu. */
|
|
118
118
|
@property({ attribute: 'slash-trigger' }) slashTrigger = '/';
|
|
119
119
|
|
|
120
|
-
/** The
|
|
120
|
+
/** The name the slash menu's list is announced by. */
|
|
121
121
|
@property({ attribute: 'slash-heading' }) slashHeading = 'Insert';
|
|
122
122
|
|
|
123
123
|
/** Hides the insertion keys normally shown against the slash menu's items. */
|
|
124
124
|
@property({ attribute: 'slash-hide-keys', type: Boolean }) slashHideKeys = false;
|
|
125
125
|
|
|
126
|
+
/** Lists the slash menu items most recently chosen here above the rest, remembered under this key. */
|
|
127
|
+
@property({ attribute: 'slash-recent-key' }) slashRecentKey = '';
|
|
128
|
+
|
|
126
129
|
/** Resolves additional slash menu items each time the menu opens. JavaScript only. */
|
|
127
130
|
@property({ attribute: false }) slashItemsProvider?: (query: string) => SlashMenuItem[] | Promise<SlashMenuItem[]>;
|
|
128
131
|
|
|
@@ -466,6 +469,7 @@ export default class ZnInlineEdit extends ZincElement implements ZincFormControl
|
|
|
466
469
|
slash-trigger="${this.slashTrigger}"
|
|
467
470
|
slash-heading="${this.slashHeading}"
|
|
468
471
|
slash-preset="${this.slashPreset}"
|
|
472
|
+
slash-recent-key="${this.slashRecentKey}"
|
|
469
473
|
?slash-hide-keys="${this.slashHideKeys}"
|
|
470
474
|
.slashItems="${this.slashItems}"
|
|
471
475
|
.slashItemsProvider="${this.slashItemsProvider}"
|
|
@@ -489,6 +493,7 @@ export default class ZnInlineEdit extends ZincElement implements ZincFormControl
|
|
|
489
493
|
slash-trigger="${this.slashTrigger}"
|
|
490
494
|
slash-heading="${this.slashHeading}"
|
|
491
495
|
slash-preset="${this.slashPreset}"
|
|
496
|
+
slash-recent-key="${this.slashRecentKey}"
|
|
492
497
|
?slash-hide-keys="${this.slashHideKeys}"
|
|
493
498
|
.slashItems="${this.slashItems}"
|
|
494
499
|
.slashItemsProvider="${this.slashItemsProvider}"
|
|
@@ -276,12 +276,18 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
276
276
|
/** The characters that open the slash menu. */
|
|
277
277
|
@property({attribute: 'slash-trigger'}) slashTrigger = '/';
|
|
278
278
|
|
|
279
|
-
/** The
|
|
279
|
+
/** The name the slash menu's list is announced by. */
|
|
280
280
|
@property({attribute: 'slash-heading'}) slashHeading = 'Insert';
|
|
281
281
|
|
|
282
282
|
/** Hides the insertion keys normally shown against the slash menu's items. */
|
|
283
283
|
@property({attribute: 'slash-hide-keys', type: Boolean}) slashHideKeys = false;
|
|
284
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Lists the items most recently chosen here above the rest, remembered in `localStorage` under this
|
|
287
|
+
* key. Share a key between the fields that should share a history; leave unset to offer no such list.
|
|
288
|
+
*/
|
|
289
|
+
@property({attribute: 'slash-recent-key'}) slashRecentKey = '';
|
|
290
|
+
|
|
285
291
|
/**
|
|
286
292
|
* Resolves additional items each time the menu opens, for lists that come from elsewhere (e.g. an
|
|
287
293
|
* API). Receives the current query and may return a promise. JavaScript only.
|
|
@@ -1100,7 +1106,11 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
1100
1106
|
</div>
|
|
1101
1107
|
${this.hasSlashMenu
|
|
1102
1108
|
? html`
|
|
1103
|
-
<zn-slash-menu
|
|
1109
|
+
<zn-slash-menu
|
|
1110
|
+
part="slash-menu"
|
|
1111
|
+
heading=${this.slashHeading}
|
|
1112
|
+
recent-key=${this.slashRecentKey}
|
|
1113
|
+
?hide-keys=${this.slashHideKeys}></zn-slash-menu>`
|
|
1104
1114
|
: ''}
|
|
1105
1115
|
<slot name="slash-menu"></slot>
|
|
1106
1116
|
<slot name="slash-items" hidden></slot>
|
|
@@ -93,8 +93,13 @@
|
|
|
93
93
|
background-repeat: no-repeat;
|
|
94
94
|
background-position: bottom center;
|
|
95
95
|
background-size: cover;
|
|
96
|
-
mix-blend-mode
|
|
97
|
-
|
|
96
|
+
// PERF: mix-blend-mode forces .page__header .header — which contains the tab
|
|
97
|
+
// navbar — into its own render surface with a backdrop readback. Dropped in
|
|
98
|
+
// favour of plain alpha compositing. This is a small visual change to the
|
|
99
|
+
// aurora wave (it no longer darkens what it sits on); restore the blend mode
|
|
100
|
+
// if the look regresses, but expect the GPU cost back with it.
|
|
101
|
+
//mix-blend-mode: color-burn;
|
|
102
|
+
opacity: 0.04;
|
|
98
103
|
pointer-events: none;
|
|
99
104
|
}
|
|
100
105
|
|
package/src/components/page-builder/modules/page-section-card/page-section-card.component.ts
CHANGED
|
@@ -39,6 +39,8 @@ export default class ZnPageSectionCard extends ZincElement {
|
|
|
39
39
|
@property({type: Boolean, reflect: true}) selected = false;
|
|
40
40
|
/** Set when the section's type has no registered template — renders greyed. */
|
|
41
41
|
@property({type: Boolean, reflect: true}) unknown = false;
|
|
42
|
+
/** Set when the builder pins this section to the page — drops the remove action. */
|
|
43
|
+
@property({type: Boolean, reflect: true}) locked = false;
|
|
42
44
|
|
|
43
45
|
protected updated(changed: PropertyValues) {
|
|
44
46
|
super.updated(changed);
|
|
@@ -67,6 +69,10 @@ export default class ZnPageSectionCard extends ZincElement {
|
|
|
67
69
|
<span class="card__label">${this.label}</span>
|
|
68
70
|
${this.summary ? html`<span class="card__summary">${this.summary}</span>` : ''}
|
|
69
71
|
</span>
|
|
72
|
+
${this.locked ? html`
|
|
73
|
+
<span class="card__lock" title="This section cannot be removed">
|
|
74
|
+
<zn-icon src="lock" size="14"></zn-icon>
|
|
75
|
+
</span>` : ''}
|
|
70
76
|
<span class="card__actions" @keydown="${this._actionKeydown}">
|
|
71
77
|
<zn-button
|
|
72
78
|
class="card__action"
|
|
@@ -77,15 +83,16 @@ export default class ZnPageSectionCard extends ZincElement {
|
|
|
77
83
|
title="Duplicate section"
|
|
78
84
|
aria-label="Duplicate section"
|
|
79
85
|
@click="${(e: Event) => this._action(e, 'page-card-duplicate')}"></zn-button>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
${this.locked ? '' : html`
|
|
87
|
+
<zn-button
|
|
88
|
+
class="card__action"
|
|
89
|
+
icon-button="small"
|
|
90
|
+
plain
|
|
91
|
+
icon="delete"
|
|
92
|
+
icon-size="14"
|
|
93
|
+
title="Remove section"
|
|
94
|
+
aria-label="Remove section"
|
|
95
|
+
@click="${(e: Event) => this._action(e, 'page-card-remove')}"></zn-button>`}
|
|
89
96
|
</span>
|
|
90
97
|
</div>
|
|
91
98
|
`;
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
cursor: grabbing;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
:host([locked]),
|
|
19
|
+
:host([locked]:active) {
|
|
20
|
+
cursor: default;
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
.card {
|
|
19
24
|
display: flex;
|
|
20
25
|
align-items: center;
|
|
@@ -77,6 +82,14 @@
|
|
|
77
82
|
text-overflow: ellipsis;
|
|
78
83
|
}
|
|
79
84
|
|
|
85
|
+
// Always visible, unlike the hover actions — it is what explains their absence.
|
|
86
|
+
.card__lock {
|
|
87
|
+
display: flex;
|
|
88
|
+
flex: 0 0 auto;
|
|
89
|
+
align-items: center;
|
|
90
|
+
opacity: 0.45;
|
|
91
|
+
}
|
|
92
|
+
|
|
80
93
|
// Space is always reserved (visibility, not display) so the card never
|
|
81
94
|
// changes size or shifts its text when the actions appear on hover.
|
|
82
95
|
.card__actions {
|
|
@@ -41,6 +41,15 @@ describe('<zn-page-section-card>', () => {
|
|
|
41
41
|
expect(hostSawKeydown, 'host keydown suppressed for button activation keys').to.be.false;
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
it('should drop the remove action and show a lock when locked', async () => {
|
|
45
|
+
const el = await fixture<ZnPageSectionCard>(html`
|
|
46
|
+
<zn-page-section-card label="Hero" locked></zn-page-section-card>`);
|
|
47
|
+
expect(el.hasAttribute('locked')).to.be.true;
|
|
48
|
+
expect(el.shadowRoot!.querySelector('zn-button[title="Remove section"]'), 'remove gone').to.not.exist;
|
|
49
|
+
expect(el.shadowRoot!.querySelector('zn-button[title="Duplicate section"]'), 'duplicate kept').to.exist;
|
|
50
|
+
expect(el.shadowRoot!.querySelector('.card__lock'), 'lock indicator').to.exist;
|
|
51
|
+
});
|
|
52
|
+
|
|
44
53
|
it('should reflect selected and unknown states', async () => {
|
|
45
54
|
const el = await fixture<ZnPageSectionCard>(html`
|
|
46
55
|
<zn-page-section-card label="Hero" selected unknown></zn-page-section-card>`);
|
|
@@ -105,6 +105,14 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
105
105
|
@property({ reflect: true }) heading = '';
|
|
106
106
|
@property({ reflect: true }) subheading = '';
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Section type key that must lead the page. The builder hoists an existing section of
|
|
110
|
+
* that type to the top, or inserts an empty one, and pins it there: it can't be
|
|
111
|
+
* removed, reordered or dragged into a slot, and nothing can be dropped above it.
|
|
112
|
+
* Its content stays fully editable in the inspector.
|
|
113
|
+
*/
|
|
114
|
+
@property({ attribute: 'required-first', reflect: true }) requiredFirst = '';
|
|
115
|
+
|
|
108
116
|
/** Section types to make available, registered into the internal registry. */
|
|
109
117
|
@property({ attribute: false }) sectionTypes: PageSectionType[] = [];
|
|
110
118
|
|
|
@@ -224,6 +232,15 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
224
232
|
}
|
|
225
233
|
}
|
|
226
234
|
|
|
235
|
+
// Idempotent, so it does not matter whether this or handleConfigChange runs first.
|
|
236
|
+
@watch('requiredFirst')
|
|
237
|
+
handleRequiredFirstChange() {
|
|
238
|
+
const sections = this._requireFirst(this._state.sections);
|
|
239
|
+
if (sections === this._state.sections) return;
|
|
240
|
+
this._state = { sections };
|
|
241
|
+
this.defaultValue = JSON.stringify(this._state);
|
|
242
|
+
}
|
|
243
|
+
|
|
227
244
|
@watch('sectionTypes')
|
|
228
245
|
handleSectionTypesChange() {
|
|
229
246
|
this.registry.registerAll(this.sectionTypes ?? []);
|
|
@@ -450,13 +467,48 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
450
467
|
}
|
|
451
468
|
this._history = [];
|
|
452
469
|
this._redoStack = [];
|
|
453
|
-
this._state = { sections };
|
|
470
|
+
this._state = { sections: this._requireFirst(sections) };
|
|
454
471
|
this.defaultValue = JSON.stringify(this._state);
|
|
455
472
|
this._selectedId = null;
|
|
456
473
|
this._pickerIndex = null;
|
|
457
474
|
this._offerRestoreIfNewer();
|
|
458
475
|
}
|
|
459
476
|
|
|
477
|
+
// --- The pinned leading section (`required-first`) ---------------------------
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Id of the section pinned to the top of the page, or null when `required-first`
|
|
481
|
+
* is unset. Derived from the state rather than stored on it, so nothing about the
|
|
482
|
+
* lock leaks into the persisted config.
|
|
483
|
+
*/
|
|
484
|
+
private get _pinnedId(): string | null {
|
|
485
|
+
const first = this._state.sections[0];
|
|
486
|
+
return this.requiredFirst && first?.type === this.requiredFirst ? first.id : null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private _isPinned(id: string): boolean {
|
|
490
|
+
return this._pinnedId === id;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Lowest top-level index a section may be added or moved to. */
|
|
494
|
+
private get _firstFreeIndex(): number {
|
|
495
|
+
return this._pinnedId === null ? 0 : 1;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Sections reordered so `required-first` leads the page: an existing section of that
|
|
500
|
+
* type is hoisted to the front, otherwise an empty one is prepended. Returns the
|
|
501
|
+
* argument unchanged when there is nothing to do, so callers can compare by identity.
|
|
502
|
+
*/
|
|
503
|
+
private _requireFirst(sections: PageSection[]): PageSection[] {
|
|
504
|
+
if (!this.requiredFirst || sections[0]?.type === this.requiredFirst) return sections;
|
|
505
|
+
const at = sections.findIndex(s => s.type === this.requiredFirst);
|
|
506
|
+
if (at === -1) return [{ id: generateSectionId(), type: this.requiredFirst, data: {} }, ...sections];
|
|
507
|
+
const hoisted = [...sections];
|
|
508
|
+
hoisted.unshift(...hoisted.splice(at, 1));
|
|
509
|
+
return hoisted;
|
|
510
|
+
}
|
|
511
|
+
|
|
460
512
|
/** Finds a section by id, searching top-level sections and slotted children. */
|
|
461
513
|
private _findSection(id: string | null): PageSection | undefined {
|
|
462
514
|
if (!id) return undefined;
|
|
@@ -551,7 +603,7 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
551
603
|
this._pushHistory();
|
|
552
604
|
const section: PageSection = { id: generateSectionId(), type, data: {} };
|
|
553
605
|
const sections = [...this._state.sections];
|
|
554
|
-
sections.splice(index ?? sections.length, 0, section);
|
|
606
|
+
sections.splice(Math.max(index ?? sections.length, this._firstFreeIndex), 0, section);
|
|
555
607
|
this._commit({ sections });
|
|
556
608
|
this._select(section.id);
|
|
557
609
|
return section;
|
|
@@ -576,6 +628,7 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
576
628
|
}
|
|
577
629
|
|
|
578
630
|
private _removeSection(id: string) {
|
|
631
|
+
if (this._isPinned(id)) return;
|
|
579
632
|
const [removed, sections] = this._extract(id);
|
|
580
633
|
if (!removed) return;
|
|
581
634
|
this._pushHistory();
|
|
@@ -620,6 +673,8 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
620
673
|
|
|
621
674
|
/** Moves a section (top-level or slotted) to a top-level position. */
|
|
622
675
|
private _moveSection(id: string, index: number) {
|
|
676
|
+
if (this._isPinned(id)) return;
|
|
677
|
+
index = Math.max(index, this._firstFreeIndex);
|
|
623
678
|
const from = this._state.sections.findIndex(s => s.id === id);
|
|
624
679
|
if (from !== -1) {
|
|
625
680
|
const to = index > from ? index - 1 : index;
|
|
@@ -648,6 +703,7 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
648
703
|
const container = this._findSection(containerId);
|
|
649
704
|
const containerType = container ? this.registry.get(container.type) : undefined;
|
|
650
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
|
|
651
707
|
if (this.registry.get(moved.type)?.slots) return; // no containers inside slots
|
|
652
708
|
if (containerType.accepts && !containerType.accepts.includes(moved.type)) return;
|
|
653
709
|
if (slotIndex < 0 || slotIndex >= containerType.slots) return;
|
|
@@ -866,7 +922,7 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
866
922
|
Drag sections here to build your page
|
|
867
923
|
</div>` : ''}
|
|
868
924
|
${sections.map((section, i) => html`
|
|
869
|
-
${this._renderDropZone(i)}
|
|
925
|
+
${i === 0 && this._pinnedId !== null ? '' : this._renderDropZone(i)}
|
|
870
926
|
${this._renderCard(section, i)}
|
|
871
927
|
`)}
|
|
872
928
|
${this._renderDropZone(sections.length)}
|
|
@@ -881,10 +937,11 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
881
937
|
extraClass = ''
|
|
882
938
|
) {
|
|
883
939
|
const type = this.registry.get(section.type);
|
|
940
|
+
const pinned = this._isPinned(section.id);
|
|
884
941
|
return html`
|
|
885
942
|
<zn-page-section-card
|
|
886
943
|
class="${extraClass}"
|
|
887
|
-
draggable="true"
|
|
944
|
+
draggable="${pinned ? 'false' : 'true'}"
|
|
888
945
|
tabindex="0"
|
|
889
946
|
label="${section.label ?? type?.label ?? section.type}"
|
|
890
947
|
summary="${type ? sectionSummary(section, type) : `Unknown type "${section.type}"`}"
|
|
@@ -893,6 +950,7 @@ export default class ZnPageBuilder extends ZincElement {
|
|
|
893
950
|
color="${ifDefined(type?.color)}"
|
|
894
951
|
?selected="${this._selectedId === section.id}"
|
|
895
952
|
?unknown="${!type}"
|
|
953
|
+
?locked="${pinned}"
|
|
896
954
|
@click="${(e: Event) => {
|
|
897
955
|
e.stopPropagation();
|
|
898
956
|
this._select(section.id);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import '../../../dist/zn.min.js';
|
|
2
2
|
import {expect, fixture, html} from '@open-wc/testing';
|
|
3
|
+
import type {PageState} from './page.types';
|
|
3
4
|
import type ZnPageBuilder from './page-builder.component';
|
|
4
5
|
|
|
5
6
|
describe('<zn-page-builder>', () => {
|
|
@@ -575,6 +576,99 @@ describe('<zn-page-builder>', () => {
|
|
|
575
576
|
expect(el.state.sections[0].children?.[0] ?? null, 'Delete removes the child').to.be.null;
|
|
576
577
|
});
|
|
577
578
|
|
|
579
|
+
it('should pin a required-first section, inserting one when the page has none', async () => {
|
|
580
|
+
const el = await fixture<ZnPageBuilder>(html`
|
|
581
|
+
<zn-page-builder required-first="hero">
|
|
582
|
+
<template type="hero" slot="config" label="Hero"></template>
|
|
583
|
+
<template type="rich-text" slot="config" label="Rich Text"></template>
|
|
584
|
+
</zn-page-builder>`);
|
|
585
|
+
await el.updateComplete;
|
|
586
|
+
|
|
587
|
+
expect(el.state.sections.map(s => s.type), 'inserted into an empty page').to.deep.equal(['hero']);
|
|
588
|
+
// The submitted value carries the pinned section, so a save can't miss it.
|
|
589
|
+
expect((JSON.parse(el.value) as PageState).sections).to.have.length(1);
|
|
590
|
+
|
|
591
|
+
const card = el.shadowRoot!.querySelector('zn-page-section-card')!;
|
|
592
|
+
expect(card.hasAttribute('locked'), 'card is locked').to.be.true;
|
|
593
|
+
expect(card.getAttribute('draggable'), 'card is not draggable').to.equal('false');
|
|
594
|
+
// No "+" strip above it — the first drop zone follows the pinned card.
|
|
595
|
+
expect(el.shadowRoot!.querySelector('.canvas > *')?.tagName.toLowerCase()).to.equal('zn-page-section-card');
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it('should hoist an existing required-first section to the top of the page', async () => {
|
|
599
|
+
const el = await fixture<ZnPageBuilder>(html`
|
|
600
|
+
<zn-page-builder required-first="hero"
|
|
601
|
+
config='{"sections":[{"id":"t","type":"rich-text","data":{}},{"id":"h","type":"hero","data":{"title":"Help"}}]}'>
|
|
602
|
+
<template type="hero" slot="config" label="Hero"></template>
|
|
603
|
+
<template type="rich-text" slot="config" label="Rich Text"></template>
|
|
604
|
+
</zn-page-builder>`);
|
|
605
|
+
await el.updateComplete;
|
|
606
|
+
|
|
607
|
+
expect(el.state.sections.map(s => s.id)).to.deep.equal(['h', 't']);
|
|
608
|
+
expect(el.state.sections[0].data, 'hoisted with its data intact').to.deep.equal({title: 'Help'});
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('should refuse to remove, reorder or displace the pinned section', async () => {
|
|
612
|
+
const el = await fixture<ZnPageBuilder>(html`
|
|
613
|
+
<zn-page-builder required-first="hero"
|
|
614
|
+
config='{"sections":[{"id":"h","type":"hero","data":{}},{"id":"t","type":"rich-text","data":{}}]}'>
|
|
615
|
+
<template type="hero" slot="config" label="Hero"></template>
|
|
616
|
+
<template type="rich-text" slot="config" label="Rich Text"></template>
|
|
617
|
+
</zn-page-builder>`);
|
|
618
|
+
await el.updateComplete;
|
|
619
|
+
|
|
620
|
+
const pinnedCard = el.shadowRoot!.querySelector('zn-page-section-card')!;
|
|
621
|
+
expect(pinnedCard.shadowRoot!.querySelector('zn-button[title="Remove section"]'), 'no remove action').to.not.exist;
|
|
622
|
+
|
|
623
|
+
pinnedCard.dispatchEvent(new CustomEvent('page-card-remove', {bubbles: true, composed: true}));
|
|
624
|
+
pinnedCard.dispatchEvent(new KeyboardEvent('keydown', {key: 'Delete', bubbles: true, cancelable: true}));
|
|
625
|
+
await el.updateComplete;
|
|
626
|
+
expect(el.state.sections.map(s => s.id), 'survives remove and Delete').to.deep.equal(['h', 't']);
|
|
627
|
+
|
|
628
|
+
// Dropping another section on the topmost zone lands it below the pinned one.
|
|
629
|
+
const dataTransfer = new DataTransfer();
|
|
630
|
+
dataTransfer.setData('application/x-zn-page-section', 't');
|
|
631
|
+
el.shadowRoot!.querySelector('.drop')!
|
|
632
|
+
.dispatchEvent(new DragEvent('drop', {dataTransfer, bubbles: true, cancelable: true}));
|
|
633
|
+
await el.updateComplete;
|
|
634
|
+
expect(el.state.sections.map(s => s.id), 'nothing lands above the pinned section').to.deep.equal(['h', 't']);
|
|
635
|
+
|
|
636
|
+
expect(el.addSection('rich-text', 0)).to.not.be.null;
|
|
637
|
+
expect(el.state.sections[0].id, 'addSection index 0 is clamped').to.equal('h');
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('should keep the pinned section out of container slots', async () => {
|
|
641
|
+
const el = await fixture<ZnPageBuilder>(html`
|
|
642
|
+
<zn-page-builder required-first="hero"
|
|
643
|
+
config='{"sections":[{"id":"h","type":"hero","data":{}},{"id":"grid","type":"article-grid","data":{}}]}'>
|
|
644
|
+
<template type="hero" slot="config" label="Hero"></template>
|
|
645
|
+
<template type="article-grid" slot="config" label="Article Grid" slots="2"></template>
|
|
646
|
+
</zn-page-builder>`);
|
|
647
|
+
await el.updateComplete;
|
|
648
|
+
|
|
649
|
+
const dataTransfer = new DataTransfer();
|
|
650
|
+
dataTransfer.setData('application/x-zn-page-section', 'h');
|
|
651
|
+
el.shadowRoot!.querySelector('.slot--empty')!
|
|
652
|
+
.dispatchEvent(new DragEvent('drop', {dataTransfer, bubbles: true, cancelable: true}));
|
|
653
|
+
await el.updateComplete;
|
|
654
|
+
|
|
655
|
+
expect(el.state.sections[0].id, 'still leading the page').to.equal('h');
|
|
656
|
+
expect(el.state.sections[1].children?.[0] ?? null, 'slot left empty').to.be.null;
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it('should leave the page alone without required-first', async () => {
|
|
660
|
+
const el = await fixture<ZnPageBuilder>(html`
|
|
661
|
+
<zn-page-builder config='{"sections":[{"id":"t","type":"rich-text","data":{}}]}'>
|
|
662
|
+
<template type="rich-text" slot="config" label="Rich Text"></template>
|
|
663
|
+
</zn-page-builder>`);
|
|
664
|
+
await el.updateComplete;
|
|
665
|
+
|
|
666
|
+
expect(el.state.sections.map(s => s.type)).to.deep.equal(['rich-text']);
|
|
667
|
+
const card = el.shadowRoot!.querySelector('zn-page-section-card')!;
|
|
668
|
+
expect(card.hasAttribute('locked')).to.be.false;
|
|
669
|
+
expect(card.getAttribute('draggable')).to.equal('true');
|
|
670
|
+
});
|
|
671
|
+
|
|
578
672
|
it('should accept a palette drop anywhere on the empty canvas', async () => {
|
|
579
673
|
const el = await fixture<ZnPageBuilder>(html`
|
|
580
674
|
<zn-page-builder>
|