@kubex/zinc 1.1.141 → 1.1.143

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.
@@ -12,6 +12,21 @@ describe('<zn-page-builder>', () => {
12
12
  expect(el.shadowRoot?.querySelector('[part="canvas"]')).to.exist;
13
13
  });
14
14
 
15
+ it('should fill the height it is given inside a growing panel', async () => {
16
+ const parent = await fixture(html`
17
+ <div style="display: flex; flex-direction: column; height: 700px; width: 900px;">
18
+ <zn-sp grow flush>
19
+ <form>
20
+ <zn-page-builder name="config"></zn-page-builder>
21
+ </form>
22
+ </zn-sp>
23
+ </div>`);
24
+ const el = parent.querySelector<ZnPageBuilder>('zn-page-builder')!;
25
+ await el.updateComplete;
26
+
27
+ expect(el.getBoundingClientRect().height).to.equal(700);
28
+ });
29
+
15
30
  it('should build the palette from slotted config templates', async () => {
16
31
  const el = await fixture<ZnPageBuilder>(html`
17
32
  <zn-page-builder>
@@ -312,6 +327,201 @@ describe('<zn-page-builder>', () => {
312
327
  expect(unnamed?.querySelector('.inspector-head__type'), 'no duplicate type line').to.not.exist;
313
328
  });
314
329
 
330
+ it('should resize the inspector by dragging its edge handle', async () => {
331
+ const el = await fixture<ZnPageBuilder>(html`
332
+ <zn-page-builder style="width: 1200px; height: 600px" config='{"sections":[{"id":"s1","type":"hero","data":{}}]}'>
333
+ <template type="hero" slot="config" label="Hero">
334
+ <zn-input name="title" label="Title"></zn-input>
335
+ </template>
336
+ </zn-page-builder>`);
337
+ await el.updateComplete;
338
+
339
+ el.shadowRoot?.querySelector('zn-page-section-card')?.dispatchEvent(new Event('click'));
340
+ await el.updateComplete;
341
+
342
+ const builder = el.shadowRoot!.querySelector<HTMLElement>('.builder')!;
343
+ const handle = el.shadowRoot!.querySelector<HTMLElement>('.inspector-resize');
344
+ expect(handle, 'resize handle').to.exist;
345
+
346
+ const before = el.shadowRoot!.querySelector('.inspector')!.getBoundingClientRect().width;
347
+ handle!.dispatchEvent(new PointerEvent('pointerdown', {bubbles: true, clientX: 800, pointerId: 1}));
348
+ window.dispatchEvent(new PointerEvent('pointermove', {bubbles: true, clientX: 600, pointerId: 1}));
349
+ window.dispatchEvent(new PointerEvent('pointerup', {bubbles: true, pointerId: 1}));
350
+ await el.updateComplete;
351
+
352
+ expect(builder.style.getPropertyValue('--inspector-col')).to.equal(`${Math.round(before + 200)}px`);
353
+ });
354
+
355
+ it('should hold the dragged inspector above its minimum width', async () => {
356
+ const el = await fixture<ZnPageBuilder>(html`
357
+ <zn-page-builder style="width: 1200px; height: 600px" config='{"sections":[{"id":"s1","type":"hero","data":{}}]}'>
358
+ <template type="hero" slot="config" label="Hero"></template>
359
+ </zn-page-builder>`);
360
+ await el.updateComplete;
361
+
362
+ el.shadowRoot?.querySelector('zn-page-section-card')?.dispatchEvent(new Event('click'));
363
+ await el.updateComplete;
364
+
365
+ const builder = el.shadowRoot!.querySelector<HTMLElement>('.builder')!;
366
+ const handle = el.shadowRoot!.querySelector<HTMLElement>('.inspector-resize')!;
367
+ handle.dispatchEvent(new PointerEvent('pointerdown', {bubbles: true, clientX: 800, pointerId: 1}));
368
+ window.dispatchEvent(new PointerEvent('pointermove', {bubbles: true, clientX: 5000, pointerId: 1}));
369
+ window.dispatchEvent(new PointerEvent('pointerup', {bubbles: true, pointerId: 1}));
370
+ await el.updateComplete;
371
+
372
+ expect(parseInt(builder.style.getPropertyValue('--inspector-col'), 10)).to.equal(300);
373
+ });
374
+
375
+ it('should resize the inspector from the keyboard', async () => {
376
+ const el = await fixture<ZnPageBuilder>(html`
377
+ <zn-page-builder style="width: 1200px; height: 600px" config='{"sections":[{"id":"s1","type":"hero","data":{}}]}'>
378
+ <template type="hero" slot="config" label="Hero"></template>
379
+ </zn-page-builder>`);
380
+ await el.updateComplete;
381
+
382
+ el.shadowRoot?.querySelector('zn-page-section-card')?.dispatchEvent(new Event('click'));
383
+ await el.updateComplete;
384
+
385
+ const builder = el.shadowRoot!.querySelector<HTMLElement>('.builder')!;
386
+ const handle = el.shadowRoot!.querySelector<HTMLElement>('.inspector-resize')!;
387
+ expect(handle.getAttribute('role')).to.equal('separator');
388
+
389
+ handle.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft', bubbles: true}));
390
+ await el.updateComplete;
391
+ expect(parseInt(builder.style.getPropertyValue('--inspector-col'), 10)).to.equal(363);
392
+
393
+ handle.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true}));
394
+ await el.updateComplete;
395
+ expect(parseInt(builder.style.getPropertyValue('--inspector-col'), 10)).to.equal(343);
396
+ });
397
+
398
+ it('should still tuck a resized inspector fully away', async () => {
399
+ const el = await fixture<ZnPageBuilder>(html`
400
+ <zn-page-builder style="width: 1200px; height: 600px" config='{"sections":[{"id":"s1","type":"hero","data":{}}]}'>
401
+ <template type="hero" slot="config" label="Hero"></template>
402
+ </zn-page-builder>`);
403
+ await el.updateComplete;
404
+
405
+ el.shadowRoot?.querySelector('zn-page-section-card')?.dispatchEvent(new Event('click'));
406
+ await el.updateComplete;
407
+
408
+ const handle = el.shadowRoot!.querySelector<HTMLElement>('.inspector-resize')!;
409
+ handle.dispatchEvent(new PointerEvent('pointerdown', {bubbles: true, clientX: 800, pointerId: 1}));
410
+ window.dispatchEvent(new PointerEvent('pointermove', {bubbles: true, clientX: 600, pointerId: 1}));
411
+ window.dispatchEvent(new PointerEvent('pointerup', {bubbles: true, pointerId: 1}));
412
+ await el.updateComplete;
413
+
414
+ const builder = el.shadowRoot!.querySelector<HTMLElement>('.builder')!;
415
+ expect(parseInt(builder.style.getPropertyValue('--inspector-col'), 10), 'resized').to.be.greaterThan(343);
416
+
417
+ // An inline width outranks the collapsed rule's 0px, so it has to step aside.
418
+ el.inspectorCollapsed = true;
419
+ await el.updateComplete;
420
+ expect(getComputedStyle(builder).getPropertyValue('--inspector-col').trim()).to.equal('0px');
421
+ });
422
+
423
+ const richBuilder = (height: number) => fixture<ZnPageBuilder>(html`
424
+ <zn-page-builder style="width: 1200px; height: ${height}px" config='{"sections":[{"id":"s1","type":"rich","data":{}}]}'>
425
+ <template type="rich" slot="config" label="Rich Text">
426
+ <zn-translations name="content" label="Content" input-type="remarkd"></zn-translations>
427
+ </template>
428
+ </zn-page-builder>`);
429
+
430
+ it('should widen the inspector by default for a remarkd editor', async () => {
431
+ const el = await richBuilder(600);
432
+ await el.updateComplete;
433
+
434
+ el.shadowRoot?.querySelector('zn-page-section-card')?.dispatchEvent(new Event('click'));
435
+ await el.updateComplete;
436
+
437
+ const builder = el.shadowRoot!.querySelector<HTMLElement>('.builder')!;
438
+ expect(builder.classList.contains('builder--inspector-wide'), 'wide').to.be.true;
439
+ expect(getComputedStyle(builder).getPropertyValue('--inspector-col').trim()).to.equal('520px');
440
+ });
441
+
442
+ it('should leave the inspector at its usual width without a remarkd editor', async () => {
443
+ const el = await fixture<ZnPageBuilder>(html`
444
+ <zn-page-builder style="width: 1200px; height: 600px" config='{"sections":[{"id":"s1","type":"hero","data":{}}]}'>
445
+ <template type="hero" slot="config" label="Hero">
446
+ <zn-input name="title" label="Title"></zn-input>
447
+ </template>
448
+ </zn-page-builder>`);
449
+ await el.updateComplete;
450
+
451
+ el.shadowRoot?.querySelector('zn-page-section-card')?.dispatchEvent(new Event('click'));
452
+ await el.updateComplete;
453
+
454
+ const builder = el.shadowRoot!.querySelector<HTMLElement>('.builder')!;
455
+ expect(builder.classList.contains('builder--inspector-wide')).to.be.false;
456
+ expect(getComputedStyle(builder).getPropertyValue('--inspector-col').trim()).to.equal('343px');
457
+ });
458
+
459
+ it('should prefill translations stamped inside a translation group', async () => {
460
+ const state = JSON.stringify({
461
+ sections: [{
462
+ id: 's1', type: 'tile', data: {
463
+ title: JSON.stringify({en: 'Billing'}),
464
+ subtitle: JSON.stringify({en: 'How to pay'})
465
+ }
466
+ }]
467
+ });
468
+ const el = await fixture<ZnPageBuilder>(html`
469
+ <zn-page-builder .config="${state}">
470
+ <template type="tile" slot="config" label="Tile">
471
+ <zn-translation-group inline languages='{"en":"English","fr":"French"}'>
472
+ <zn-translations name="title" label="Title"></zn-translations>
473
+ <zn-translations name="subtitle" label="Subtitle"></zn-translations>
474
+ </zn-translation-group>
475
+ </template>
476
+ </zn-page-builder>`);
477
+ await el.updateComplete;
478
+
479
+ el.shadowRoot?.querySelector('zn-page-section-card')?.dispatchEvent(new Event('click'));
480
+ await el.updateComplete;
481
+
482
+ const fields = [...el.shadowRoot!.querySelectorAll<HTMLElement & {
483
+ values: Record<string, string>; languages: Record<string, string>; grouped: boolean;
484
+ updateComplete: Promise<unknown>;
485
+ }>('.inspector__form zn-translations')];
486
+ expect(fields.length, 'both fields stamped').to.equal(2);
487
+ await Promise.all(fields.map(f => f.updateComplete));
488
+
489
+ expect(fields[0].values.en).to.equal('Billing');
490
+ expect(fields[1].values.en).to.equal('How to pay');
491
+ // The group owns the select and the language list, so the children drop theirs.
492
+ fields.forEach(field => {
493
+ expect(field.grouped, 'grouped').to.be.true;
494
+ expect(field.languages).to.have.property('fr');
495
+ expect(field.shadowRoot!.querySelector('[part="language-select"]'), 'child select').to.not.exist;
496
+ });
497
+ expect(el.shadowRoot!.querySelectorAll('.inspector__form [part="language-select"]').length).to.equal(0);
498
+ });
499
+
500
+ it('should not strand the group language select above its fields', async () => {
501
+ const el = await fixture<ZnPageBuilder>(html`
502
+ <zn-page-builder style="width: 1200px; height: 600px" config='{"sections":[{"id":"s1","type":"tile","data":{}}]}'>
503
+ <template type="tile" slot="config" label="Tile">
504
+ <zn-translation-group inline languages='{"en":"English","fr":"French"}'>
505
+ <zn-translations name="title" label="Title" flush></zn-translations>
506
+ <zn-translations name="subtitle" label="Subtitle" flush></zn-translations>
507
+ </zn-translation-group>
508
+ </template>
509
+ </zn-page-builder>`);
510
+ await el.updateComplete;
511
+
512
+ el.shadowRoot?.querySelector('zn-page-section-card')?.dispatchEvent(new Event('click'));
513
+ await el.updateComplete;
514
+
515
+ const group = el.shadowRoot!.querySelector<HTMLElement & { updateComplete: Promise<unknown> }>(
516
+ '.inspector__form zn-translation-group')!;
517
+ await group.updateComplete;
518
+ const select = group.shadowRoot!.querySelector<HTMLElement>('[part="language-field"]')!;
519
+ const first = group.querySelector<HTMLElement>('zn-translations')!;
520
+
521
+ const gap = first.getBoundingClientRect().top - select.getBoundingClientRect().bottom;
522
+ expect(gap, 'gap under the language select').to.be.at.most(24);
523
+ });
524
+
315
525
  it('should clear the selection from the inspector close button', async () => {
316
526
  const el = await fixture<ZnPageBuilder>(html`
317
527
  <zn-page-builder config='{"sections":[{"id":"s1","type":"hero","data":{}}]}'>
@@ -1778,12 +1778,14 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
1778
1778
 
1779
1779
  /** A picker action with no endpoint configured is not offered at all. */
1780
1780
  private actionAvailable(action: EditorAction): boolean {
1781
+ if (action.opens === 'image') return !!this.attachmentUrl;
1781
1782
  if (action.opens === 'include') return !!this.includeUrl;
1782
1783
  if (action.opens === 'link') return !!this.linkUrl;
1783
1784
  return true;
1784
1785
  }
1785
1786
 
1786
1787
  private slashItemAvailable(item: SlashMenuItem): boolean {
1788
+ if (item.action === 'image') return !!this.attachmentUrl;
1787
1789
  if (item.action === 'include') return !!this.includeUrl;
1788
1790
  if (item.action === 'link') return !!this.linkUrl;
1789
1791
  return true;
@@ -331,7 +331,7 @@ Second"></zn-remarkd-editor>`);
331
331
 
332
332
  it('should group the toolbar buttons', async () => {
333
333
  const el = await fixture<ZnRemarkdEditor>(html`
334
- <zn-remarkd-editor include-url="/includes" link-url="/links"></zn-remarkd-editor>`);
334
+ <zn-remarkd-editor attachment-url="/attachments" include-url="/includes" link-url="/links"></zn-remarkd-editor>`);
335
335
  const groups = el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar .toolbar__group');
336
336
 
337
337
  expect(groups.length).to.be.greaterThan(5);
@@ -343,7 +343,7 @@ Second"></zn-remarkd-editor>`);
343
343
 
344
344
  it('should move toolbar groups into an overflow menu when narrow', async () => {
345
345
  const el = await fixture<ZnRemarkdEditor>(html`
346
- <zn-remarkd-editor style="width: 240px"></zn-remarkd-editor>`);
346
+ <zn-remarkd-editor attachment-url="/attachments" style="width: 240px"></zn-remarkd-editor>`);
347
347
  await el.updateComplete;
348
348
  await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__toolbar-more'),
349
349
  'the overflow trigger never appeared');
@@ -748,7 +748,7 @@ Second"></zn-remarkd-editor>`);
748
748
 
749
749
  it('should list every action in the slash menu under its group', async () => {
750
750
  const el = await fixture<ZnRemarkdEditor>(html`
751
- <zn-remarkd-editor value="Hello" include-url="/includes" link-url="/links"></zn-remarkd-editor>`);
751
+ <zn-remarkd-editor value="Hello" attachment-url="/attachments" include-url="/includes" link-url="/links"></zn-remarkd-editor>`);
752
752
  el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
753
753
  await el.updateComplete;
754
754
  typeInBlock(el, '/');
@@ -761,20 +761,32 @@ Second"></zn-remarkd-editor>`);
761
761
 
762
762
  it('should omit the Include action from the slash menu without an include-url', async () => {
763
763
  const el = await fixture<ZnRemarkdEditor>(html`
764
- <zn-remarkd-editor value="Hello"></zn-remarkd-editor>`);
764
+ <zn-remarkd-editor value="Hello" attachment-url="/attachments" link-url="/links"></zn-remarkd-editor>`);
765
765
  el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
766
766
  await el.updateComplete;
767
767
  typeInBlock(el, '/');
768
768
  await waitUntil(() => el.shadowRoot!.querySelector('zn-slash-menu[open]'), 'the slash menu never opened');
769
769
 
770
770
  const menu = el.shadowRoot!.querySelector<ZnSlashMenu>('zn-slash-menu')!;
771
- expect(menu.items.length).to.equal(EDITOR_ACTIONS.length - 2);
771
+ expect(menu.items.length).to.equal(EDITOR_ACTIONS.length - 1);
772
772
  expect(menu.items.every(item => item.action !== 'include')).to.be.true;
773
773
  });
774
774
 
775
+ it('should omit the Image action from the slash menu without an attachment-url', async () => {
776
+ const el = await fixture<ZnRemarkdEditor>(html`
777
+ <zn-remarkd-editor value="Hello" include-url="/includes" link-url="/links"></zn-remarkd-editor>`);
778
+ el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
779
+ await el.updateComplete;
780
+ typeInBlock(el, '/');
781
+ await waitUntil(() => el.shadowRoot!.querySelector('zn-slash-menu[open]'), 'the slash menu never opened');
782
+
783
+ const menu = el.shadowRoot!.querySelector<ZnSlashMenu>('zn-slash-menu')!;
784
+ expect(menu.items.every(item => item.action !== 'image')).to.be.true;
785
+ });
786
+
775
787
  it('should omit the article link action without a link-url', async () => {
776
788
  const el = await fixture<ZnRemarkdEditor>(html`
777
- <zn-remarkd-editor value="Hello" include-url="/includes"></zn-remarkd-editor>`);
789
+ <zn-remarkd-editor value="Hello" attachment-url="/attachments" include-url="/includes"></zn-remarkd-editor>`);
778
790
  el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
779
791
  await el.updateComplete;
780
792
  typeInBlock(el, '/');
@@ -787,7 +799,7 @@ Second"></zn-remarkd-editor>`);
787
799
 
788
800
  it('should offer the article link action with a link-url', async () => {
789
801
  const el = await fixture<ZnRemarkdEditor>(html`
790
- <zn-remarkd-editor value="Hello" include-url="/includes" link-url="/links"></zn-remarkd-editor>`);
802
+ <zn-remarkd-editor value="Hello" attachment-url="/attachments" include-url="/includes" link-url="/links"></zn-remarkd-editor>`);
791
803
  el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
792
804
  await el.updateComplete;
793
805
  typeInBlock(el, '/');
@@ -1179,6 +1191,22 @@ include::inc-1[Payment Terms]"></zn-remarkd-editor>`);
1179
1191
  expect(options[0].textContent).to.contain('Refund Policy');
1180
1192
  });
1181
1193
 
1194
+ it('should offer no image entry points without an attachment-url', async () => {
1195
+ const el = await fixture<ZnRemarkdEditor>(html`
1196
+ <zn-remarkd-editor value="# Title"></zn-remarkd-editor>`);
1197
+ const tooltips = Array.from(el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar zn-button'))
1198
+ .map(button => button.getAttribute('tooltip'));
1199
+ expect(tooltips).to.not.contain('Image');
1200
+ });
1201
+
1202
+ it('should offer the Image action with an attachment-url', async () => {
1203
+ const el = await fixture<ZnRemarkdEditor>(html`
1204
+ <zn-remarkd-editor value="# Title" attachment-url="/attachments"></zn-remarkd-editor>`);
1205
+ const tooltips = Array.from(el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar zn-button'))
1206
+ .map(button => button.getAttribute('tooltip'));
1207
+ expect(tooltips).to.contain('Image');
1208
+ });
1209
+
1182
1210
  it('should offer no include entry points without an include-url', async () => {
1183
1211
  const el = await fixture<ZnRemarkdEditor>(html`
1184
1212
  <zn-remarkd-editor value="# Title"></zn-remarkd-editor>`);
@@ -15,6 +15,12 @@
15
15
  height: 100%;
16
16
  }
17
17
 
18
+ // A form wrapping the whole panel is there to submit it, not to lay it out —
19
+ // leaving it in the box tree floors the panel at the form's content height.
20
+ :host([grow]) ::slotted(form:only-child) {
21
+ display: contents;
22
+ }
23
+
18
24
  :host([max-full]) {
19
25
  max-height: 100%;
20
26
  overflow: hidden;
@@ -8,6 +8,37 @@ describe('<zn-sp>', () => {
8
8
  expect(el).to.exist;
9
9
  });
10
10
 
11
+ it('should lay out through a form that wraps the whole panel', async () => {
12
+ const parent = await fixture(html`
13
+ <div style="display: flex; flex-direction: column; height: 400px; width: 600px;">
14
+ <zn-sp grow flush>
15
+ <form>
16
+ <div id="panel" style="height: 100%;">Content</div>
17
+ </form>
18
+ </zn-sp>
19
+ </div>
20
+ `);
21
+
22
+ const form = parent.querySelector('form')!;
23
+ expect(getComputedStyle(form).display).to.equal('contents');
24
+ expect(parent.querySelector<HTMLElement>('#panel')!.getBoundingClientRect().height).to.equal(400);
25
+ });
26
+
27
+ it('should leave a form that shares the panel with siblings alone', async () => {
28
+ const parent = await fixture(html`
29
+ <div style="display: flex; flex-direction: column; height: 400px;">
30
+ <zn-sp grow>
31
+ <h2>Heading</h2>
32
+ <form>
33
+ <div>Content</div>
34
+ </form>
35
+ </zn-sp>
36
+ </div>
37
+ `);
38
+
39
+ expect(getComputedStyle(parent.querySelector('form')!).display).to.equal('block');
40
+ });
41
+
11
42
  it('should fill its parent container height', async () => {
12
43
  const parent = await fixture(html`
13
44
  <div style="height: 300px;">
@@ -13,6 +13,7 @@ import ZnChip from '../chip';
13
13
  import ZnInlineEdit from '../inline-edit';
14
14
  import ZnInput from '../input';
15
15
  import ZnOption from '../option';
16
+ import ZnRemarkdEditor from '../remarkd-editor';
16
17
  import ZnSelect from '../select';
17
18
  import ZnTextarea from '../textarea';
18
19
  import type {PropertyValues} from 'lit';
@@ -64,6 +65,7 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
64
65
  'zn-inline-edit': ZnInlineEdit,
65
66
  'zn-input': ZnInput,
66
67
  'zn-option': ZnOption,
68
+ 'zn-remarkd-editor': ZnRemarkdEditor,
67
69
  'zn-select': ZnSelect,
68
70
  'zn-textarea': ZnTextarea
69
71
  };
@@ -96,11 +98,23 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
96
98
  @property({type: Boolean, reflect: true}) flush = false;
97
99
 
98
100
  /** The control each translation is edited through. */
99
- @property({attribute: "input-type"}) inputType: 'text' | 'number' | 'textarea' = 'text';
101
+ @property({attribute: "input-type"}) inputType: 'text' | 'number' | 'textarea' | 'remarkd' = 'text';
100
102
 
101
103
  /** Rows of the textarea, when `input-type` is `textarea`. */
102
104
  @property({attribute: "textarea-rows", type: Number}) textareaRows: number | undefined;
103
105
 
106
+ /** Allows raw HTML blocks, when `input-type` is `remarkd`. */
107
+ @property({type: Boolean, attribute: 'allow-raw'}) allowRaw = false;
108
+
109
+ /** Where the remarkd editor uploads images, when `input-type` is `remarkd`. */
110
+ @property({attribute: 'attachment-url'}) attachmentUrl = '';
111
+
112
+ /** Where the remarkd editor lists embeddable includes, when `input-type` is `remarkd`. */
113
+ @property({attribute: 'include-url'}) includeUrl = '';
114
+
115
+ /** Where the remarkd editor searches link targets, when `input-type` is `remarkd`. */
116
+ @property({attribute: 'link-url'}) linkUrl = '';
117
+
104
118
  /**
105
119
  * Edits the translation through a `zn-inline-edit` — the value reads as text until it is clicked — rather than a
106
120
  * plain input or textarea.
@@ -325,7 +339,7 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
325
339
  };
326
340
 
327
341
  private handleValueUpdate = (e: CustomEvent) => {
328
- const target = e.target as (ZnInput | ZnInlineEdit | ZnTextarea);
342
+ const target = e.target as (ZnInput | ZnInlineEdit | ZnRemarkdEditor | ZnTextarea);
329
343
  if (this._activeLanguage) {
330
344
  const newValue: string = target.value as string;
331
345
  if (newValue !== this.values[this._activeLanguage]) {
@@ -387,6 +401,23 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
387
401
  ></zn-inline-edit>`;
388
402
  }
389
403
 
404
+ if (this.inputType === 'remarkd') {
405
+ return html`
406
+ <zn-remarkd-editor
407
+ .value=${live(value)}
408
+ name="${this.name}"
409
+ placeholder="${placeholder}"
410
+ dir="${dir}"
411
+ ?disabled="${this.disabled}"
412
+ ?allow-raw="${this.allowRaw}"
413
+ attachment-url="${this.attachmentUrl}"
414
+ include-url="${this.includeUrl}"
415
+ link-url="${this.linkUrl}"
416
+ @zn-change="${this.handleValueUpdate}"
417
+ @zn-input="${this.handleValueUpdate}"
418
+ ></zn-remarkd-editor>`;
419
+ }
420
+
390
421
  if (this.inputType === 'textarea') {
391
422
  return html`
392
423
  <zn-textarea
@@ -219,6 +219,47 @@ describe('<zn-translations>', () => {
219
219
  expect(el.shadowRoot!.querySelector('zn-input')).to.be.null;
220
220
  });
221
221
 
222
+ it('renders a remarkd editor for input-type="remarkd"', async () => {
223
+ const el = await fixture<ZnTranslations>(html`
224
+ <zn-translations input-type="remarkd" values='{"en":"= Hello"}'></zn-translations>`);
225
+ await el.updateComplete;
226
+
227
+ const editor = el.shadowRoot!.querySelector<HTMLElement & { value: string }>('zn-remarkd-editor');
228
+ expect(editor).to.exist;
229
+ expect(editor!.value).to.equal('= Hello');
230
+ expect(el.shadowRoot!.querySelector('zn-input')).to.be.null;
231
+ expect(el.shadowRoot!.querySelector('zn-textarea')).to.be.null;
232
+ });
233
+
234
+ it('passes its editor endpoints through to the remarkd editor', async () => {
235
+ const el = await fixture<ZnTranslations>(html`
236
+ <zn-translations input-type="remarkd"
237
+ allow-raw
238
+ attachment-url="/a"
239
+ include-url="/i"
240
+ link-url="/l"></zn-translations>`);
241
+ await el.updateComplete;
242
+
243
+ const editor = el.shadowRoot!.querySelector('zn-remarkd-editor')!;
244
+ expect(editor.getAttribute('attachment-url')).to.equal('/a');
245
+ expect(editor.getAttribute('include-url')).to.equal('/i');
246
+ expect(editor.getAttribute('link-url')).to.equal('/l');
247
+ expect(editor.hasAttribute('allow-raw')).to.be.true;
248
+ });
249
+
250
+ it('writes a remarkd edit back to the active language', async () => {
251
+ const el = await fixture<ZnTranslations>(html`
252
+ <zn-translations input-type="remarkd" values='{"en":"one"}'></zn-translations>`);
253
+ await el.updateComplete;
254
+
255
+ const editor = el.shadowRoot!.querySelector<HTMLElement & { value: string }>('zn-remarkd-editor')!;
256
+ editor.value = 'two';
257
+ editor.dispatchEvent(new CustomEvent('zn-change', {bubbles: true, composed: true}));
258
+ await el.updateComplete;
259
+
260
+ expect(el.values.en).to.equal('two');
261
+ });
262
+
222
263
  it('renders an inline edit when asked for one', async () => {
223
264
  const el = await fixture<ZnTranslations>(html`
224
265
  <zn-translations inline-edit values='{"en":"Hello"}'></zn-translations>`);