@kubex/zinc 1.1.95 → 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 +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 +331 -377
- 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/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,37 +1,8 @@
|
|
|
1
1
|
import '../../../dist/zn.min.js';
|
|
2
2
|
import {expect, fixture, html} from '@open-wc/testing';
|
|
3
|
-
import {
|
|
4
|
-
import type {PageSectionType, PageState} from './page.types';
|
|
3
|
+
import type {PageState} from './page.types';
|
|
5
4
|
import type ZnPageBuilder from './page-builder.component';
|
|
6
5
|
|
|
7
|
-
/** Reaches the private registry to inspect a parsed template's PageSectionType. */
|
|
8
|
-
const registryOf = (el: ZnPageBuilder) =>
|
|
9
|
-
(el as unknown as { registry: { get: (type: string) => PageSectionType | undefined } }).registry;
|
|
10
|
-
|
|
11
|
-
const containerConfig = (over = {}) => JSON.stringify({
|
|
12
|
-
sections: [{
|
|
13
|
-
id: 'outer', type: 'row', data: {},
|
|
14
|
-
layout: {widths: [1, 2, 1], grow: false},
|
|
15
|
-
cells: [[{id: 'a', type: 'hero', data: {}}], [], []],
|
|
16
|
-
...over,
|
|
17
|
-
}],
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
const selectFirst = async (el: ZnPageBuilder) => {
|
|
21
|
-
el.shadowRoot?.querySelector('zn-page-section-card')?.dispatchEvent(new Event('click'));
|
|
22
|
-
await el.updateComplete;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const dragTo = (el: Element, mime: string, payload: string, kind: 'dragover' | 'drop') => {
|
|
26
|
-
const data = new Map([[mime, payload]]);
|
|
27
|
-
const event = new DragEvent(kind, {bubbles: true, cancelable: true, composed: true});
|
|
28
|
-
Object.defineProperty(event, 'dataTransfer', {
|
|
29
|
-
value: {types: [...data.keys()], getData: (k: string) => data.get(k) ?? '', setData: () => {}},
|
|
30
|
-
});
|
|
31
|
-
el.dispatchEvent(event);
|
|
32
|
-
return event;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
6
|
describe('<zn-page-builder>', () => {
|
|
36
7
|
it('should render the shell', async () => {
|
|
37
8
|
const el = await fixture<ZnPageBuilder>(html`
|
|
@@ -57,58 +28,6 @@ describe('<zn-page-builder>', () => {
|
|
|
57
28
|
expect(categories).to.deep.equal(['Headers', 'Content']);
|
|
58
29
|
});
|
|
59
30
|
|
|
60
|
-
it('should parse container/columns/widths/grow template attributes', async () => {
|
|
61
|
-
const el = await fixture<ZnPageBuilder>(html`
|
|
62
|
-
<zn-page-builder>
|
|
63
|
-
<template type="row" slot="config" label="Row" container widths="1, 2 1" grow></template>
|
|
64
|
-
<template type="cols" slot="config" label="Cols" container columns="4"></template>
|
|
65
|
-
<template type="bare" slot="config" label="Bare" container></template>
|
|
66
|
-
<template type="legacy" slot="config" label="Legacy" slots="6"></template>
|
|
67
|
-
</zn-page-builder>`);
|
|
68
|
-
await el.updateComplete;
|
|
69
|
-
const registry = registryOf(el);
|
|
70
|
-
|
|
71
|
-
// widths beats columns and tolerates mixed comma/whitespace separators.
|
|
72
|
-
const row = registry.get('row');
|
|
73
|
-
expect(row?.container).to.be.true;
|
|
74
|
-
expect(row?.defaultWidths).to.deep.equal([1, 2, 1]);
|
|
75
|
-
expect(row?.defaultGrow).to.be.true;
|
|
76
|
-
|
|
77
|
-
expect(registry.get('cols')?.defaultWidths).to.deep.equal([1, 1, 1, 1]);
|
|
78
|
-
expect(registry.get('cols')?.defaultGrow).to.be.false;
|
|
79
|
-
|
|
80
|
-
// bare `container` seeds DEFAULT_WIDTHS.
|
|
81
|
-
expect(registry.get('bare')?.defaultWidths).to.deep.equal([1, 1, 1]);
|
|
82
|
-
expect(registry.get('bare')?.defaultGrow).to.be.false;
|
|
83
|
-
|
|
84
|
-
// legacy `slots` keeps working, untouched by the new fields.
|
|
85
|
-
const legacy = registry.get('legacy');
|
|
86
|
-
expect(legacy?.slots).to.equal(6);
|
|
87
|
-
expect(legacy?.container).to.be.undefined;
|
|
88
|
-
expect(legacy?.defaultWidths).to.be.undefined;
|
|
89
|
-
expect(legacy?.defaultGrow).to.be.undefined;
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('should filter non-numeric widths tokens before falling back to columns/DEFAULT_WIDTHS', async () => {
|
|
93
|
-
const el = await fixture<ZnPageBuilder>(html`
|
|
94
|
-
<zn-page-builder>
|
|
95
|
-
<template type="typo" slot="config" label="Typo" container widths="1 x 2"></template>
|
|
96
|
-
<template type="rescue" slot="config" label="Rescue" container widths="abc" columns="4"></template>
|
|
97
|
-
<template type="garbage" slot="config" label="Garbage" container widths="abc"></template>
|
|
98
|
-
</zn-page-builder>`);
|
|
99
|
-
await el.updateComplete;
|
|
100
|
-
const registry = registryOf(el);
|
|
101
|
-
|
|
102
|
-
// A stray non-numeric token is dropped, not coerced to a column via sanitiseWidths.
|
|
103
|
-
expect(registry.get('typo')?.defaultWidths).to.deep.equal([1, 2]);
|
|
104
|
-
|
|
105
|
-
// A wholly non-numeric widths falls through to columns, not to a single column.
|
|
106
|
-
expect(registry.get('rescue')?.defaultWidths).to.deep.equal([1, 1, 1, 1]);
|
|
107
|
-
|
|
108
|
-
// A wholly non-numeric widths with no columns falls through to DEFAULT_WIDTHS.
|
|
109
|
-
expect(registry.get('garbage')?.defaultWidths).to.deep.equal([1, 1, 1]);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
31
|
it('should tuck the palette away via the canvas-edge chevron', async () => {
|
|
113
32
|
const el = await fixture<ZnPageBuilder>(html`
|
|
114
33
|
<zn-page-builder>
|
|
@@ -473,208 +392,82 @@ describe('<zn-page-builder>', () => {
|
|
|
473
392
|
await expect(el).to.be.accessible();
|
|
474
393
|
});
|
|
475
394
|
|
|
476
|
-
it('should
|
|
395
|
+
it('should render a slot grid for container sections and fill slots via addSectionToSlot', async () => {
|
|
477
396
|
const el = await fixture<ZnPageBuilder>(html`
|
|
478
|
-
<zn-page-builder
|
|
397
|
+
<zn-page-builder config='{"sections":[{"id":"grid","type":"article-grid","data":{}}]}'>
|
|
398
|
+
<template type="article-grid" slot="config" label="Article Grid" slots="6" accepts="article-tile"></template>
|
|
399
|
+
<template type="article-tile" slot="config" label="Article"></template>
|
|
479
400
|
<template type="hero" slot="config" label="Hero"></template>
|
|
480
|
-
<template type="rich-text" slot="config" label="Rich Text"></template>
|
|
481
401
|
</zn-page-builder>`);
|
|
482
402
|
await el.updateComplete;
|
|
483
403
|
|
|
484
|
-
expect(el.
|
|
485
|
-
// The submitted value carries the pinned section, so a save can't miss it.
|
|
486
|
-
expect((JSON.parse(el.value) as PageState).sections).to.have.length(1);
|
|
404
|
+
expect(el.shadowRoot?.querySelectorAll('.slot--empty')).to.have.length(6);
|
|
487
405
|
|
|
488
|
-
|
|
489
|
-
expect(
|
|
490
|
-
expect(card.getAttribute('draggable'), 'card is not draggable').to.equal('false');
|
|
491
|
-
// No "+" strip above it — the first drop zone follows the pinned card.
|
|
492
|
-
expect(el.shadowRoot!.querySelector('.canvas > *')?.tagName.toLowerCase()).to.equal('zn-page-section-card');
|
|
493
|
-
});
|
|
406
|
+
expect(el.addSectionToSlot('hero', 'grid', 0), 'not in accepts').to.be.null;
|
|
407
|
+
expect(el.addSectionToSlot('article-grid', 'grid', 0), 'no nested containers').to.be.null;
|
|
494
408
|
|
|
495
|
-
|
|
496
|
-
const el = await fixture<ZnPageBuilder>(html`
|
|
497
|
-
<zn-page-builder required-first="hero"
|
|
498
|
-
config='{"sections":[{"id":"t","type":"rich-text","data":{}},{"id":"h","type":"hero","data":{"title":"Help"}}]}'>
|
|
499
|
-
<template type="hero" slot="config" label="Hero"></template>
|
|
500
|
-
<template type="rich-text" slot="config" label="Rich Text"></template>
|
|
501
|
-
</zn-page-builder>`);
|
|
409
|
+
const child = el.addSectionToSlot('article-tile', 'grid', 1);
|
|
502
410
|
await el.updateComplete;
|
|
411
|
+
expect(child?.type).to.equal('article-tile');
|
|
412
|
+
expect(el.state.sections[0].children?.[1]?.id).to.equal(child?.id);
|
|
413
|
+
expect(el.shadowRoot?.querySelectorAll('.slot--empty')).to.have.length(5);
|
|
414
|
+
expect(el.shadowRoot?.querySelectorAll('.slots zn-page-section-card')).to.have.length(1);
|
|
503
415
|
|
|
504
|
-
expect(el.
|
|
505
|
-
expect(el.state.sections[0].data, 'hoisted with its data intact').to.deep.equal({title: 'Help'});
|
|
416
|
+
expect(el.addSectionToSlot('article-tile', 'grid', 1), 'occupied slot').to.be.null;
|
|
506
417
|
});
|
|
507
418
|
|
|
508
|
-
it('should
|
|
419
|
+
it('should swap children when dropping one filled slot onto another', async () => {
|
|
509
420
|
const el = await fixture<ZnPageBuilder>(html`
|
|
510
|
-
<zn-page-builder
|
|
511
|
-
|
|
512
|
-
<template type="
|
|
513
|
-
<template type="rich-text" slot="config" label="Rich Text"></template>
|
|
421
|
+
<zn-page-builder config='{"sections":[{"id":"grid","type":"article-grid","data":{}}]}'>
|
|
422
|
+
<template type="article-grid" slot="config" label="Article Grid" slots="4"></template>
|
|
423
|
+
<template type="article-tile" slot="config" label="Article"></template>
|
|
514
424
|
</zn-page-builder>`);
|
|
515
425
|
await el.updateComplete;
|
|
516
426
|
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
pinnedCard.dispatchEvent(new CustomEvent('page-card-remove', {bubbles: true, composed: true}));
|
|
521
|
-
pinnedCard.dispatchEvent(new KeyboardEvent('keydown', {key: 'Delete', bubbles: true, cancelable: true}));
|
|
427
|
+
const a = el.addSectionToSlot('article-tile', 'grid', 0)!;
|
|
428
|
+
const b = el.addSectionToSlot('article-tile', 'grid', 1)!;
|
|
522
429
|
await el.updateComplete;
|
|
523
|
-
expect(el.state.sections.map(s => s.id), 'survives remove and Delete').to.deep.equal(['h', 't']);
|
|
524
430
|
|
|
525
|
-
//
|
|
431
|
+
// Drag child A onto child B's card (slot 1) — they swap places.
|
|
432
|
+
const cards = el.shadowRoot!.querySelectorAll('.slots zn-page-section-card');
|
|
526
433
|
const dataTransfer = new DataTransfer();
|
|
527
|
-
dataTransfer.setData('application/x-zn-page-section',
|
|
528
|
-
|
|
529
|
-
.dispatchEvent(new DragEvent('drop', {dataTransfer, bubbles: true, cancelable: true}));
|
|
530
|
-
await el.updateComplete;
|
|
531
|
-
expect(el.state.sections.map(s => s.id), 'nothing lands above the pinned section').to.deep.equal(['h', 't']);
|
|
532
|
-
|
|
533
|
-
expect(el.addSection('rich-text', 0)).to.not.be.null;
|
|
534
|
-
expect(el.state.sections[0].id, 'addSection index 0 is clamped').to.equal('h');
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
it('should seed layout and cells for an inserted required-first container', async () => {
|
|
538
|
-
const el = await fixture<ZnPageBuilder>(html`
|
|
539
|
-
<zn-page-builder required-first="row">
|
|
540
|
-
<template type="row" slot="config" label="Row" container widths="1 2 1"></template>
|
|
541
|
-
</zn-page-builder>`);
|
|
434
|
+
dataTransfer.setData('application/x-zn-page-section', a.id);
|
|
435
|
+
cards[1].dispatchEvent(new DragEvent('drop', {dataTransfer, bubbles: true, cancelable: true}));
|
|
542
436
|
await el.updateComplete;
|
|
543
437
|
|
|
544
|
-
|
|
545
|
-
expect(
|
|
546
|
-
expect(row.layout, 'the one guaranteed section is seeded like any other container').to.deep.equal({widths: [1, 2, 1], grow: false});
|
|
547
|
-
expect(row.cells).to.have.lengthOf(3);
|
|
438
|
+
expect(el.state.sections[0].children?.[0]?.id).to.equal(b.id);
|
|
439
|
+
expect(el.state.sections[0].children?.[1]?.id).to.equal(a.id);
|
|
548
440
|
});
|
|
549
441
|
|
|
550
|
-
it('should
|
|
442
|
+
it('should edit a slotted child through the inspector', async () => {
|
|
551
443
|
const el = await fixture<ZnPageBuilder>(html`
|
|
552
|
-
<zn-page-builder config='{"sections":[{"id":"
|
|
553
|
-
<template type="
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
expect(el.state.sections.map(s => s.type)).to.deep.equal(['rich-text']);
|
|
558
|
-
const card = el.shadowRoot!.querySelector('zn-page-section-card')!;
|
|
559
|
-
expect(card.hasAttribute('locked')).to.be.false;
|
|
560
|
-
expect(card.getAttribute('draggable')).to.equal('true');
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
it('should accept a palette drop anywhere on the empty canvas', async () => {
|
|
564
|
-
const el = await fixture<ZnPageBuilder>(html`
|
|
565
|
-
<zn-page-builder>
|
|
566
|
-
<template type="hero" slot="config" label="Hero"></template>
|
|
567
|
-
</zn-page-builder>`);
|
|
568
|
-
await el.updateComplete;
|
|
569
|
-
|
|
570
|
-
const canvas = el.shadowRoot!.querySelector('.canvas')!;
|
|
571
|
-
const dataTransfer = new DataTransfer();
|
|
572
|
-
dataTransfer.setData('application/x-zn-page-type', 'hero');
|
|
573
|
-
|
|
574
|
-
const over = new DragEvent('dragover', {dataTransfer, bubbles: true, cancelable: true});
|
|
575
|
-
canvas.dispatchEvent(over);
|
|
576
|
-
expect(over.defaultPrevented, 'canvas accepts the drag').to.be.true;
|
|
577
|
-
|
|
578
|
-
canvas.dispatchEvent(new DragEvent('drop', {dataTransfer, bubbles: true, cancelable: true}));
|
|
579
|
-
await el.updateComplete;
|
|
580
|
-
expect(el.state.sections).to.have.length(1);
|
|
581
|
-
expect(el.state.sections[0].type).to.equal('hero');
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
it('round-trips an old-shape config into the new cells shape', async () => {
|
|
585
|
-
const el = await fixture<ZnPageBuilder>(html`
|
|
586
|
-
<zn-page-builder config='{"sections":[{"id":"g1","type":"grid","data":{},"children":[{"id":"a","type":"hero","data":{"title":"Kept"}},null,null]}]}'>
|
|
587
|
-
<template type="grid" slot="config" label="Grid" slots="6"></template>
|
|
588
|
-
<template type="hero" slot="config" label="Hero"><zn-input name="title" label="Title"></zn-input></template>
|
|
589
|
-
</zn-page-builder>`);
|
|
590
|
-
await el.updateComplete;
|
|
591
|
-
|
|
592
|
-
const grid = el.state.sections[0];
|
|
593
|
-
expect(grid.children, 'legacy shape not written back').to.be.undefined;
|
|
594
|
-
expect(grid.layout).to.deep.equal({widths: [1, 1, 1], grow: false});
|
|
595
|
-
expect(grid.cells).to.have.lengthOf(6);
|
|
596
|
-
expect(grid.cells?.[0][0].data.title).to.equal('Kept');
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
it('seeds layout and cells when a container is added', async () => {
|
|
600
|
-
const el = await fixture<ZnPageBuilder>(html`
|
|
601
|
-
<zn-page-builder>
|
|
602
|
-
<template type="row" slot="config" label="Row" container widths="1 2 1"></template>
|
|
603
|
-
</zn-page-builder>`);
|
|
604
|
-
await el.updateComplete;
|
|
605
|
-
|
|
606
|
-
el.addSection('row');
|
|
607
|
-
await el.updateComplete;
|
|
608
|
-
|
|
609
|
-
const row = el.state.sections[0];
|
|
610
|
-
expect(row.layout).to.deep.equal({widths: [1, 2, 1], grow: false});
|
|
611
|
-
expect(row.cells).to.have.lengthOf(3);
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
it('renames and edits a section nested two container levels deep', async () => {
|
|
615
|
-
const config = JSON.stringify({
|
|
616
|
-
sections: [{
|
|
617
|
-
id: 'outer', type: 'row', data: {}, layout: {widths: [1, 1], grow: false},
|
|
618
|
-
cells: [[{
|
|
619
|
-
id: 'inner', type: 'row', data: {}, layout: {widths: [1, 1], grow: false},
|
|
620
|
-
cells: [[{id: 'deep', type: 'hero', data: {}}], []],
|
|
621
|
-
}], []],
|
|
622
|
-
}],
|
|
623
|
-
});
|
|
624
|
-
const el = await fixture<ZnPageBuilder>(html`
|
|
625
|
-
<zn-page-builder config=${config}>
|
|
626
|
-
<template type="row" slot="config" label="Row" container></template>
|
|
627
|
-
<template type="hero" slot="config" label="Hero"><input name="title"></template>
|
|
444
|
+
<zn-page-builder config='{"sections":[{"id":"grid","type":"article-grid","data":{},"children":[{"id":"c1","type":"article-tile","data":{"title":"Old"}}]}]}'>
|
|
445
|
+
<template type="article-grid" slot="config" label="Article Grid" slots="2"></template>
|
|
446
|
+
<template type="article-tile" slot="config" label="Article">
|
|
447
|
+
<input name="title">
|
|
448
|
+
</template>
|
|
628
449
|
</zn-page-builder>`);
|
|
629
450
|
await el.updateComplete;
|
|
630
451
|
|
|
631
|
-
|
|
452
|
+
el.shadowRoot?.querySelector('.slots zn-page-section-card')?.dispatchEvent(new Event('click'));
|
|
632
453
|
await el.updateComplete;
|
|
633
454
|
|
|
634
455
|
const input = el.shadowRoot?.querySelector<HTMLInputElement>('.inspector__form input[name="title"]');
|
|
635
|
-
expect(input, '
|
|
636
|
-
input!.value
|
|
637
|
-
input!.dispatchEvent(new Event('input', {bubbles: true, composed: true}));
|
|
638
|
-
await el.updateComplete;
|
|
456
|
+
expect(input, 'stamped input for child').to.exist;
|
|
457
|
+
expect(input!.value).to.equal('Old');
|
|
639
458
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
it('duplicates a container with fresh ids throughout', async () => {
|
|
645
|
-
const config = JSON.stringify({
|
|
646
|
-
sections: [{
|
|
647
|
-
id: 'outer', type: 'row', data: {}, layout: {widths: [1, 1], grow: false},
|
|
648
|
-
cells: [[{id: 'a', type: 'hero', data: {}}], []],
|
|
649
|
-
}],
|
|
650
|
-
});
|
|
651
|
-
const el = await fixture<ZnPageBuilder>(html`
|
|
652
|
-
<zn-page-builder config=${config}>
|
|
653
|
-
<template type="row" slot="config" label="Row" container></template>
|
|
654
|
-
<template type="hero" slot="config" label="Hero"></template>
|
|
655
|
-
</zn-page-builder>`);
|
|
656
|
-
await el.updateComplete;
|
|
657
|
-
|
|
658
|
-
el.shadowRoot?.querySelector('zn-page-section-card')
|
|
659
|
-
?.dispatchEvent(new CustomEvent('page-card-duplicate', {bubbles: true, composed: true}));
|
|
459
|
+
input!.value = 'New';
|
|
460
|
+
input!.dispatchEvent(new Event('change', {bubbles: true}));
|
|
660
461
|
await el.updateComplete;
|
|
661
462
|
|
|
662
|
-
expect(el.state.sections).to.
|
|
663
|
-
const ids = el.state.sections.flatMap(s => [s.id, ...(s.cells ?? []).flat().map(c => c.id)]);
|
|
664
|
-
expect(new Set(ids).size, 'every id unique').to.equal(ids.length);
|
|
463
|
+
expect(el.state.sections[0].children?.[0]?.data.title).to.equal('New');
|
|
665
464
|
});
|
|
666
465
|
|
|
667
|
-
it('
|
|
668
|
-
const config = JSON.stringify({
|
|
669
|
-
sections: [{
|
|
670
|
-
id: 'outer', type: 'row', data: {}, layout: {widths: [1, 1], grow: false},
|
|
671
|
-
cells: [[{id: 'a', type: 'hero', data: {}}], []],
|
|
672
|
-
}],
|
|
673
|
-
});
|
|
466
|
+
it('should clear selection when removing a container holding the selected child', async () => {
|
|
674
467
|
const el = await fixture<ZnPageBuilder>(html`
|
|
675
|
-
<zn-page-builder config
|
|
676
|
-
<template type="
|
|
677
|
-
<template type="
|
|
468
|
+
<zn-page-builder config='{"sections":[{"id":"grid","type":"article-grid","data":{},"children":[{"id":"c1","type":"article-tile","data":{}}]}]}'>
|
|
469
|
+
<template type="article-grid" slot="config" label="Article Grid" slots="2"></template>
|
|
470
|
+
<template type="article-tile" slot="config" label="Article"></template>
|
|
678
471
|
</zn-page-builder>`);
|
|
679
472
|
await el.updateComplete;
|
|
680
473
|
|
|
@@ -682,463 +475,218 @@ describe('<zn-page-builder>', () => {
|
|
|
682
475
|
el.addEventListener('zn-page-selection-change', e =>
|
|
683
476
|
(lastSelection = (e as CustomEvent<{sectionId: string | null}>).detail.sectionId));
|
|
684
477
|
|
|
685
|
-
|
|
478
|
+
// Select the child, then remove its parent container.
|
|
479
|
+
el.shadowRoot?.querySelector('.slots zn-page-section-card')?.dispatchEvent(new Event('click'));
|
|
686
480
|
await el.updateComplete;
|
|
687
|
-
expect(lastSelection).to.equal('
|
|
481
|
+
expect(lastSelection).to.equal('c1');
|
|
688
482
|
|
|
689
|
-
|
|
483
|
+
el.shadowRoot?.querySelector('.container > zn-page-section-card')
|
|
484
|
+
?.dispatchEvent(new CustomEvent('page-card-remove'));
|
|
690
485
|
await el.updateComplete;
|
|
691
486
|
|
|
692
|
-
expect(el.state.sections[0].cells?.[0]).to.deep.equal([]);
|
|
693
487
|
expect(lastSelection, 'selection cleared').to.be.null;
|
|
488
|
+
expect(el.shadowRoot?.querySelector('[part="inspector"]'), 'no ghost inspector').to.not.exist;
|
|
694
489
|
});
|
|
695
490
|
|
|
696
|
-
it('
|
|
697
|
-
const config = JSON.stringify({
|
|
698
|
-
sections: [{
|
|
699
|
-
id: 'outer', type: 'row', data: {}, layout: {widths: [1, 1], grow: true},
|
|
700
|
-
cells: [
|
|
701
|
-
[{id: 'a', type: 'hero', data: {}}], [{id: 'b', type: 'hero', data: {}}],
|
|
702
|
-
[{id: 'c', type: 'hero', data: {}}], [],
|
|
703
|
-
],
|
|
704
|
-
}],
|
|
705
|
-
});
|
|
706
|
-
const el = await fixture<ZnPageBuilder>(html`
|
|
707
|
-
<zn-page-builder config=${config}>
|
|
708
|
-
<template type="row" slot="config" label="Row" container grow></template>
|
|
709
|
-
<template type="hero" slot="config" label="Hero"></template>
|
|
710
|
-
</zn-page-builder>`);
|
|
711
|
-
await el.updateComplete;
|
|
712
|
-
|
|
713
|
-
(el as unknown as {_removeSection(id: string): void})._removeSection('c');
|
|
714
|
-
await el.updateComplete;
|
|
715
|
-
|
|
716
|
-
// Removing "c" leaves the last row (cells 2 and 3) all-empty — trimmed immediately,
|
|
717
|
-
// not only the next time the container is read out.
|
|
718
|
-
expect(el.state.sections[0].cells, 'trailing empty row trimmed on commit').to.have.lengthOf(2);
|
|
719
|
-
expect(el.state.sections[0].cells?.flat().map(c => c.id)).to.deep.equal(['a', 'b']);
|
|
720
|
-
});
|
|
721
|
-
|
|
722
|
-
it('leaves a fixed container trailing empty row untouched after a commit', async () => {
|
|
723
|
-
const config = JSON.stringify({
|
|
724
|
-
sections: [{
|
|
725
|
-
id: 'outer', type: 'row', data: {}, layout: {widths: [1, 1], grow: false},
|
|
726
|
-
cells: [
|
|
727
|
-
[{id: 'a', type: 'hero', data: {}}], [{id: 'b', type: 'hero', data: {}}],
|
|
728
|
-
[{id: 'c', type: 'hero', data: {}}], [],
|
|
729
|
-
],
|
|
730
|
-
}],
|
|
731
|
-
});
|
|
732
|
-
const el = await fixture<ZnPageBuilder>(html`
|
|
733
|
-
<zn-page-builder config=${config}>
|
|
734
|
-
<template type="row" slot="config" label="Row" container></template>
|
|
735
|
-
<template type="hero" slot="config" label="Hero"></template>
|
|
736
|
-
</zn-page-builder>`);
|
|
737
|
-
await el.updateComplete;
|
|
738
|
-
|
|
739
|
-
(el as unknown as {_removeSection(id: string): void})._removeSection('c');
|
|
740
|
-
await el.updateComplete;
|
|
741
|
-
|
|
742
|
-
// A fixed container's trailing empty row is its chosen layout — it must survive.
|
|
743
|
-
expect(el.state.sections[0].cells, 'trailing empty row survives').to.have.lengthOf(4);
|
|
744
|
-
expect(el.state.sections[0].cells?.[3]).to.deep.equal([]);
|
|
745
|
-
});
|
|
746
|
-
|
|
747
|
-
it('clears selection when removing an ancestor of the selected grandchild', async () => {
|
|
748
|
-
const config = JSON.stringify({
|
|
749
|
-
sections: [{
|
|
750
|
-
id: 'outer', type: 'row', data: {}, layout: {widths: [1, 1], grow: false},
|
|
751
|
-
cells: [[{
|
|
752
|
-
id: 'inner', type: 'row', data: {}, layout: {widths: [1, 1], grow: false},
|
|
753
|
-
cells: [[{id: 'deep', type: 'hero', data: {}}], []],
|
|
754
|
-
}], []],
|
|
755
|
-
}],
|
|
756
|
-
});
|
|
757
|
-
const el = await fixture<ZnPageBuilder>(html`
|
|
758
|
-
<zn-page-builder config=${config}>
|
|
759
|
-
<template type="row" slot="config" label="Row" container></template>
|
|
760
|
-
<template type="hero" slot="config" label="Hero"></template>
|
|
761
|
-
</zn-page-builder>`);
|
|
762
|
-
await el.updateComplete;
|
|
763
|
-
|
|
764
|
-
let lastSelection: string | null | undefined;
|
|
765
|
-
el.addEventListener('zn-page-selection-change', e =>
|
|
766
|
-
(lastSelection = (e as CustomEvent<{sectionId: string | null}>).detail.sectionId));
|
|
767
|
-
|
|
768
|
-
(el as unknown as {_select(id: string): void})._select('deep');
|
|
769
|
-
await el.updateComplete;
|
|
770
|
-
expect(lastSelection).to.equal('deep');
|
|
771
|
-
|
|
772
|
-
// Removing the top-level ancestor must clear selection of a grandchild two cell levels down.
|
|
773
|
-
(el as unknown as {_removeSection(id: string): void})._removeSection('outer');
|
|
774
|
-
await el.updateComplete;
|
|
775
|
-
|
|
776
|
-
expect(el.state.sections).to.have.lengthOf(0);
|
|
777
|
-
expect(lastSelection, 'selection cleared at depth 2').to.be.null;
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
it('duplicates a section living in a cell directly below itself in the same stack', async () => {
|
|
781
|
-
const config = JSON.stringify({
|
|
782
|
-
sections: [{
|
|
783
|
-
id: 'outer', type: 'row', data: {}, layout: {widths: [1, 1], grow: false},
|
|
784
|
-
cells: [[{id: 'a', type: 'hero', data: {}}], []],
|
|
785
|
-
}],
|
|
786
|
-
});
|
|
787
|
-
const el = await fixture<ZnPageBuilder>(html`
|
|
788
|
-
<zn-page-builder config=${config}>
|
|
789
|
-
<template type="row" slot="config" label="Row" container></template>
|
|
790
|
-
<template type="hero" slot="config" label="Hero"></template>
|
|
791
|
-
</zn-page-builder>`);
|
|
792
|
-
await el.updateComplete;
|
|
793
|
-
|
|
794
|
-
(el as unknown as {_duplicateSection(id: string): void})._duplicateSection('a');
|
|
795
|
-
await el.updateComplete;
|
|
796
|
-
|
|
797
|
-
const stack = el.state.sections[0].cells?.[0] ?? [];
|
|
798
|
-
expect(stack, 'copy lands right after the original in the same stack').to.have.lengthOf(2);
|
|
799
|
-
expect(stack[0].id).to.equal('a');
|
|
800
|
-
expect(stack[1].id, 'fresh id').to.not.equal('a');
|
|
801
|
-
expect(stack[1].type).to.equal('hero');
|
|
802
|
-
});
|
|
803
|
-
|
|
804
|
-
it('renders one column per width with the weights as fr units', async () => {
|
|
491
|
+
it('should move a top-level section into an empty slot but not an occupied one', async () => {
|
|
805
492
|
const el = await fixture<ZnPageBuilder>(html`
|
|
806
|
-
<zn-page-builder config
|
|
807
|
-
<template type="
|
|
808
|
-
<template type="
|
|
493
|
+
<zn-page-builder config='{"sections":[{"id":"grid","type":"article-grid","data":{}},{"id":"t1","type":"article-tile","data":{}},{"id":"t2","type":"article-tile","data":{}}]}'>
|
|
494
|
+
<template type="article-grid" slot="config" label="Article Grid" slots="2"></template>
|
|
495
|
+
<template type="article-tile" slot="config" label="Article"></template>
|
|
809
496
|
</zn-page-builder>`);
|
|
810
497
|
await el.updateComplete;
|
|
811
498
|
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
499
|
+
const dropOnSlot = (sectionId: string, slotSelector: string) => {
|
|
500
|
+
const dataTransfer = new DataTransfer();
|
|
501
|
+
dataTransfer.setData('application/x-zn-page-section', sectionId);
|
|
502
|
+
el.shadowRoot!.querySelector(slotSelector)!
|
|
503
|
+
.dispatchEvent(new DragEvent('drop', {dataTransfer, bubbles: true, cancelable: true}));
|
|
504
|
+
};
|
|
817
505
|
|
|
818
|
-
|
|
819
|
-
const el = await fixture<ZnPageBuilder>(html`
|
|
820
|
-
<zn-page-builder config=${containerConfig()}>
|
|
821
|
-
<template type="row" slot="config" label="Row" container></template>
|
|
822
|
-
<template type="hero" slot="config" label="Hero"></template>
|
|
823
|
-
</zn-page-builder>`);
|
|
506
|
+
dropOnSlot('t1', '.slot--empty'); // into first empty slot
|
|
824
507
|
await el.updateComplete;
|
|
508
|
+
expect(el.state.sections.map(s => s.id)).to.deep.equal(['grid', 't2']);
|
|
509
|
+
expect(el.state.sections[0].children?.[0]?.id).to.equal('t1');
|
|
825
510
|
|
|
826
|
-
|
|
827
|
-
dragTo(cell, PAGE_TYPE_MIME, 'hero', 'drop');
|
|
511
|
+
dropOnSlot('t2', '.slots zn-page-section-card'); // onto the occupied slot — rejected
|
|
828
512
|
await el.updateComplete;
|
|
829
|
-
|
|
830
|
-
expect(el.state.sections[0].
|
|
513
|
+
expect(el.state.sections.map(s => s.id)).to.deep.equal(['grid', 't2']);
|
|
514
|
+
expect(el.state.sections[0].children?.[0]?.id).to.equal('t1');
|
|
831
515
|
});
|
|
832
516
|
|
|
833
|
-
it('
|
|
517
|
+
it('should swap children across different containers', async () => {
|
|
834
518
|
const el = await fixture<ZnPageBuilder>(html`
|
|
835
|
-
<zn-page-builder
|
|
836
|
-
|
|
837
|
-
<template type="
|
|
519
|
+
<zn-page-builder
|
|
520
|
+
config='{"sections":[{"id":"g1","type":"article-grid","data":{},"children":[{"id":"a1","type":"article-tile","data":{}}]},{"id":"g2","type":"article-grid","data":{},"children":[{"id":"b1","type":"article-tile","data":{}}]}]}'>
|
|
521
|
+
<template type="article-grid" slot="config" label="Article Grid" slots="2"></template>
|
|
522
|
+
<template type="article-tile" slot="config" label="Article"></template>
|
|
838
523
|
</zn-page-builder>`);
|
|
839
524
|
await el.updateComplete;
|
|
840
525
|
|
|
841
|
-
|
|
842
|
-
|
|
526
|
+
// Drag a1 (in g1) onto b1's card (slot 0 of g2) — they trade containers.
|
|
527
|
+
const dataTransfer = new DataTransfer();
|
|
528
|
+
dataTransfer.setData('application/x-zn-page-section', 'a1');
|
|
529
|
+
const g2Card = el.shadowRoot!.querySelectorAll('.slots zn-page-section-card')[1];
|
|
530
|
+
g2Card.dispatchEvent(new DragEvent('drop', {dataTransfer, bubbles: true, cancelable: true}));
|
|
843
531
|
await el.updateComplete;
|
|
844
532
|
|
|
845
|
-
expect(el.state.sections[0].
|
|
533
|
+
expect(el.state.sections[0].children?.[0]?.id).to.equal('b1');
|
|
534
|
+
expect(el.state.sections[1].children?.[0]?.id).to.equal('a1');
|
|
846
535
|
});
|
|
847
536
|
|
|
848
|
-
it('
|
|
849
|
-
const nestedConfig = JSON.stringify({
|
|
850
|
-
sections: [{
|
|
851
|
-
id: 'outer', type: 'row', data: {}, layout: {widths: [1, 1], grow: false},
|
|
852
|
-
cells: [[{id: 'inner', type: 'row', data: {}, layout: {widths: [1, 1], grow: false}, cells: [[], []]}], []],
|
|
853
|
-
}],
|
|
854
|
-
});
|
|
537
|
+
it('should undo a slot mutation and duplicate a child into the next empty slot', async () => {
|
|
855
538
|
const el = await fixture<ZnPageBuilder>(html`
|
|
856
|
-
<zn-page-builder config
|
|
857
|
-
<template type="
|
|
858
|
-
<template type="
|
|
539
|
+
<zn-page-builder config='{"sections":[{"id":"grid","type":"article-grid","data":{}}]}'>
|
|
540
|
+
<template type="article-grid" slot="config" label="Article Grid" slots="3"></template>
|
|
541
|
+
<template type="article-tile" slot="config" label="Article"></template>
|
|
859
542
|
</zn-page-builder>`);
|
|
860
543
|
await el.updateComplete;
|
|
861
544
|
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
expect(
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
// the outer cell — a `.cell` inside a `.cell` is deterministically the inner one.
|
|
868
|
-
const innerCell = el.shadowRoot?.querySelectorAll('.cell .cell')[0];
|
|
869
|
-
expect(innerCell, 'nested cell found').to.exist;
|
|
870
|
-
const refused = dragTo(innerCell!, PAGE_TYPE_MIME, 'row', 'dragover');
|
|
871
|
-
expect(refused.defaultPrevented, 'level 3 refused').to.be.false;
|
|
872
|
-
});
|
|
545
|
+
const child = el.addSectionToSlot('article-tile', 'grid', 0)!;
|
|
546
|
+
el.undo();
|
|
547
|
+
expect(el.state.sections[0].children?.[0] ?? null, 'undo empties the slot').to.be.null;
|
|
548
|
+
el.redo();
|
|
549
|
+
expect(el.state.sections[0].children?.[0]?.id).to.equal(child.id);
|
|
873
550
|
|
|
874
|
-
|
|
875
|
-
const nestedConfig = JSON.stringify({
|
|
876
|
-
sections: [{
|
|
877
|
-
id: 'outer', type: 'row', data: {}, layout: {widths: [1, 1], grow: false},
|
|
878
|
-
cells: [[{id: 'inner', type: 'row', data: {}, layout: {widths: [1, 1], grow: false}, cells: [[], []]}], []],
|
|
879
|
-
}],
|
|
880
|
-
});
|
|
881
|
-
const el = await fixture<ZnPageBuilder>(html`
|
|
882
|
-
<zn-page-builder config=${nestedConfig}>
|
|
883
|
-
<template type="row" slot="config" label="Row" container accepts="row"></template>
|
|
884
|
-
</zn-page-builder>`);
|
|
551
|
+
// Duplicate the child: the copy takes the next empty slot with a fresh id.
|
|
885
552
|
await el.updateComplete;
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
// `accepts` list must not let a level-3 drop through.
|
|
889
|
-
const innerCell = el.shadowRoot?.querySelectorAll('.cell .cell')[0];
|
|
890
|
-
expect(innerCell, 'nested cell found').to.exist;
|
|
891
|
-
dragTo(innerCell!, PAGE_TYPE_MIME, 'row', 'drop');
|
|
553
|
+
el.shadowRoot?.querySelector('.slots zn-page-section-card')
|
|
554
|
+
?.dispatchEvent(new CustomEvent('page-card-duplicate'));
|
|
892
555
|
await el.updateComplete;
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
expect(
|
|
556
|
+
const children = el.state.sections[0].children ?? [];
|
|
557
|
+
expect(children[1]?.type).to.equal('article-tile');
|
|
558
|
+
expect(children[1]?.id).to.not.equal(children[0]?.id);
|
|
896
559
|
});
|
|
897
560
|
|
|
898
|
-
|
|
899
|
-
// has height 1 — a moved section can already carry nested containers of its own,
|
|
900
|
-
// which is exactly what this exercises via PAGE_SECTION_MIME.
|
|
901
|
-
it('refuses moving an already-placed container whose own nesting would push past the cap', async () => {
|
|
902
|
-
const config = JSON.stringify({
|
|
903
|
-
sections: [
|
|
904
|
-
{
|
|
905
|
-
id: 'A', type: 'row', data: {}, layout: {widths: [1, 1], grow: false},
|
|
906
|
-
cells: [[{
|
|
907
|
-
id: 'B', type: 'row', data: {}, layout: {widths: [1, 1], grow: false},
|
|
908
|
-
cells: [[{id: 'article', type: 'hero', data: {}}], []],
|
|
909
|
-
}], []],
|
|
910
|
-
},
|
|
911
|
-
{id: 'C', type: 'row', data: {}, layout: {widths: [1, 1], grow: false}, cells: [[], []]},
|
|
912
|
-
],
|
|
913
|
-
});
|
|
561
|
+
it('should select a card with Enter and delete a child with Delete via keyboard', async () => {
|
|
914
562
|
const el = await fixture<ZnPageBuilder>(html`
|
|
915
|
-
<zn-page-builder config
|
|
916
|
-
<template type="
|
|
917
|
-
<template type="
|
|
563
|
+
<zn-page-builder config='{"sections":[{"id":"grid","type":"article-grid","data":{},"children":[{"id":"c1","type":"article-tile","data":{}}]}]}'>
|
|
564
|
+
<template type="article-grid" slot="config" label="Article Grid" slots="2"></template>
|
|
565
|
+
<template type="article-tile" slot="config" label="Article"></template>
|
|
918
566
|
</zn-page-builder>`);
|
|
919
567
|
await el.updateComplete;
|
|
920
568
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
const cCell0 = el.shadowRoot!.querySelectorAll('.cell')[4];
|
|
924
|
-
dragTo(cCell0, PAGE_SECTION_MIME, 'A', 'drop');
|
|
925
|
-
await el.updateComplete;
|
|
926
|
-
|
|
927
|
-
expect(el.state.sections.map(s => s.id), 'A stays top-level, C stays untouched').to.deep.equal(['A', 'C']);
|
|
928
|
-
expect(el.state.sections[1].cells).to.deep.equal([[], []]);
|
|
929
|
-
});
|
|
930
|
-
|
|
931
|
-
it('still moves an already-placed flat container into another container cell', async () => {
|
|
932
|
-
const config = JSON.stringify({
|
|
933
|
-
sections: [
|
|
934
|
-
{id: 'flat', type: 'row', data: {}, layout: {widths: [1], grow: false}, cells: [[]]},
|
|
935
|
-
{id: 'target', type: 'row', data: {}, layout: {widths: [1, 1], grow: false}, cells: [[], []]},
|
|
936
|
-
],
|
|
937
|
-
});
|
|
938
|
-
const el = await fixture<ZnPageBuilder>(html`
|
|
939
|
-
<zn-page-builder config=${config}>
|
|
940
|
-
<template type="row" slot="config" label="Row" container></template>
|
|
941
|
-
</zn-page-builder>`);
|
|
569
|
+
const childCard = el.shadowRoot!.querySelector('.slots zn-page-section-card')!;
|
|
570
|
+
childCard.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true, cancelable: true}));
|
|
942
571
|
await el.updateComplete;
|
|
572
|
+
expect(el.shadowRoot?.querySelector('[part="inspector"]'), 'Enter selects').to.exist;
|
|
943
573
|
|
|
944
|
-
|
|
945
|
-
const targetCell0 = el.shadowRoot!.querySelectorAll('.cell')[1];
|
|
946
|
-
dragTo(targetCell0, PAGE_SECTION_MIME, 'flat', 'drop');
|
|
574
|
+
childCard.dispatchEvent(new KeyboardEvent('keydown', {key: 'Delete', bubbles: true, cancelable: true}));
|
|
947
575
|
await el.updateComplete;
|
|
948
|
-
|
|
949
|
-
expect(el.state.sections.map(s => s.id), 'flat is no longer top-level').to.deep.equal(['target']);
|
|
950
|
-
expect(el.state.sections[0].cells?.[0].map(c => c.id)).to.deep.equal(['flat']);
|
|
576
|
+
expect(el.state.sections[0].children?.[0] ?? null, 'Delete removes the child').to.be.null;
|
|
951
577
|
});
|
|
952
578
|
|
|
953
|
-
it('
|
|
954
|
-
const config = JSON.stringify({
|
|
955
|
-
sections: [{id: 'g1', type: 'grid', data: {}, cells: [[], [], []]}],
|
|
956
|
-
});
|
|
579
|
+
it('should pin a required-first section, inserting one when the page has none', async () => {
|
|
957
580
|
const el = await fixture<ZnPageBuilder>(html`
|
|
958
|
-
<zn-page-builder
|
|
959
|
-
<template type="grid" slot="config" label="Grid" slots="3"></template>
|
|
960
|
-
<template type="row" slot="config" label="Row" container></template>
|
|
581
|
+
<zn-page-builder required-first="hero">
|
|
961
582
|
<template type="hero" slot="config" label="Hero"></template>
|
|
583
|
+
<template type="rich-text" slot="config" label="Rich Text"></template>
|
|
962
584
|
</zn-page-builder>`);
|
|
963
585
|
await el.updateComplete;
|
|
964
586
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
expect(el.state.sections[0].cells?.[0], 'container refused by legacy slots alias').to.deep.equal([]);
|
|
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);
|
|
969
590
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
expect(
|
|
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');
|
|
973
596
|
});
|
|
974
597
|
|
|
975
|
-
it('
|
|
598
|
+
it('should hoist an existing required-first section to the top of the page', async () => {
|
|
976
599
|
const el = await fixture<ZnPageBuilder>(html`
|
|
977
|
-
<zn-page-builder
|
|
978
|
-
|
|
600
|
+
<zn-page-builder required-first="hero"
|
|
601
|
+
config='{"sections":[{"id":"t","type":"rich-text","data":{}},{"id":"h","type":"hero","data":{"title":"Help"}}]}'>
|
|
979
602
|
<template type="hero" slot="config" label="Hero"></template>
|
|
980
|
-
<template type="
|
|
603
|
+
<template type="rich-text" slot="config" label="Rich Text"></template>
|
|
981
604
|
</zn-page-builder>`);
|
|
982
605
|
await el.updateComplete;
|
|
983
606
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
await el.updateComplete;
|
|
987
|
-
expect(el.state.sections[0].cells?.[1], 'hero refused').to.deep.equal([]);
|
|
988
|
-
|
|
989
|
-
dragTo(cell, PAGE_TYPE_MIME, 'tile', 'drop');
|
|
990
|
-
await el.updateComplete;
|
|
991
|
-
expect(el.state.sections[0].cells?.[1].map(c => c.type)).to.deep.equal(['tile']);
|
|
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'});
|
|
992
609
|
});
|
|
993
610
|
|
|
994
|
-
it('
|
|
995
|
-
const growConfig = JSON.stringify({
|
|
996
|
-
sections: [{
|
|
997
|
-
id: 'outer', type: 'row', data: {},
|
|
998
|
-
layout: {widths: [1, 1], grow: true},
|
|
999
|
-
cells: [[{id: 'a', type: 'hero', data: {}}], []],
|
|
1000
|
-
}],
|
|
1001
|
-
});
|
|
611
|
+
it('should refuse to remove, reorder or displace the pinned section', async () => {
|
|
1002
612
|
const el = await fixture<ZnPageBuilder>(html`
|
|
1003
|
-
<zn-page-builder
|
|
1004
|
-
|
|
613
|
+
<zn-page-builder required-first="hero"
|
|
614
|
+
config='{"sections":[{"id":"h","type":"hero","data":{}},{"id":"t","type":"rich-text","data":{}}]}'>
|
|
1005
615
|
<template type="hero" slot="config" label="Hero"></template>
|
|
616
|
+
<template type="rich-text" slot="config" label="Rich Text"></template>
|
|
1006
617
|
</zn-page-builder>`);
|
|
1007
618
|
await el.updateComplete;
|
|
1008
619
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
expect(cells!.length, 'a trailing row is rendered').to.equal(4);
|
|
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;
|
|
1012
622
|
|
|
1013
|
-
|
|
623
|
+
pinnedCard.dispatchEvent(new CustomEvent('page-card-remove', {bubbles: true, composed: true}));
|
|
624
|
+
pinnedCard.dispatchEvent(new KeyboardEvent('keydown', {key: 'Delete', bubbles: true, cancelable: true}));
|
|
1014
625
|
await el.updateComplete;
|
|
1015
|
-
expect(el.state.sections
|
|
1016
|
-
});
|
|
626
|
+
expect(el.state.sections.map(s => s.id), 'survives remove and Delete').to.deep.equal(['h', 't']);
|
|
1017
627
|
|
|
1018
|
-
|
|
1019
|
-
const
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
</zn-page-builder>`);
|
|
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}));
|
|
1024
633
|
await el.updateComplete;
|
|
1025
|
-
|
|
1026
|
-
expect(el.shadowRoot?.querySelector('.layout-group'), 'container shows layout').to.exist;
|
|
634
|
+
expect(el.state.sections.map(s => s.id), 'nothing lands above the pinned section').to.deep.equal(['h', 't']);
|
|
1027
635
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
<template type="hero" slot="config" label="Hero"></template>
|
|
1031
|
-
</zn-page-builder>`);
|
|
1032
|
-
await plain.updateComplete;
|
|
1033
|
-
await selectFirst(plain);
|
|
1034
|
-
expect(plain.shadowRoot?.querySelector('.layout-group'), 'non-container has none').to.not.exist;
|
|
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');
|
|
1035
638
|
});
|
|
1036
639
|
|
|
1037
|
-
it('
|
|
1038
|
-
// Trailing empty cells on the pre-edit row are load-bearing for this test: with
|
|
1039
|
-
// grow false, the downstream normaliseCells pass does not trim, so only
|
|
1040
|
-
// recolumnCells's own trim-then-pad determines the final length. A passthrough
|
|
1041
|
-
// that skips recolumnCells would carry all 5 stacks through and land on 6 cells
|
|
1042
|
-
// after padding to 2 columns, not 4.
|
|
1043
|
-
const config = JSON.stringify({
|
|
1044
|
-
sections: [{
|
|
1045
|
-
id: 'outer', type: 'row', data: {}, layout: {widths: [1, 1, 1, 1, 1], grow: false},
|
|
1046
|
-
cells: [
|
|
1047
|
-
[{id: 'a', type: 'hero', data: {}}],
|
|
1048
|
-
[{id: 'b', type: 'hero', data: {}}],
|
|
1049
|
-
[{id: 'c', type: 'hero', data: {}}],
|
|
1050
|
-
[],
|
|
1051
|
-
[],
|
|
1052
|
-
],
|
|
1053
|
-
}],
|
|
1054
|
-
});
|
|
640
|
+
it('should keep the pinned section out of container slots', async () => {
|
|
1055
641
|
const el = await fixture<ZnPageBuilder>(html`
|
|
1056
|
-
<zn-page-builder
|
|
1057
|
-
|
|
642
|
+
<zn-page-builder required-first="hero"
|
|
643
|
+
config='{"sections":[{"id":"h","type":"hero","data":{}},{"id":"grid","type":"article-grid","data":{}}]}'>
|
|
1058
644
|
<template type="hero" slot="config" label="Hero"></template>
|
|
645
|
+
<template type="article-grid" slot="config" label="Article Grid" slots="2"></template>
|
|
1059
646
|
</zn-page-builder>`);
|
|
1060
647
|
await el.updateComplete;
|
|
1061
648
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
expect(row.layout?.widths).to.have.lengthOf(2);
|
|
1067
|
-
expect(row.cells?.flat().map(c => c.id), 'nothing lost or reordered').to.deep.equal(['a', 'b', 'c']);
|
|
1068
|
-
expect(row.cells, 'trailing empties trimmed before re-chunking, not carried through').to.have.lengthOf(4);
|
|
1069
|
-
});
|
|
1070
|
-
|
|
1071
|
-
it('sets a per-column width', async () => {
|
|
1072
|
-
const el = await fixture<ZnPageBuilder>(html`
|
|
1073
|
-
<zn-page-builder config=${containerConfig()}>
|
|
1074
|
-
<template type="row" slot="config" label="Row" container></template>
|
|
1075
|
-
<template type="hero" slot="config" label="Hero"></template>
|
|
1076
|
-
</zn-page-builder>`);
|
|
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}));
|
|
1077
653
|
await el.updateComplete;
|
|
1078
654
|
|
|
1079
|
-
(el
|
|
1080
|
-
|
|
1081
|
-
expect(el.state.sections[0].layout?.widths).to.deep.equal([1, 3, 1]);
|
|
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;
|
|
1082
657
|
});
|
|
1083
658
|
|
|
1084
|
-
it('
|
|
1085
|
-
const config = JSON.stringify({
|
|
1086
|
-
sections: [{
|
|
1087
|
-
id: 'outer', type: 'row', data: {}, layout: {widths: [2], grow: false},
|
|
1088
|
-
cells: [[{id: 'a', type: 'hero', data: {}}], [{id: 'b', type: 'hero', data: {}}], []],
|
|
1089
|
-
}],
|
|
1090
|
-
});
|
|
659
|
+
it('should leave the page alone without required-first', async () => {
|
|
1091
660
|
const el = await fixture<ZnPageBuilder>(html`
|
|
1092
|
-
<zn-page-builder config
|
|
1093
|
-
<template type="
|
|
1094
|
-
<template type="hero" slot="config" label="Hero"></template>
|
|
661
|
+
<zn-page-builder config='{"sections":[{"id":"t","type":"rich-text","data":{}}]}'>
|
|
662
|
+
<template type="rich-text" slot="config" label="Rich Text"></template>
|
|
1095
663
|
</zn-page-builder>`);
|
|
1096
664
|
await el.updateComplete;
|
|
1097
665
|
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
expect(
|
|
1102
|
-
|
|
1103
|
-
api._setRows('outer', 1);
|
|
1104
|
-
await el.updateComplete;
|
|
1105
|
-
expect(el.state.sections[0].cells?.flat().map(c => c.id), 'content survives').to.deep.equal(['a', 'b']);
|
|
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');
|
|
1106
670
|
});
|
|
1107
671
|
|
|
1108
|
-
it('
|
|
672
|
+
it('should accept a palette drop anywhere on the empty canvas', async () => {
|
|
1109
673
|
const el = await fixture<ZnPageBuilder>(html`
|
|
1110
|
-
<zn-page-builder
|
|
1111
|
-
<template type="row" slot="config" label="Row" container></template>
|
|
674
|
+
<zn-page-builder>
|
|
1112
675
|
<template type="hero" slot="config" label="Hero"></template>
|
|
1113
676
|
</zn-page-builder>`);
|
|
1114
677
|
await el.updateComplete;
|
|
1115
678
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
el.undo();
|
|
1121
|
-
await el.updateComplete;
|
|
1122
|
-
expect(el.state.sections[0].layout?.widths).to.deep.equal([1, 2, 1]);
|
|
1123
|
-
});
|
|
679
|
+
const canvas = el.shadowRoot!.querySelector('.canvas')!;
|
|
680
|
+
const dataTransfer = new DataTransfer();
|
|
681
|
+
dataTransfer.setData('application/x-zn-page-type', 'hero');
|
|
1124
682
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
<template type="row" slot="config" label="Row" container></template>
|
|
1129
|
-
<template type="hero" slot="config" label="Hero"></template>
|
|
1130
|
-
</zn-page-builder>`);
|
|
1131
|
-
await el.updateComplete;
|
|
1132
|
-
await selectFirst(el);
|
|
683
|
+
const over = new DragEvent('dragover', {dataTransfer, bubbles: true, cancelable: true});
|
|
684
|
+
canvas.dispatchEvent(over);
|
|
685
|
+
expect(over.defaultPrevented, 'canvas accepts the drag').to.be.true;
|
|
1133
686
|
|
|
1134
|
-
|
|
1135
|
-
expect(toggle, 'grow toggle').to.exist;
|
|
1136
|
-
toggle.checked = true;
|
|
1137
|
-
// zn-toggle only ever emits zn-input (never zn-change) on interaction — this is the
|
|
1138
|
-
// exact event the layout group's listener must be bound to.
|
|
1139
|
-
toggle.dispatchEvent(new CustomEvent('zn-input', {bubbles: true}));
|
|
687
|
+
canvas.dispatchEvent(new DragEvent('drop', {dataTransfer, bubbles: true, cancelable: true}));
|
|
1140
688
|
await el.updateComplete;
|
|
1141
|
-
|
|
1142
|
-
expect(el.state.sections[0].
|
|
689
|
+
expect(el.state.sections).to.have.length(1);
|
|
690
|
+
expect(el.state.sections[0].type).to.equal('hero');
|
|
1143
691
|
});
|
|
1144
692
|
});
|