@fusion.dev/helix-headless 0.219.0 → 0.221.0

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.
@@ -0,0 +1,860 @@
1
+ import { __decorate } from "tslib";
2
+ import { auditName, customElement, drawIcon, formatWords, HelixElement, MIRROR_RULE, onApple, stopPress, words, } from '@fusion.dev/helix-core';
3
+ import { arrowClockwise, arrowCounterClockwise, caretDown, check, code, link, linkBreak, listBullets, listNumbers, magnifyingGlass, quotes, textB, textItalic, textStrikethrough, x, } from '@fusion.dev/helix-icons';
4
+ import { nothing } from 'lit';
5
+ import { property, state } from 'lit/decorators.js';
6
+ import { live } from 'lit/directives/live.js';
7
+ import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
8
+ import { html } from 'lit/static-html.js';
9
+ import '../../button/index.js';
10
+ import '../../divider/index.js';
11
+ import '../../find/index.js';
12
+ import '../../icon-button/index.js';
13
+ import '../../menu/index.js';
14
+ import '../../toolbar/index.js';
15
+ export const EDITOR_BLOCKS = [
16
+ 'paragraph',
17
+ 'heading-1',
18
+ 'heading-2',
19
+ 'heading-3',
20
+ 'code-block',
21
+ ];
22
+ const GROUPS = [
23
+ ['history', ['undo', 'redo']],
24
+ ['block', EDITOR_BLOCKS],
25
+ ['marks', ['bold', 'italic', 'strike', 'code']],
26
+ ['link', ['link']],
27
+ ['lists', ['bullet-list', 'ordered-list']],
28
+ ['quote', ['quote']],
29
+ ];
30
+ const RESTING_TOOLS = 'history block marks link lists quote';
31
+ const SHORTCUTS = {
32
+ 'code-block': 'mod+alt+8',
33
+ 'heading-1': 'mod+alt+1',
34
+ 'heading-2': 'mod+alt+2',
35
+ 'heading-3': 'mod+alt+3',
36
+ paragraph: 'mod+alt+0',
37
+ };
38
+ const TOOL_PARTS = [
39
+ 'undo',
40
+ 'redo',
41
+ 'block',
42
+ 'bold',
43
+ 'italic',
44
+ 'strike',
45
+ 'code',
46
+ 'link',
47
+ 'bullet-list',
48
+ 'ordered-list',
49
+ 'quote',
50
+ 'find-toggle',
51
+ ]
52
+ .map((name) => `[part~="${name}"]`)
53
+ .join(', ');
54
+ const NO_FORMATS = { applied: [], block: '', link: '', unavailable: [] };
55
+ const commandWith = (event, key) => event.key.toLowerCase() === key &&
56
+ !event.altKey &&
57
+ !event.shiftKey &&
58
+ (onApple() ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey);
59
+ let HelixHeadlessEditor = class HelixHeadlessEditor extends HelixElement {
60
+ constructor() {
61
+ super(...arguments);
62
+ this.blockChosenLabel = 'Text style: {name}';
63
+ this.blockLabel = 'Text style';
64
+ this.boldLabel = 'Bold';
65
+ this.bulletListLabel = 'Bulleted list';
66
+ this.codeBlockLabel = 'Code block';
67
+ this.codeLabel = 'Code';
68
+ this.controlsLabel = 'Formatting';
69
+ this.fallbackLabel = 'Editor';
70
+ this.findLabel = 'Find';
71
+ this.headingLabel = 'Heading {level}';
72
+ this.italicLabel = 'Italic';
73
+ this.label = '';
74
+ this.linkApplyLabel = 'Apply link';
75
+ this.linkCloseLabel = 'Close';
76
+ this.linkControlsLabel = 'Link controls';
77
+ this.linkFieldLabel = 'Address';
78
+ this.linkLabel = 'Link';
79
+ this.linkRemoveLabel = 'Remove link';
80
+ this.orderedListLabel = 'Numbered list';
81
+ this.paragraphLabel = 'Paragraph';
82
+ this.placeholder = '';
83
+ this.quoteLabel = 'Quote';
84
+ this.readOnly = false;
85
+ this.redoLabel = 'Redo';
86
+ this.strikeLabel = 'Strikethrough';
87
+ this.undoLabel = 'Undo';
88
+ this.formats = NO_FORMATS;
89
+ this.renderer = undefined;
90
+ this.linkOpen = false;
91
+ this.draft = '';
92
+ this.findOpen = false;
93
+ this.hasActions = false;
94
+ this.escaped = false;
95
+ this.chosen = false;
96
+ this.handleLinkInput = (event) => {
97
+ this.draft = event.target.value;
98
+ };
99
+ this.handleLinkKeys = (event) => {
100
+ if (event.isComposing) {
101
+ return;
102
+ }
103
+ if (event.key === 'Enter') {
104
+ event.preventDefault();
105
+ this.applyLink();
106
+ }
107
+ else if (event.key === 'Escape') {
108
+ event.preventDefault();
109
+ this.closeLink();
110
+ }
111
+ };
112
+ this.handleToolbarKeys = (event) => {
113
+ if (event.key === 'Escape' && !event.defaultPrevented && !event.isComposing) {
114
+ event.preventDefault();
115
+ this.returnFocus();
116
+ }
117
+ };
118
+ this.handleFindToggle = (event) => {
119
+ this.findOpen = event.newState === 'open';
120
+ };
121
+ this.handleKeys = (event) => {
122
+ if (event.isComposing || !this.fromDocument(event)) {
123
+ return;
124
+ }
125
+ if (event.key === 'Tab' && this.escaped && !event.ctrlKey && !event.metaKey && !event.altKey) {
126
+ this.escaped = false;
127
+ event.stopPropagation();
128
+ return;
129
+ }
130
+ this.escaped = event.key === 'Escape';
131
+ if (commandWith(event, 'f') && this.searches && this.find) {
132
+ event.preventDefault();
133
+ event.stopPropagation();
134
+ void this.openFind();
135
+ }
136
+ else if (commandWith(event, 'k') && this.drawsAny && this.draws('link')) {
137
+ event.preventDefault();
138
+ event.stopPropagation();
139
+ this.openLink();
140
+ }
141
+ else if (event.key === 'F10' &&
142
+ event.altKey &&
143
+ !event.ctrlKey &&
144
+ !event.metaKey &&
145
+ this.drawsAny) {
146
+ event.preventDefault();
147
+ event.stopPropagation();
148
+ this.focusToolbar();
149
+ }
150
+ };
151
+ this.handleFocusIn = (event) => {
152
+ this.toggleState('document-focused', this.fromDocument(event));
153
+ };
154
+ this.handleFocusOut = () => {
155
+ this.toggleState('document-focused', false);
156
+ };
157
+ this.handleActions = () => {
158
+ this.requestUpdate();
159
+ };
160
+ this.keepTick = (event) => {
161
+ if (event.target === event.currentTarget) {
162
+ event.preventDefault();
163
+ event.stopPropagation();
164
+ }
165
+ };
166
+ this.handleMenuToggle = (event) => {
167
+ if (event.newState === 'closed' && this.chosen) {
168
+ this.chosen = false;
169
+ this.returnFocus();
170
+ }
171
+ };
172
+ }
173
+ static { this.markRules = [MIRROR_RULE]; }
174
+ static { this.words = 'helix-editor'; }
175
+ attach(renderer) {
176
+ this.renderer = renderer;
177
+ this.formats = NO_FORMATS;
178
+ renderer.settings?.(this.settings);
179
+ this.toggleState('connected', true);
180
+ const current = () => this.renderer === renderer;
181
+ return {
182
+ detach: () => {
183
+ if (current()) {
184
+ this.renderer = undefined;
185
+ this.formats = NO_FORMATS;
186
+ this.linkOpen = false;
187
+ this.toggleState('connected', false);
188
+ }
189
+ },
190
+ formats: (formats) => {
191
+ if (current()) {
192
+ this.formats = formats;
193
+ }
194
+ },
195
+ found: (result) => {
196
+ if (current()) {
197
+ this.find?.sink.found(result);
198
+ }
199
+ },
200
+ };
201
+ }
202
+ run(tool, value) {
203
+ if (!this.renderer || this.readOnly) {
204
+ return false;
205
+ }
206
+ return this.renderer.run(tool, value);
207
+ }
208
+ focus(options) {
209
+ if (this.renderer?.focus) {
210
+ this.renderer.focus();
211
+ return;
212
+ }
213
+ super.focus(options);
214
+ }
215
+ get settings() {
216
+ return {
217
+ label: this.label || this.getAttribute('aria-label') || this.fallbackLabel,
218
+ placeholder: this.placeholder,
219
+ readOnly: this.readOnly,
220
+ };
221
+ }
222
+ get groups() {
223
+ return new Set((this.tools ?? RESTING_TOOLS).split(/\s+/).filter(Boolean));
224
+ }
225
+ draws(tool) {
226
+ const group = GROUPS.find(([, tools]) => tools.includes(tool));
227
+ if (!group || !this.groups.has(group[0])) {
228
+ return false;
229
+ }
230
+ const runs = this.renderer?.tools?.();
231
+ return runs === undefined || runs.includes(tool);
232
+ }
233
+ get drawsAny() {
234
+ return (!this.readOnly &&
235
+ (this.drawsFind || GROUPS.some(([, tools]) => tools.some((tool) => this.draws(tool)))));
236
+ }
237
+ get searches() {
238
+ return this.renderer === undefined || this.renderer.find !== undefined;
239
+ }
240
+ get drawsFind() {
241
+ return this.groups.has('find') && this.searches;
242
+ }
243
+ disabled(tool) {
244
+ return !this.renderer || this.formats.unavailable.includes(tool);
245
+ }
246
+ nameOf(tool) {
247
+ switch (tool) {
248
+ case 'bold':
249
+ return this.boldLabel;
250
+ case 'bullet-list':
251
+ return this.bulletListLabel;
252
+ case 'code':
253
+ return this.codeLabel;
254
+ case 'code-block':
255
+ return this.codeBlockLabel;
256
+ case 'heading-1':
257
+ case 'heading-2':
258
+ case 'heading-3':
259
+ return formatWords(this, 'headingLabel', { level: Number(tool.slice(-1)) });
260
+ case 'italic':
261
+ return this.italicLabel;
262
+ case 'link':
263
+ return this.linkLabel;
264
+ case 'ordered-list':
265
+ return this.orderedListLabel;
266
+ case 'paragraph':
267
+ return this.paragraphLabel;
268
+ case 'quote':
269
+ return this.quoteLabel;
270
+ case 'redo':
271
+ return this.redoLabel;
272
+ case 'strike':
273
+ return this.strikeLabel;
274
+ case 'undo':
275
+ return this.undoLabel;
276
+ }
277
+ }
278
+ returnFocus() {
279
+ this.renderer?.focus?.();
280
+ }
281
+ press(tool) {
282
+ if (this.disabled(tool)) {
283
+ return;
284
+ }
285
+ this.run(tool);
286
+ this.returnFocus();
287
+ }
288
+ openLink() {
289
+ if (this.readOnly || !this.renderer || !this.draws('link') || this.disabled('link')) {
290
+ return;
291
+ }
292
+ this.draft = this.formats.link;
293
+ this.linkOpen = true;
294
+ void this.updateComplete.then(() => {
295
+ const field = this.partNamed('link-field');
296
+ field?.focus();
297
+ field?.select();
298
+ });
299
+ }
300
+ closeLink() {
301
+ this.linkOpen = false;
302
+ this.returnFocus();
303
+ }
304
+ applyLink() {
305
+ this.run('link', this.draft.trim());
306
+ this.closeLink();
307
+ }
308
+ removeLink() {
309
+ this.run('link', '');
310
+ this.closeLink();
311
+ }
312
+ focusToolbar() {
313
+ const tools = [...this.renderRoot.querySelectorAll(TOOL_PARTS)].filter((one) => !one.hasAttribute('disabled'));
314
+ const stop = tools.find((one) => one.shadowRoot?.querySelector('[tabindex="0"]')) ?? tools[0];
315
+ stop?.focus();
316
+ }
317
+ async openFind() {
318
+ const find = this.find?.element;
319
+ if (!find) {
320
+ return;
321
+ }
322
+ this.findOpen = true;
323
+ await this.updateComplete;
324
+ await find.show();
325
+ this.findOpen = find.open;
326
+ }
327
+ attachFind() {
328
+ const element = this.partNamed('find') ?? undefined;
329
+ const renderer = this.renderer;
330
+ if (element === this.find?.element && renderer === this.find?.renderer) {
331
+ return;
332
+ }
333
+ this.find?.sink.detach();
334
+ this.find =
335
+ element && renderer
336
+ ? { element, renderer, sink: element.attach(this.finderFor(renderer)) }
337
+ : undefined;
338
+ }
339
+ finderFor(renderer) {
340
+ return {
341
+ end: () => renderer.findEnd?.(),
342
+ find: (query, direction) => renderer.find?.(query, direction),
343
+ ...(renderer.replace
344
+ ? { replace: (query, all) => renderer.replace?.(query, all) }
345
+ : {}),
346
+ };
347
+ }
348
+ fromDocument(event) {
349
+ const origin = event.composedPath()[0];
350
+ return (origin instanceof Node &&
351
+ [...this.children].some((child) => child.slot === '' && child.contains(origin)));
352
+ }
353
+ choose(tool) {
354
+ this.run(tool);
355
+ this.chosen = true;
356
+ }
357
+ connectedCallback() {
358
+ super.connectedCallback();
359
+ this.addEventListener('keydown', this.handleKeys, { capture: true });
360
+ this.addEventListener('focusin', this.handleFocusIn);
361
+ this.addEventListener('focusout', this.handleFocusOut);
362
+ }
363
+ disconnectedCallback() {
364
+ super.disconnectedCallback();
365
+ this.removeEventListener('keydown', this.handleKeys, { capture: true });
366
+ this.removeEventListener('focusin', this.handleFocusIn);
367
+ this.removeEventListener('focusout', this.handleFocusOut);
368
+ }
369
+ firstUpdated(changed) {
370
+ super.firstUpdated(changed);
371
+ this.finishHydration();
372
+ auditName(this, () => null, 'editor', () => this.label !== '');
373
+ }
374
+ updated(changed) {
375
+ super.updated(changed);
376
+ if (this.renderer &&
377
+ (changed.has('label') ||
378
+ changed.has('placeholder') ||
379
+ changed.has('readOnly') ||
380
+ changed.has('fallbackLabel'))) {
381
+ this.renderer.settings?.(this.settings);
382
+ }
383
+ if (changed.has('readOnly') && this.readOnly) {
384
+ this.linkOpen = false;
385
+ }
386
+ this.attachFind();
387
+ }
388
+ willUpdate(changed) {
389
+ super.willUpdate(changed);
390
+ this.hasActions = this.knownChildren.some((child) => child.getAttribute('slot') === 'actions');
391
+ this.toggleState('link-open', this.linkOpen);
392
+ this.toggleState('find-open', this.findOpen);
393
+ }
394
+ get blockEmphasis() {
395
+ return undefined;
396
+ }
397
+ pressed(tool) {
398
+ return this.formats.applied.includes(tool);
399
+ }
400
+ renderPointing(glyph) {
401
+ return html `<svg aria-hidden="true" data-glyph data-mirror fill="currentColor" viewBox=${glyph.viewBox}>
402
+ ${unsafeSVG(glyph.content)}
403
+ </svg>`;
404
+ }
405
+ renderHistory() {
406
+ if (!this.draws('undo') && !this.draws('redo')) {
407
+ return nothing;
408
+ }
409
+ const button = this.tag('icon-button');
410
+ return html `<div part="tools">${this.draws('undo')
411
+ ? html `<${button}
412
+ part="undo"
413
+ exportparts="button: undo-button, live: undo-live, spinner: undo-spinner, text: undo-text"
414
+ ?disabled=${this.disabled('undo')}
415
+ label=${this.undoLabel}
416
+ @click=${() => this.press('undo')}
417
+ @helix-press=${stopPress}
418
+ >
419
+ <slot name="undo-icon">${this.renderPointing(arrowCounterClockwise)}</slot>
420
+ </${button}>`
421
+ : nothing}${this.draws('redo')
422
+ ? html `<${button}
423
+ part="redo"
424
+ exportparts="button: redo-button, live: redo-live, spinner: redo-spinner, text: redo-text"
425
+ ?disabled=${this.disabled('redo')}
426
+ label=${this.redoLabel}
427
+ @click=${() => this.press('redo')}
428
+ @helix-press=${stopPress}
429
+ >
430
+ <slot name="redo-icon">${this.renderPointing(arrowClockwise)}</slot>
431
+ </${button}>`
432
+ : nothing}</div>`;
433
+ }
434
+ renderMarks() {
435
+ if (!['bold', 'italic', 'strike', 'code'].some((tool) => this.draws(tool))) {
436
+ return nothing;
437
+ }
438
+ const button = this.tag('icon-button');
439
+ return html `<div part="tools">${this.draws('bold')
440
+ ? html `<${button}
441
+ part="bold${this.pressed('bold') ? ' bold-pressed' : ''}"
442
+ exportparts="button: bold-button, live: bold-live, spinner: bold-spinner, text: bold-text"
443
+ aria-pressed=${this.pressed('bold') ? 'true' : 'false'}
444
+ ?disabled=${this.disabled('bold')}
445
+ label=${this.boldLabel}
446
+ @click=${() => this.press('bold')}
447
+ @helix-press=${stopPress}
448
+ >
449
+ <slot name="bold-icon">${drawIcon(textB)}</slot>
450
+ </${button}>`
451
+ : nothing}${this.draws('italic')
452
+ ? html `<${button}
453
+ part="italic${this.pressed('italic') ? ' italic-pressed' : ''}"
454
+ exportparts="button: italic-button, live: italic-live, spinner: italic-spinner, text: italic-text"
455
+ aria-pressed=${this.pressed('italic') ? 'true' : 'false'}
456
+ ?disabled=${this.disabled('italic')}
457
+ label=${this.italicLabel}
458
+ @click=${() => this.press('italic')}
459
+ @helix-press=${stopPress}
460
+ >
461
+ <slot name="italic-icon">${drawIcon(textItalic)}</slot>
462
+ </${button}>`
463
+ : nothing}${this.draws('strike')
464
+ ? html `<${button}
465
+ part="strike${this.pressed('strike') ? ' strike-pressed' : ''}"
466
+ exportparts="button: strike-button, live: strike-live, spinner: strike-spinner, text: strike-text"
467
+ aria-pressed=${this.pressed('strike') ? 'true' : 'false'}
468
+ ?disabled=${this.disabled('strike')}
469
+ label=${this.strikeLabel}
470
+ @click=${() => this.press('strike')}
471
+ @helix-press=${stopPress}
472
+ >
473
+ <slot name="strike-icon">${drawIcon(textStrikethrough)}</slot>
474
+ </${button}>`
475
+ : nothing}${this.draws('code')
476
+ ? html `<${button}
477
+ part="code${this.pressed('code') ? ' code-pressed' : ''}"
478
+ exportparts="button: code-button, live: code-live, spinner: code-spinner, text: code-text"
479
+ aria-pressed=${this.pressed('code') ? 'true' : 'false'}
480
+ ?disabled=${this.disabled('code')}
481
+ label=${this.codeLabel}
482
+ @click=${() => this.press('code')}
483
+ @helix-press=${stopPress}
484
+ >
485
+ <slot name="code-icon">${drawIcon(code)}</slot>
486
+ </${button}>`
487
+ : nothing}</div>`;
488
+ }
489
+ renderLinkTool() {
490
+ if (!this.draws('link')) {
491
+ return nothing;
492
+ }
493
+ const button = this.tag('icon-button');
494
+ const linked = this.formats.link !== '';
495
+ return html `<${button}
496
+ part="link${linked ? ' link-pressed' : ''}"
497
+ exportparts="button: link-button, live: link-live, spinner: link-spinner, text: link-text"
498
+ aria-pressed=${linked ? 'true' : 'false'}
499
+ .expanded=${this.linkOpen}
500
+ ?disabled=${this.disabled('link')}
501
+ label=${this.linkLabel}
502
+ @click=${() => (this.linkOpen ? this.closeLink() : this.openLink())}
503
+ @helix-press=${stopPress}
504
+ >
505
+ <slot name="link-icon">${drawIcon(link)}</slot>
506
+ </${button}>`;
507
+ }
508
+ renderLists() {
509
+ if (!this.draws('bullet-list') && !this.draws('ordered-list')) {
510
+ return nothing;
511
+ }
512
+ const button = this.tag('icon-button');
513
+ return html `${this.draws('bullet-list')
514
+ ? html `<${button}
515
+ part="bullet-list${this.pressed('bullet-list') ? ' bullet-list-pressed' : ''}"
516
+ exportparts="button: bullet-list-button, live: bullet-list-live, spinner: bullet-list-spinner, text: bullet-list-text"
517
+ aria-pressed=${this.pressed('bullet-list') ? 'true' : 'false'}
518
+ ?disabled=${this.disabled('bullet-list')}
519
+ label=${this.bulletListLabel}
520
+ @click=${() => this.press('bullet-list')}
521
+ @helix-press=${stopPress}
522
+ >
523
+ <slot name="bullet-list-icon">${drawIcon(listBullets)}</slot>
524
+ </${button}>`
525
+ : nothing}${this.draws('ordered-list')
526
+ ? html `<${button}
527
+ part="ordered-list${this.pressed('ordered-list') ? ' ordered-list-pressed' : ''}"
528
+ exportparts="button: ordered-list-button, live: ordered-list-live, spinner: ordered-list-spinner, text: ordered-list-text"
529
+ aria-pressed=${this.pressed('ordered-list') ? 'true' : 'false'}
530
+ ?disabled=${this.disabled('ordered-list')}
531
+ label=${this.orderedListLabel}
532
+ @click=${() => this.press('ordered-list')}
533
+ @helix-press=${stopPress}
534
+ >
535
+ <slot name="ordered-list-icon">${drawIcon(listNumbers)}</slot>
536
+ </${button}>`
537
+ : nothing}`;
538
+ }
539
+ renderBlocks() {
540
+ const lists = this.renderLists();
541
+ const quote = this.renderQuote();
542
+ return lists === nothing && quote === nothing
543
+ ? nothing
544
+ : html `<div part="tools">${lists}${quote}</div>`;
545
+ }
546
+ renderQuote() {
547
+ if (!this.draws('quote')) {
548
+ return nothing;
549
+ }
550
+ const button = this.tag('icon-button');
551
+ return html `<${button}
552
+ part="quote${this.pressed('quote') ? ' quote-pressed' : ''}"
553
+ exportparts="button: quote-button, live: quote-live, spinner: quote-spinner, text: quote-text"
554
+ aria-pressed=${this.pressed('quote') ? 'true' : 'false'}
555
+ ?disabled=${this.disabled('quote')}
556
+ label=${this.quoteLabel}
557
+ @click=${() => this.press('quote')}
558
+ @helix-press=${stopPress}
559
+ >
560
+ <slot name="quote-icon">${drawIcon(quotes)}</slot>
561
+ </${button}>`;
562
+ }
563
+ renderBlockMenu() {
564
+ const blocks = EDITOR_BLOCKS.filter((tool) => this.draws(tool));
565
+ if (blocks.length === 0) {
566
+ return nothing;
567
+ }
568
+ const menu = this.tag('menu');
569
+ const item = this.tag('menu-item');
570
+ const button = this.tag('button');
571
+ const current = this.formats.block;
572
+ const shown = current === '' ? 'paragraph' : current;
573
+ return html `<${menu}
574
+ part="block-menu"
575
+ exportparts="back: block-menu-back, back-mark: block-menu-back-mark, empty: block-menu-empty,
576
+ header: block-menu-header, live: block-menu-live, loading: block-menu-loading,
577
+ search-field: block-menu-search-field, surface: block-menu-surface,
578
+ title: block-menu-title"
579
+ label=${this.blockLabel}
580
+ @toggle=${this.handleMenuToggle}
581
+ >
582
+ <${button}
583
+ part="block"
584
+ exportparts="button: block-button, live: block-live, spinner: block-spinner, text: block-text"
585
+ slot="trigger"
586
+ ?disabled=${!this.renderer || blocks.every((tool) => this.disabled(tool))}
587
+ emphasis=${this.blockEmphasis ?? nothing}
588
+ label=${formatWords(this, 'blockChosenLabel', { name: this.nameOf(shown) })}
589
+ @helix-press=${stopPress}
590
+ >
591
+ <span part="block-name">
592
+ ${blocks.map((tool) => html `<span data-block=${tool} ?hidden=${tool !== shown}>${this.nameOf(tool)}</span>`)}
593
+ </span>
594
+ ${drawIcon(caretDown, { slot: 'end' })}
595
+ </${button}>
596
+ ${blocks.map((tool) => html `<${item}
597
+ part="block-option"
598
+ exportparts="end: block-option-end, item: block-option-item, mark: block-option-mark,
599
+ shortcut: block-option-shortcut, shortcut-kbd: block-option-shortcut-kbd,
600
+ shortcut-key: block-option-shortcut-key, shortcut-separator: block-option-shortcut-separator,
601
+ start: block-option-start, surface: block-option-surface"
602
+ value=${tool}
603
+ checkable
604
+ exclusive
605
+ ?checked=${tool === current}
606
+ ?disabled=${this.disabled(tool)}
607
+ shortcut=${SHORTCUTS[tool] ?? nothing}
608
+ @helix-before-toggle=${this.keepTick}
609
+ @helix-select=${() => this.choose(tool)}
610
+ >
611
+ ${this.nameOf(tool)}
612
+ </${item}>`)}
613
+ </${menu}>`;
614
+ }
615
+ renderDivider() {
616
+ const divider = this.tag('divider');
617
+ return html `<${divider}
618
+ part="divider"
619
+ exportparts="content: divider-content, line: divider-line"
620
+ orientation="vertical"
621
+ ></${divider}>`;
622
+ }
623
+ renderFindTool() {
624
+ if (!this.drawsFind) {
625
+ return nothing;
626
+ }
627
+ const button = this.tag('icon-button');
628
+ return html `<${button}
629
+ part="find-toggle"
630
+ exportparts="button: find-toggle-button, live: find-toggle-live, spinner: find-toggle-spinner, text: find-toggle-text"
631
+ .expanded=${this.findOpen}
632
+ ?disabled=${!this.renderer}
633
+ label=${this.findLabel}
634
+ @click=${() => (this.findOpen ? this.find?.element.hide() : void this.openFind())}
635
+ @helix-press=${stopPress}
636
+ >
637
+ <slot name="find-icon">${drawIcon(magnifyingGlass)}</slot>
638
+ </${button}>`;
639
+ }
640
+ renderFind() {
641
+ if (!this.searches) {
642
+ return nothing;
643
+ }
644
+ const find = this.tag('find');
645
+ return html `<${find}
646
+ part="find"
647
+ exportparts="actions: find-actions, bar: find-bar, close: find-close, close-button: find-close-button, close-live: find-close-live, close-spinner: find-close-spinner, close-text: find-close-text, controls: find-controls, controls-toolbar: find-controls-toolbar, count: find-count, field: find-field, live: find-live, match-case: find-match-case, match-case-button: find-match-case-button, match-case-live: find-match-case-live, match-case-pressed: find-match-case-pressed, match-case-spinner: find-match-case-spinner, match-case-text: find-match-case-text, next: find-next, next-button: find-next-button, next-live: find-next-live, next-spinner: find-next-spinner, next-text: find-next-text, previous: find-previous, previous-button: find-previous-button, previous-live: find-previous-live, previous-spinner: find-previous-spinner, previous-text: find-previous-text, regex: find-regex, regex-button: find-regex-button, regex-live: find-regex-live, regex-pressed: find-regex-pressed, regex-spinner: find-regex-spinner, regex-text: find-regex-text, replace: find-replace, replace-all: find-replace-all, replace-all-button: find-replace-all-button, replace-all-live: find-replace-all-live, replace-all-spinner: find-replace-all-spinner, replace-all-text: find-replace-all-text, replace-button: find-replace-button, replace-field: find-replace-field, replace-live: find-replace-live, replace-spinner: find-replace-spinner, replace-text: find-replace-text, replace-toggle: find-replace-toggle, replace-toggle-button: find-replace-toggle-button, replace-toggle-live: find-replace-toggle-live, replace-toggle-spinner: find-replace-toggle-spinner, replace-toggle-text: find-replace-toggle-text, whole-word: find-whole-word, whole-word-button: find-whole-word-button, whole-word-live: find-whole-word-live, whole-word-pressed: find-whole-word-pressed, whole-word-spinner: find-whole-word-spinner, whole-word-text: find-whole-word-text"
648
+ label=${this.findLabel}
649
+ ?readonly=${this.readOnly}
650
+ shortcut=""
651
+ @toggle=${this.handleFindToggle}
652
+ ></${find}>`;
653
+ }
654
+ renderControls() {
655
+ const toolbar = this.tag('toolbar');
656
+ const groups = this.readOnly
657
+ ? []
658
+ : [
659
+ this.renderHistory(),
660
+ this.renderBlockMenu(),
661
+ this.renderMarks(),
662
+ this.renderLinkTool(),
663
+ this.renderBlocks(),
664
+ this.renderFindTool(),
665
+ ].filter((group) => group !== nothing);
666
+ return html `<${toolbar} part="controls" exportparts="toolbar: controls-toolbar" label=${this.controlsLabel} @keydown=${this.handleToolbarKeys}>
667
+ ${groups.map((group, at) => (at === 0 ? group : html `${this.renderDivider()}${group}`))}
668
+ <slot name="actions" part="actions" ?hidden=${!this.hasActions} @slotchange=${this.handleActions}></slot>
669
+ </${toolbar}>`;
670
+ }
671
+ renderLink() {
672
+ if (this.readOnly || !this.draws('link')) {
673
+ return nothing;
674
+ }
675
+ const toolbar = this.tag('toolbar');
676
+ const button = this.tag('icon-button');
677
+ return html `<div part="link-bar" role="group" aria-label=${this.linkLabel} ?hidden=${!this.linkOpen}>
678
+ <input
679
+ part="link-field"
680
+ aria-label=${this.linkFieldLabel}
681
+ autocapitalize="off"
682
+ autocomplete="url"
683
+ dir="ltr"
684
+ enterkeyhint="done"
685
+ placeholder=${this.linkFieldLabel}
686
+ spellcheck="false"
687
+ type="url"
688
+ .value=${live(this.draft)}
689
+ @input=${this.handleLinkInput}
690
+ @keydown=${this.handleLinkKeys}
691
+ />
692
+ <${toolbar} part="link-controls" exportparts="toolbar: link-controls-toolbar" label=${this.linkControlsLabel}>
693
+ <${button}
694
+ part="link-apply"
695
+ exportparts="button: link-apply-button, live: link-apply-live, spinner: link-apply-spinner, text: link-apply-text"
696
+ label=${this.linkApplyLabel}
697
+ @click=${() => this.applyLink()}
698
+ @helix-press=${stopPress}
699
+ >
700
+ ${drawIcon(check)}
701
+ </${button}>
702
+ <${button}
703
+ part="link-remove"
704
+ exportparts="button: link-remove-button, live: link-remove-live, spinner: link-remove-spinner, text: link-remove-text"
705
+ ?disabled=${this.formats.link === ''}
706
+ label=${this.linkRemoveLabel}
707
+ @click=${() => this.removeLink()}
708
+ @helix-press=${stopPress}
709
+ >
710
+ ${drawIcon(linkBreak)}
711
+ </${button}>
712
+ <${button}
713
+ part="link-close"
714
+ exportparts="button: link-close-button, live: link-close-live, spinner: link-close-spinner, text: link-close-text"
715
+ label=${this.linkCloseLabel}
716
+ @click=${() => this.closeLink()}
717
+ @helix-press=${stopPress}
718
+ >
719
+ ${drawIcon(x)}
720
+ </${button}>
721
+ </${toolbar}>
722
+ </div>`;
723
+ }
724
+ render() {
725
+ return html `<div
726
+ part="region"
727
+ role="group"
728
+ aria-label=${this.label || this.getAttribute('aria-label') || this.fallbackLabel}
729
+ >
730
+ <div part="header" ?hidden=${!this.drawsAny && !this.hasActions && !this.findOpen}>
731
+ ${this.renderControls()} ${this.renderLink()} ${this.renderFind()}
732
+ </div>
733
+ <div part="viewport"><slot></slot></div>
734
+ <slot name="footer" part="footer"></slot>
735
+ </div>`;
736
+ }
737
+ };
738
+ __decorate([
739
+ words,
740
+ property({ attribute: 'block-chosen-label' })
741
+ ], HelixHeadlessEditor.prototype, "blockChosenLabel", void 0);
742
+ __decorate([
743
+ words,
744
+ property({ attribute: 'block-label' })
745
+ ], HelixHeadlessEditor.prototype, "blockLabel", void 0);
746
+ __decorate([
747
+ words,
748
+ property({ attribute: 'bold-label' })
749
+ ], HelixHeadlessEditor.prototype, "boldLabel", void 0);
750
+ __decorate([
751
+ words,
752
+ property({ attribute: 'bullet-list-label' })
753
+ ], HelixHeadlessEditor.prototype, "bulletListLabel", void 0);
754
+ __decorate([
755
+ words,
756
+ property({ attribute: 'code-block-label' })
757
+ ], HelixHeadlessEditor.prototype, "codeBlockLabel", void 0);
758
+ __decorate([
759
+ words,
760
+ property({ attribute: 'code-label' })
761
+ ], HelixHeadlessEditor.prototype, "codeLabel", void 0);
762
+ __decorate([
763
+ words,
764
+ property({ attribute: 'controls-label' })
765
+ ], HelixHeadlessEditor.prototype, "controlsLabel", void 0);
766
+ __decorate([
767
+ words,
768
+ property({ attribute: 'fallback-label' })
769
+ ], HelixHeadlessEditor.prototype, "fallbackLabel", void 0);
770
+ __decorate([
771
+ words,
772
+ property({ attribute: 'find-label' })
773
+ ], HelixHeadlessEditor.prototype, "findLabel", void 0);
774
+ __decorate([
775
+ words,
776
+ property({ attribute: 'heading-label' })
777
+ ], HelixHeadlessEditor.prototype, "headingLabel", void 0);
778
+ __decorate([
779
+ words,
780
+ property({ attribute: 'italic-label' })
781
+ ], HelixHeadlessEditor.prototype, "italicLabel", void 0);
782
+ __decorate([
783
+ property()
784
+ ], HelixHeadlessEditor.prototype, "label", void 0);
785
+ __decorate([
786
+ words,
787
+ property({ attribute: 'link-apply-label' })
788
+ ], HelixHeadlessEditor.prototype, "linkApplyLabel", void 0);
789
+ __decorate([
790
+ words,
791
+ property({ attribute: 'link-close-label' })
792
+ ], HelixHeadlessEditor.prototype, "linkCloseLabel", void 0);
793
+ __decorate([
794
+ words,
795
+ property({ attribute: 'link-controls-label' })
796
+ ], HelixHeadlessEditor.prototype, "linkControlsLabel", void 0);
797
+ __decorate([
798
+ words,
799
+ property({ attribute: 'link-field-label' })
800
+ ], HelixHeadlessEditor.prototype, "linkFieldLabel", void 0);
801
+ __decorate([
802
+ words,
803
+ property({ attribute: 'link-label' })
804
+ ], HelixHeadlessEditor.prototype, "linkLabel", void 0);
805
+ __decorate([
806
+ words,
807
+ property({ attribute: 'link-remove-label' })
808
+ ], HelixHeadlessEditor.prototype, "linkRemoveLabel", void 0);
809
+ __decorate([
810
+ words,
811
+ property({ attribute: 'ordered-list-label' })
812
+ ], HelixHeadlessEditor.prototype, "orderedListLabel", void 0);
813
+ __decorate([
814
+ words,
815
+ property({ attribute: 'paragraph-label' })
816
+ ], HelixHeadlessEditor.prototype, "paragraphLabel", void 0);
817
+ __decorate([
818
+ property()
819
+ ], HelixHeadlessEditor.prototype, "placeholder", void 0);
820
+ __decorate([
821
+ words,
822
+ property({ attribute: 'quote-label' })
823
+ ], HelixHeadlessEditor.prototype, "quoteLabel", void 0);
824
+ __decorate([
825
+ property({ attribute: 'readonly', type: Boolean })
826
+ ], HelixHeadlessEditor.prototype, "readOnly", void 0);
827
+ __decorate([
828
+ words,
829
+ property({ attribute: 'redo-label' })
830
+ ], HelixHeadlessEditor.prototype, "redoLabel", void 0);
831
+ __decorate([
832
+ words,
833
+ property({ attribute: 'strike-label' })
834
+ ], HelixHeadlessEditor.prototype, "strikeLabel", void 0);
835
+ __decorate([
836
+ property()
837
+ ], HelixHeadlessEditor.prototype, "tools", void 0);
838
+ __decorate([
839
+ words,
840
+ property({ attribute: 'undo-label' })
841
+ ], HelixHeadlessEditor.prototype, "undoLabel", void 0);
842
+ __decorate([
843
+ state()
844
+ ], HelixHeadlessEditor.prototype, "formats", void 0);
845
+ __decorate([
846
+ state()
847
+ ], HelixHeadlessEditor.prototype, "renderer", void 0);
848
+ __decorate([
849
+ state()
850
+ ], HelixHeadlessEditor.prototype, "linkOpen", void 0);
851
+ __decorate([
852
+ state()
853
+ ], HelixHeadlessEditor.prototype, "draft", void 0);
854
+ __decorate([
855
+ state()
856
+ ], HelixHeadlessEditor.prototype, "findOpen", void 0);
857
+ HelixHeadlessEditor = __decorate([
858
+ customElement('helix-headless-editor')
859
+ ], HelixHeadlessEditor);
860
+ export { HelixHeadlessEditor };