@besaitech/ng-design-system 0.0.72 → 0.0.74
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/README.md +5 -2
- package/charts/index.d.ts +1 -1
- package/fesm2022/besaitech-ng-design-system-rich-text.mjs +223 -5
- package/fesm2022/besaitech-ng-design-system-rich-text.mjs.map +1 -1
- package/fesm2022/besaitech-ng-design-system.mjs +4 -0
- package/fesm2022/besaitech-ng-design-system.mjs.map +1 -1
- package/index.d.ts +24 -20
- package/package.json +2 -1
- package/rich-text/index.d.ts +36 -3
package/README.md
CHANGED
|
@@ -273,13 +273,16 @@ import { SdsRichTextEditor, SDS_RICH_TEXT_LABELS } from '@besaitech/ng-design-sy
|
|
|
273
273
|
|
|
274
274
|
| Component | Selector | Notes |
|
|
275
275
|
| --- | --- | --- |
|
|
276
|
-
| `SdsRichTextEditor` | `<sds-rich-text-editor>` | CVA over an HTML string (`''` when empty). `[tools]` (headings, bold, italic, underline, strike, lists, quote, link, divider, clear, undo/redo) is the toolbar AND the schema; `rows` / `maxRows`, `maxLength` + counter, `label`, `hint`, `error`, `invalid`, `required`, `readonly`, `disabled`, `appearance` (default·filled), `contentLanguage` (spell-check language); `focus()`, `insertText(text)` |
|
|
276
|
+
| `SdsRichTextEditor` | `<sds-rich-text-editor>` | CVA over an HTML string (`''` when empty). `[tools]` (headings, bold, italic, underline, strike, lists, quote, link, table, divider, clear, undo/redo) is the toolbar AND the schema; `rows` / `maxRows`, `maxLength` + counter, `label`, `hint`, `error`, `invalid`, `required`, `readonly`, `disabled`, `appearance` (default·filled), `contentLanguage` (spell-check language); `focus()`, `insertText(text)` |
|
|
277
277
|
| `SdsLocalizedRichTextEditor` | `<sds-localized-rich-text-editor>` | CVA over an `SdsLocalizedValue` (`{ fr: '<p>…</p>', de: … }`), like `sds-localized-input`: `languages`, `currentLanguage`, `requiredLanguages`, switcher at the end of the toolbar with filled / required / missing dots, empty translations pruned. One editor per language underneath — undo, caret and spell-check stay per language. Same rich-text inputs as above (`maxLength` is per language) |
|
|
278
278
|
| `SdsRichTextToolbarEnd` | `[sdsRichTextToolbarEnd]` | Marks content projected at the end of the toolbar (kept in `readonly`): a language switcher, "insert variable" buttons calling `editor.insertText(…)` |
|
|
279
279
|
|
|
280
280
|
The emitted HTML is clean by construction: no inline styles, nothing outside the enabled tools, and links
|
|
281
281
|
limited to http(s), mailto, tel, site-relative paths and anchors (`sdsSafeHref`) with
|
|
282
|
-
`rel="noopener noreferrer nofollow"` — still sanitise stored HTML when you display it.
|
|
282
|
+
`rel="noopener noreferrer nofollow"` — still sanitise stored HTML when you display it. Tables are saved bare
|
|
283
|
+
(no `<colgroup>`, no inline widths). When loaded HTML holds content the tools cannot keep (`<img>`, `<iframe>`, a
|
|
284
|
+
table without the `table` tool…), a warning under the field names it before anyone edits — saving an edit would
|
|
285
|
+
drop it. ⌘/Ctrl+K opens the
|
|
283
286
|
link field and is not passed on to an app-wide ⌘K palette. Strings are English by default: translate them
|
|
284
287
|
per instance with `[labels]` (a `Partial<SdsRichTextLabels>`, follows a live language switch) or app-wide with
|
|
285
288
|
`{ provide: SDS_RICH_TEXT_LABELS, useValue: { bold: 'Gras', … } }`.
|
package/charts/index.d.ts
CHANGED
|
@@ -1293,7 +1293,7 @@ declare class SdsKpi {
|
|
|
1293
1293
|
readonly trendLabels: _angular_core.InputSignal<readonly string[] | null>;
|
|
1294
1294
|
readonly trendVariant: _angular_core.InputSignal<SdsSparklineVariant>;
|
|
1295
1295
|
readonly trendColor: _angular_core.InputSignal<string | null>;
|
|
1296
|
-
readonly icon: _angular_core.InputSignal<"x" | "search" | "bold" | "link" | "filter" | "menu" | "table" | "copy" | "square" | "list" | "chart-column" | "chart-line" | "chart-bar" | "chart-area" | "chart-pie" | "chart-scatter" | "grid-3x3" | "activity" | "gauge" | "trending-up" | "trending-down" | "clipboard-list" | "credit-card" | "external-link" | "languages" | "megaphone" | "monitor" | "store" | "user-round" | "home" | "user-plus" | "user-check" | "hand" | "users" | "bike" | "building-2" | "package" | "package-plus" | "package-open" | "rotate-ccw" | "bar-chart-3" | "log-out" | "panel-left-close" | "panel-left-open" | "panel-left" | "calendar" | "chevron-right" | "chevron-left" | "chevron-down" | "chevron-up" | "plus" | "minus" | "chevrons-left" | "chevrons-right" | "check" | "alert-circle" | "info" | "type" | "tag" | "ticket" | "square-check" | "circle-dot" | "circle-user" | "layout-grid" | "panels-top-left" | "panel-top" | "rows-3" | "book-open" | "palette" | "bell" | "phone" | "settings" | "align-left" | "toggle-left" | "arrow-up" | "arrow-down" | "chevrons-up-down" | "chevrons-up" | "chevrons-down" | "star" | "message-square" | "qr-code" | "eye" | "eye-off" | "user" | "check-circle" | "arrows-clockwise" | "file-text" | "file-check" | "hash" | "map-pin" | "inbox" | "arrow-right" | "refresh-cw" | "loader-circle" | "locate-fixed" | "locate-off" | "bug" | "scroll-text" | "moon" | "sun" | "search-x" | "command" | "sparkles" | "triangle-alert" | "octagon-x" | "ban" | "ellipsis" | "ellipsis-vertical" | "grip-vertical" | "upload" | "image" | "image-off" | "utensils" | "zoom-in" | "zoom-out" | "trash-2" | "clock" | "pencil" | "circle-help" | "stock" | "allergen-cereals-gluten" | "allergen-crustaceans" | "allergen-eggs" | "allergen-fish" | "allergen-peanuts" | "allergen-soybeans" | "allergen-milk" | "allergen-nuts" | "allergen-celery" | "allergen-mustard" | "allergen-sesame" | "allergen-sulphites" | "allergen-lupin" | "allergen-molluscs" | "italic" | "underline" | "strikethrough" | "heading-2" | "heading-3" | "pilcrow" | "list-ordered" | "text-quote" | "unlink" | "undo-2" | "redo-2" | "remove-formatting" | null>;
|
|
1296
|
+
readonly icon: _angular_core.InputSignal<"x" | "search" | "bold" | "link" | "filter" | "menu" | "table" | "copy" | "square" | "list" | "chart-column" | "chart-line" | "chart-bar" | "chart-area" | "chart-pie" | "chart-scatter" | "grid-3x3" | "activity" | "gauge" | "trending-up" | "trending-down" | "clipboard-list" | "credit-card" | "external-link" | "languages" | "megaphone" | "monitor" | "store" | "user-round" | "home" | "user-plus" | "user-check" | "hand" | "users" | "bike" | "building-2" | "package" | "package-plus" | "package-open" | "rotate-ccw" | "bar-chart-3" | "log-out" | "panel-left-close" | "panel-left-open" | "panel-left" | "calendar" | "chevron-right" | "chevron-left" | "chevron-down" | "chevron-up" | "plus" | "minus" | "chevrons-left" | "chevrons-right" | "check" | "alert-circle" | "info" | "type" | "tag" | "ticket" | "square-check" | "circle-dot" | "circle-user" | "layout-grid" | "panels-top-left" | "panel-top" | "rows-3" | "book-open" | "palette" | "bell" | "phone" | "settings" | "printer" | "align-left" | "toggle-left" | "arrow-up" | "arrow-down" | "chevrons-up-down" | "chevrons-up" | "chevrons-down" | "star" | "message-square" | "qr-code" | "eye" | "eye-off" | "user" | "check-circle" | "arrows-clockwise" | "file-text" | "file-check" | "hash" | "map-pin" | "inbox" | "arrow-right" | "refresh-cw" | "loader-circle" | "locate-fixed" | "locate-off" | "bug" | "scroll-text" | "moon" | "sun" | "search-x" | "command" | "sparkles" | "triangle-alert" | "octagon-x" | "ban" | "ellipsis" | "ellipsis-vertical" | "grip-vertical" | "upload" | "image" | "image-off" | "utensils" | "zoom-in" | "zoom-out" | "trash-2" | "clock" | "pencil" | "circle-help" | "stock" | "allergen-cereals-gluten" | "allergen-crustaceans" | "allergen-eggs" | "allergen-fish" | "allergen-peanuts" | "allergen-soybeans" | "allergen-milk" | "allergen-nuts" | "allergen-celery" | "allergen-mustard" | "allergen-sesame" | "allergen-sulphites" | "allergen-lupin" | "allergen-molluscs" | "italic" | "underline" | "strikethrough" | "heading-2" | "heading-3" | "pilcrow" | "list-ordered" | "text-quote" | "unlink" | "undo-2" | "redo-2" | "remove-formatting" | "between-horizontal-end" | "between-vertical-end" | "grid-2x2-x" | null>;
|
|
1297
1297
|
/** Shown under the value when there is no change to report. */
|
|
1298
1298
|
readonly hint: _angular_core.InputSignal<string>;
|
|
1299
1299
|
readonly size: _angular_core.InputSignal<SdsKpiSize>;
|
|
@@ -5,6 +5,7 @@ import { NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
|
|
|
5
5
|
import { SdsIcon, SdsButton } from '@besaitech/ng-design-system';
|
|
6
6
|
import { Editor } from '@tiptap/core';
|
|
7
7
|
import { Placeholder, CharacterCount } from '@tiptap/extensions';
|
|
8
|
+
import { TableKit } from '@tiptap/extension-table';
|
|
8
9
|
import StarterKit from '@tiptap/starter-kit';
|
|
9
10
|
|
|
10
11
|
/** The toolbar shown when `[tools]` is not set. */
|
|
@@ -19,6 +20,7 @@ const SDS_RICH_TEXT_DEFAULT_TOOLS = [
|
|
|
19
20
|
'orderedList',
|
|
20
21
|
'blockquote',
|
|
21
22
|
'link',
|
|
23
|
+
'table',
|
|
22
24
|
'clearFormatting',
|
|
23
25
|
'undo',
|
|
24
26
|
'redo',
|
|
@@ -36,6 +38,7 @@ const SDS_RICH_TEXT_LABELS_EN = {
|
|
|
36
38
|
orderedList: 'Numbered list',
|
|
37
39
|
blockquote: 'Quote',
|
|
38
40
|
link: 'Link',
|
|
41
|
+
table: 'Table',
|
|
39
42
|
horizontalRule: 'Divider',
|
|
40
43
|
clearFormatting: 'Clear formatting',
|
|
41
44
|
undo: 'Undo',
|
|
@@ -46,6 +49,14 @@ const SDS_RICH_TEXT_LABELS_EN = {
|
|
|
46
49
|
linkRemove: 'Remove link',
|
|
47
50
|
linkCancel: 'Cancel',
|
|
48
51
|
linkInvalid: 'Enter a web address (https://…), an e-mail address or a path starting with /.',
|
|
52
|
+
tableTools: 'Table',
|
|
53
|
+
tableAddRow: 'Row below',
|
|
54
|
+
tableAddColumn: 'Column right',
|
|
55
|
+
tableDeleteRow: 'Delete row',
|
|
56
|
+
tableDeleteColumn: 'Delete column',
|
|
57
|
+
tableHeaderRow: 'Header row',
|
|
58
|
+
tableDelete: 'Delete table',
|
|
59
|
+
unsupportedContent: 'This text contains elements this editor cannot keep: {elements}. Changing it will remove them.',
|
|
49
60
|
countLimit: '{count} of {max} characters used',
|
|
50
61
|
count: '{count} characters',
|
|
51
62
|
};
|
|
@@ -103,11 +114,44 @@ const TOOL_DEFS = [
|
|
|
103
114
|
{ tool: 'orderedList', icon: 'list-ordered', group: 2, toggle: true, shortcut: 'Mod-Shift-7' },
|
|
104
115
|
{ tool: 'blockquote', icon: 'text-quote', group: 2, toggle: true, shortcut: 'Mod-Shift-B' },
|
|
105
116
|
{ tool: 'link', icon: 'link', group: 3, toggle: true, shortcut: 'Mod-K' },
|
|
117
|
+
{ tool: 'table', icon: 'table', group: 3, toggle: false },
|
|
106
118
|
{ tool: 'horizontalRule', icon: 'minus', group: 3, toggle: false },
|
|
107
119
|
{ tool: 'clearFormatting', icon: 'remove-formatting', group: 4, toggle: false },
|
|
108
120
|
{ tool: 'undo', icon: 'undo-2', group: 5, toggle: false, shortcut: 'Mod-Z' },
|
|
109
121
|
{ tool: 'redo', icon: 'redo-2', group: 5, toggle: false, shortcut: 'Mod-Shift-Z' },
|
|
110
122
|
];
|
|
123
|
+
const TABLE_ACTIONS = [
|
|
124
|
+
{ action: 'addRow', label: 'tableAddRow', icon: 'between-horizontal-end' },
|
|
125
|
+
{ action: 'addColumn', label: 'tableAddColumn', icon: 'between-vertical-end' },
|
|
126
|
+
{ action: 'deleteRow', label: 'tableDeleteRow', icon: 'minus' },
|
|
127
|
+
{ action: 'deleteColumn', label: 'tableDeleteColumn', icon: 'minus' },
|
|
128
|
+
{ action: 'headerRow', label: 'tableHeaderRow', icon: null },
|
|
129
|
+
{ action: 'delete', label: 'tableDelete', icon: 'grid-2x2-x' },
|
|
130
|
+
];
|
|
131
|
+
/**
|
|
132
|
+
* Elements whose CONTENT (not just formatting) is lost when the schema cannot hold them — pictures, embeds,
|
|
133
|
+
* tables, form controls… Loaded HTML holding one of them that the editor drops triggers the warning.
|
|
134
|
+
*/
|
|
135
|
+
const LOSSY_TAGS = new Set([
|
|
136
|
+
'img',
|
|
137
|
+
'picture',
|
|
138
|
+
'figure',
|
|
139
|
+
'svg',
|
|
140
|
+
'canvas',
|
|
141
|
+
'video',
|
|
142
|
+
'audio',
|
|
143
|
+
'iframe',
|
|
144
|
+
'embed',
|
|
145
|
+
'object',
|
|
146
|
+
'table',
|
|
147
|
+
'hr',
|
|
148
|
+
'form',
|
|
149
|
+
'input',
|
|
150
|
+
'select',
|
|
151
|
+
'textarea',
|
|
152
|
+
'button',
|
|
153
|
+
'math',
|
|
154
|
+
]);
|
|
111
155
|
/** One line of the editable area, for `[rows]` → min-height. */
|
|
112
156
|
const LINE_EM = 1.6;
|
|
113
157
|
const FRAME_BASE = 'overflow-hidden rounded-control border text-ink transition-colors';
|
|
@@ -212,6 +256,11 @@ class SdsRichTextEditor {
|
|
|
212
256
|
/** Formatting active at the selection, per tool (refreshed on every transaction). */
|
|
213
257
|
activeTools = signal(new Set(), ...(ngDevMode ? [{ debugName: "activeTools" }] : []));
|
|
214
258
|
history = signal({ undo: false, redo: false }, ...(ngDevMode ? [{ debugName: "history" }] : []));
|
|
259
|
+
/** The first row of the table holding the caret is a header row. */
|
|
260
|
+
headerRow = signal(false, ...(ngDevMode ? [{ debugName: "headerRow" }] : []));
|
|
261
|
+
/** Elements of the loaded HTML the enabled tools could not keep (see {@link LOSSY_TAGS}). */
|
|
262
|
+
lostElements = signal([], ...(ngDevMode ? [{ debugName: "lostElements" }] : []));
|
|
263
|
+
tableActions = TABLE_ACTIONS;
|
|
215
264
|
characters = signal(0, ...(ngDevMode ? [{ debugName: "characters" }] : []));
|
|
216
265
|
rovingTool = signal(null, ...(ngDevMode ? [{ debugName: "rovingTool" }] : []));
|
|
217
266
|
linkOpen = signal(false, ...(ngDevMode ? [{ debugName: "linkOpen" }] : []));
|
|
@@ -238,6 +287,9 @@ class SdsRichTextEditor {
|
|
|
238
287
|
return [...byGroup.entries()].sort(([a], [b]) => a - b).map(([, defs]) => defs);
|
|
239
288
|
}, ...(ngDevMode ? [{ debugName: "groups" }] : []));
|
|
240
289
|
hasCounter = computed(() => this.showCount() || this.maxLength() > 0, ...(ngDevMode ? [{ debugName: "hasCounter" }] : []));
|
|
290
|
+
lostLabel = computed(() => this.l().unsupportedContent.replace('{elements}', this.lostElements()
|
|
291
|
+
.map((tag) => `<${tag}>`)
|
|
292
|
+
.join(', ')), ...(ngDevMode ? [{ debugName: "lostLabel" }] : []));
|
|
241
293
|
atLimit = computed(() => this.maxLength() > 0 && this.characters() >= this.maxLength(), ...(ngDevMode ? [{ debugName: "atLimit" }] : []));
|
|
242
294
|
countLabel = computed(() => (this.maxLength() ? this.l().countLimit : this.l().count)
|
|
243
295
|
.replace('{count}', String(this.characters()))
|
|
@@ -271,6 +323,8 @@ class SdsRichTextEditor {
|
|
|
271
323
|
ids.push(`${this.fieldId}-hint`);
|
|
272
324
|
if (this.hasCounter())
|
|
273
325
|
ids.push(`${this.fieldId}-count`);
|
|
326
|
+
if (this.lostElements().length)
|
|
327
|
+
ids.push(`${this.fieldId}-lost`);
|
|
274
328
|
return ids.join(' ');
|
|
275
329
|
}, ...(ngDevMode ? [{ debugName: "describedBy" }] : []));
|
|
276
330
|
/** Attributes of the contenteditable document (ProseMirror's own element). */
|
|
@@ -370,6 +424,8 @@ class SdsRichTextEditor {
|
|
|
370
424
|
.run();
|
|
371
425
|
this.refresh(editor);
|
|
372
426
|
}
|
|
427
|
+
if (editor)
|
|
428
|
+
this.detectLoss(this.html, editor);
|
|
373
429
|
}
|
|
374
430
|
registerOnChange(fn) {
|
|
375
431
|
this.onChange = fn;
|
|
@@ -389,6 +445,9 @@ class SdsRichTextEditor {
|
|
|
389
445
|
return this.history().undo;
|
|
390
446
|
if (tool === 'redo')
|
|
391
447
|
return this.history().redo;
|
|
448
|
+
// no table inside a table
|
|
449
|
+
if (tool === 'table')
|
|
450
|
+
return !this.isOn('table');
|
|
392
451
|
return true;
|
|
393
452
|
}
|
|
394
453
|
buttonClass(def) {
|
|
@@ -454,6 +513,9 @@ class SdsRichTextEditor {
|
|
|
454
513
|
case 'horizontalRule':
|
|
455
514
|
chain.setHorizontalRule().run();
|
|
456
515
|
break;
|
|
516
|
+
case 'table':
|
|
517
|
+
chain.insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
|
|
518
|
+
break;
|
|
457
519
|
case 'clearFormatting':
|
|
458
520
|
chain.unsetAllMarks().clearNodes().run();
|
|
459
521
|
break;
|
|
@@ -497,6 +559,41 @@ class SdsRichTextEditor {
|
|
|
497
559
|
editor.commands.focus('end');
|
|
498
560
|
}
|
|
499
561
|
}
|
|
562
|
+
tableButtonClass(action) {
|
|
563
|
+
const base = 'inline-flex h-7 items-center gap-1 rounded-md px-2 text-xs font-medium transition-colors ' +
|
|
564
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500';
|
|
565
|
+
if (action === 'headerRow' && this.headerRow())
|
|
566
|
+
return `${base} ${TOOL_BUTTON_ON}`;
|
|
567
|
+
if (action === 'delete')
|
|
568
|
+
return `${base} text-danger hover:bg-danger/10`;
|
|
569
|
+
return `${base} text-ink-soft hover:bg-muted hover:text-ink`;
|
|
570
|
+
}
|
|
571
|
+
runTable(action) {
|
|
572
|
+
const editor = this.editor;
|
|
573
|
+
if (!editor || !this.interactive())
|
|
574
|
+
return;
|
|
575
|
+
const chain = editor.chain().focus();
|
|
576
|
+
switch (action) {
|
|
577
|
+
case 'addRow':
|
|
578
|
+
chain.addRowAfter().run();
|
|
579
|
+
break;
|
|
580
|
+
case 'addColumn':
|
|
581
|
+
chain.addColumnAfter().run();
|
|
582
|
+
break;
|
|
583
|
+
case 'deleteRow':
|
|
584
|
+
chain.deleteRow().run();
|
|
585
|
+
break;
|
|
586
|
+
case 'deleteColumn':
|
|
587
|
+
chain.deleteColumn().run();
|
|
588
|
+
break;
|
|
589
|
+
case 'headerRow':
|
|
590
|
+
chain.toggleHeaderRow().run();
|
|
591
|
+
break;
|
|
592
|
+
case 'delete':
|
|
593
|
+
chain.deleteTable().run();
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
500
597
|
// --- link -------------------------------------------------------------------
|
|
501
598
|
openLink() {
|
|
502
599
|
const editor = this.editor;
|
|
@@ -582,6 +679,7 @@ class SdsRichTextEditor {
|
|
|
582
679
|
});
|
|
583
680
|
this.editor = editor;
|
|
584
681
|
this.refresh(editor);
|
|
682
|
+
this.detectLoss(content, editor);
|
|
585
683
|
this.ready.set(true);
|
|
586
684
|
}
|
|
587
685
|
extensions() {
|
|
@@ -615,6 +713,7 @@ class SdsRichTextEditor {
|
|
|
615
713
|
}
|
|
616
714
|
: false,
|
|
617
715
|
}),
|
|
716
|
+
...(tools.has('table') ? [TableKit.configure({ table: { resizable: false } })] : []),
|
|
618
717
|
Placeholder.configure({ placeholder: () => this.placeholder() }),
|
|
619
718
|
CharacterCount.configure({ limit: this.maxLength() > 0 ? this.maxLength() : null }),
|
|
620
719
|
];
|
|
@@ -642,11 +741,29 @@ class SdsRichTextEditor {
|
|
|
642
741
|
return true;
|
|
643
742
|
}
|
|
644
743
|
/**
|
|
645
|
-
* The control's value for the editor's document: '' when it is empty, and without the
|
|
646
|
-
* the
|
|
744
|
+
* The control's value for the editor's document: '' when it is empty, and without the editor's own
|
|
745
|
+
* scaffolding — the empty paragraphs kept after a final list / quote / table so the caret can leave it
|
|
746
|
+
* (Tiptap's trailing node), and the table layout hints (`<colgroup>`, `style="min-width…"`, the default
|
|
747
|
+
* `colspan="1" rowspan="1"`) that columns no one resized do not need.
|
|
647
748
|
*/
|
|
648
749
|
currentHtml(editor) {
|
|
649
|
-
|
|
750
|
+
if (isBlank(editor))
|
|
751
|
+
return '';
|
|
752
|
+
return editor
|
|
753
|
+
.getHTML()
|
|
754
|
+
.replace(/<colgroup>[\s\S]*?<\/colgroup>/g, '')
|
|
755
|
+
.replace(/<table style="[^"]*">/g, '<table>')
|
|
756
|
+
.replace(/ (?:colspan|rowspan)="1"/g, '')
|
|
757
|
+
.replace(/(?:<p><\/p>)+$/, '');
|
|
758
|
+
}
|
|
759
|
+
/** Flags the elements of `source` whose content the schema could not keep (shown under the field). */
|
|
760
|
+
detectLoss(source, editor) {
|
|
761
|
+
if (!source || typeof DOMParser === 'undefined') {
|
|
762
|
+
this.lostElements.set([]);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
const kept = tagsOf(editor.getHTML());
|
|
766
|
+
this.lostElements.set([...tagsOf(source)].filter((tag) => LOSSY_TAGS.has(tag) && !kept.has(tag)));
|
|
650
767
|
}
|
|
651
768
|
emit(editor) {
|
|
652
769
|
const html = this.currentHtml(editor);
|
|
@@ -672,7 +789,9 @@ class SdsRichTextEditor {
|
|
|
672
789
|
check('orderedList', 'orderedList');
|
|
673
790
|
check('blockquote', 'blockquote');
|
|
674
791
|
check('link', 'link');
|
|
792
|
+
check('table', 'table');
|
|
675
793
|
this.activeTools.set(active);
|
|
794
|
+
this.headerRow.set(active.has('table') && firstRowIsHeader(editor));
|
|
676
795
|
this.history.set({ undo: editor.can().undo(), redo: editor.can().redo() });
|
|
677
796
|
this.characters.set(editor.storage.characterCount.characters());
|
|
678
797
|
}
|
|
@@ -781,6 +900,32 @@ class SdsRichTextEditor {
|
|
|
781
900
|
</div>
|
|
782
901
|
}
|
|
783
902
|
|
|
903
|
+
@if (isOn('table') && interactive()) {
|
|
904
|
+
<div
|
|
905
|
+
role="toolbar"
|
|
906
|
+
[attr.aria-label]="l().tableTools"
|
|
907
|
+
class="flex flex-wrap items-center gap-0.5 border-b border-border bg-surface px-1.5 py-1"
|
|
908
|
+
>
|
|
909
|
+
@for (t of tableActions; track t.action) {
|
|
910
|
+
<button
|
|
911
|
+
type="button"
|
|
912
|
+
[attr.data-table-action]="t.action"
|
|
913
|
+
[attr.aria-pressed]="
|
|
914
|
+
t.action === 'headerRow' ? (headerRow() ? 'true' : 'false') : null
|
|
915
|
+
"
|
|
916
|
+
[class]="tableButtonClass(t.action)"
|
|
917
|
+
(mousedown)="$event.preventDefault()"
|
|
918
|
+
(click)="runTable(t.action)"
|
|
919
|
+
>
|
|
920
|
+
@if (t.icon) {
|
|
921
|
+
<sds-icon [name]="t.icon" [size]="14" />
|
|
922
|
+
}
|
|
923
|
+
{{ l()[t.label] }}
|
|
924
|
+
</button>
|
|
925
|
+
}
|
|
926
|
+
</div>
|
|
927
|
+
}
|
|
928
|
+
|
|
784
929
|
<div
|
|
785
930
|
#content
|
|
786
931
|
class="sds-rte"
|
|
@@ -790,6 +935,16 @@ class SdsRichTextEditor {
|
|
|
790
935
|
></div>
|
|
791
936
|
</div>
|
|
792
937
|
|
|
938
|
+
@if (lostElements().length) {
|
|
939
|
+
<p
|
|
940
|
+
[id]="fieldId + '-lost'"
|
|
941
|
+
class="mt-1.5 flex items-start gap-1.5 rounded-control border border-warning/40 bg-warning/10 px-2.5 py-1.5 text-xs text-ink"
|
|
942
|
+
>
|
|
943
|
+
<sds-icon name="triangle-alert" [size]="14" class="mt-px shrink-0 text-warning" />
|
|
944
|
+
<span>{{ lostLabel() }}</span>
|
|
945
|
+
</p>
|
|
946
|
+
}
|
|
947
|
+
|
|
793
948
|
@if (hasCounter()) {
|
|
794
949
|
<div class="mt-1.5 flex items-start gap-2 text-xs">
|
|
795
950
|
@if (error()) {
|
|
@@ -814,7 +969,7 @@ class SdsRichTextEditor {
|
|
|
814
969
|
} @else if (hint()) {
|
|
815
970
|
<p [id]="fieldId + '-hint'" class="mt-1.5 text-xs text-ink-faint">{{ hint() }}</p>
|
|
816
971
|
}
|
|
817
|
-
`, isInline: true, styles: [".sds-rte-content{outline:none;padding:.625rem .875rem;color:var(--color-ink);font-size:.875rem;line-height:1.6;overflow-wrap:anywhere;white-space:pre-wrap}.sds-rte-content>:first-child{margin-top:0}.sds-rte-content>:last-child{margin-bottom:0}.sds-rte-content p,.sds-rte-content ul,.sds-rte-content ol,.sds-rte-content blockquote{margin:0 0 .75em}.sds-rte-content h2{font-size:1.25em;font-weight:600;line-height:1.3;margin:1.1em 0 .5em}.sds-rte-content h3{font-size:1.1em;font-weight:600;line-height:1.35;margin:1em 0 .4em}.sds-rte-content ul{list-style:disc;padding-left:1.5em}.sds-rte-content ol{list-style:decimal;padding-left:1.5em}.sds-rte-content li>p{margin:0}.sds-rte-content li+li{margin-top:.25em}.sds-rte-content li>ul,.sds-rte-content li>ol{margin:.25em 0 0}.sds-rte-content blockquote{border-left:3px solid var(--color-border);padding-left:.875em;color:var(--color-ink-soft)}.sds-rte-content a{color:var(--color-brand-700, var(--color-accent-blue));text-decoration:underline;text-underline-offset:2px}.dark .sds-rte-content a{color:var(--sds-dark-on-tint, var(--color-ink))}.sds-rte-content hr{border:0;border-top:1px solid var(--color-border);margin:1em 0}.sds-rte-content hr.ProseMirror-selectednode{outline:2px solid var(--color-brand-500, var(--color-accent-blue));outline-offset:2px}.sds-rte-content p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:var(--color-ink-faint);float:left;height:0;pointer-events:none}.sds-rte-content[contenteditable=false]{cursor:default}\n"], dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth", "filled"] }, { kind: "component", type: SdsButton, selector: "sds-button", inputs: ["variant", "size", "shape", "type", "disabled", "loading", "block", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
972
|
+
`, isInline: true, styles: [".sds-rte-content{outline:none;padding:.625rem .875rem;color:var(--color-ink);font-size:.875rem;line-height:1.6;overflow-wrap:anywhere;white-space:pre-wrap}.sds-rte-content>:first-child{margin-top:0}.sds-rte-content>:last-child{margin-bottom:0}.sds-rte-content p,.sds-rte-content ul,.sds-rte-content ol,.sds-rte-content blockquote{margin:0 0 .75em}.sds-rte-content h2{font-size:1.25em;font-weight:600;line-height:1.3;margin:1.1em 0 .5em}.sds-rte-content h3{font-size:1.1em;font-weight:600;line-height:1.35;margin:1em 0 .4em}.sds-rte-content ul{list-style:disc;padding-left:1.5em}.sds-rte-content ol{list-style:decimal;padding-left:1.5em}.sds-rte-content li>p{margin:0}.sds-rte-content li+li{margin-top:.25em}.sds-rte-content li>ul,.sds-rte-content li>ol{margin:.25em 0 0}.sds-rte-content blockquote{border-left:3px solid var(--color-border);padding-left:.875em;color:var(--color-ink-soft)}.sds-rte-content a{color:var(--color-brand-700, var(--color-accent-blue));text-decoration:underline;text-underline-offset:2px}.dark .sds-rte-content a{color:var(--sds-dark-on-tint, var(--color-ink))}.sds-rte{overflow-x:auto}.sds-rte-content table{border-collapse:collapse;width:100%;margin:0 0 .75em}.sds-rte-content th,.sds-rte-content td{border:1px solid var(--color-border);padding:.375em .5em;vertical-align:top;text-align:left;min-width:4em}.sds-rte-content th{background:var(--color-muted);font-weight:600}.sds-rte-content th>p,.sds-rte-content td>p{margin:0}.sds-rte-content .selectedCell{background:color-mix(in oklab,var(--color-brand-500, var(--color-accent-blue)) 14%,transparent)}.sds-rte-content hr{border:0;border-top:1px solid var(--color-border);margin:1em 0}.sds-rte-content hr.ProseMirror-selectednode{outline:2px solid var(--color-brand-500, var(--color-accent-blue));outline-offset:2px}.sds-rte-content p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:var(--color-ink-faint);float:left;height:0;pointer-events:none}.sds-rte-content[contenteditable=false]{cursor:default}\n"], dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth", "filled"] }, { kind: "component", type: SdsButton, selector: "sds-button", inputs: ["variant", "size", "shape", "type", "disabled", "loading", "block", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
818
973
|
}
|
|
819
974
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsRichTextEditor, decorators: [{
|
|
820
975
|
type: Component,
|
|
@@ -922,6 +1077,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
922
1077
|
</div>
|
|
923
1078
|
}
|
|
924
1079
|
|
|
1080
|
+
@if (isOn('table') && interactive()) {
|
|
1081
|
+
<div
|
|
1082
|
+
role="toolbar"
|
|
1083
|
+
[attr.aria-label]="l().tableTools"
|
|
1084
|
+
class="flex flex-wrap items-center gap-0.5 border-b border-border bg-surface px-1.5 py-1"
|
|
1085
|
+
>
|
|
1086
|
+
@for (t of tableActions; track t.action) {
|
|
1087
|
+
<button
|
|
1088
|
+
type="button"
|
|
1089
|
+
[attr.data-table-action]="t.action"
|
|
1090
|
+
[attr.aria-pressed]="
|
|
1091
|
+
t.action === 'headerRow' ? (headerRow() ? 'true' : 'false') : null
|
|
1092
|
+
"
|
|
1093
|
+
[class]="tableButtonClass(t.action)"
|
|
1094
|
+
(mousedown)="$event.preventDefault()"
|
|
1095
|
+
(click)="runTable(t.action)"
|
|
1096
|
+
>
|
|
1097
|
+
@if (t.icon) {
|
|
1098
|
+
<sds-icon [name]="t.icon" [size]="14" />
|
|
1099
|
+
}
|
|
1100
|
+
{{ l()[t.label] }}
|
|
1101
|
+
</button>
|
|
1102
|
+
}
|
|
1103
|
+
</div>
|
|
1104
|
+
}
|
|
1105
|
+
|
|
925
1106
|
<div
|
|
926
1107
|
#content
|
|
927
1108
|
class="sds-rte"
|
|
@@ -931,6 +1112,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
931
1112
|
></div>
|
|
932
1113
|
</div>
|
|
933
1114
|
|
|
1115
|
+
@if (lostElements().length) {
|
|
1116
|
+
<p
|
|
1117
|
+
[id]="fieldId + '-lost'"
|
|
1118
|
+
class="mt-1.5 flex items-start gap-1.5 rounded-control border border-warning/40 bg-warning/10 px-2.5 py-1.5 text-xs text-ink"
|
|
1119
|
+
>
|
|
1120
|
+
<sds-icon name="triangle-alert" [size]="14" class="mt-px shrink-0 text-warning" />
|
|
1121
|
+
<span>{{ lostLabel() }}</span>
|
|
1122
|
+
</p>
|
|
1123
|
+
}
|
|
1124
|
+
|
|
934
1125
|
@if (hasCounter()) {
|
|
935
1126
|
<div class="mt-1.5 flex items-start gap-2 text-xs">
|
|
936
1127
|
@if (error()) {
|
|
@@ -955,8 +1146,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
955
1146
|
} @else if (hint()) {
|
|
956
1147
|
<p [id]="fieldId + '-hint'" class="mt-1.5 text-xs text-ink-faint">{{ hint() }}</p>
|
|
957
1148
|
}
|
|
958
|
-
`, styles: [".sds-rte-content{outline:none;padding:.625rem .875rem;color:var(--color-ink);font-size:.875rem;line-height:1.6;overflow-wrap:anywhere;white-space:pre-wrap}.sds-rte-content>:first-child{margin-top:0}.sds-rte-content>:last-child{margin-bottom:0}.sds-rte-content p,.sds-rte-content ul,.sds-rte-content ol,.sds-rte-content blockquote{margin:0 0 .75em}.sds-rte-content h2{font-size:1.25em;font-weight:600;line-height:1.3;margin:1.1em 0 .5em}.sds-rte-content h3{font-size:1.1em;font-weight:600;line-height:1.35;margin:1em 0 .4em}.sds-rte-content ul{list-style:disc;padding-left:1.5em}.sds-rte-content ol{list-style:decimal;padding-left:1.5em}.sds-rte-content li>p{margin:0}.sds-rte-content li+li{margin-top:.25em}.sds-rte-content li>ul,.sds-rte-content li>ol{margin:.25em 0 0}.sds-rte-content blockquote{border-left:3px solid var(--color-border);padding-left:.875em;color:var(--color-ink-soft)}.sds-rte-content a{color:var(--color-brand-700, var(--color-accent-blue));text-decoration:underline;text-underline-offset:2px}.dark .sds-rte-content a{color:var(--sds-dark-on-tint, var(--color-ink))}.sds-rte-content hr{border:0;border-top:1px solid var(--color-border);margin:1em 0}.sds-rte-content hr.ProseMirror-selectednode{outline:2px solid var(--color-brand-500, var(--color-accent-blue));outline-offset:2px}.sds-rte-content p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:var(--color-ink-faint);float:left;height:0;pointer-events:none}.sds-rte-content[contenteditable=false]{cursor:default}\n"] }]
|
|
1149
|
+
`, styles: [".sds-rte-content{outline:none;padding:.625rem .875rem;color:var(--color-ink);font-size:.875rem;line-height:1.6;overflow-wrap:anywhere;white-space:pre-wrap}.sds-rte-content>:first-child{margin-top:0}.sds-rte-content>:last-child{margin-bottom:0}.sds-rte-content p,.sds-rte-content ul,.sds-rte-content ol,.sds-rte-content blockquote{margin:0 0 .75em}.sds-rte-content h2{font-size:1.25em;font-weight:600;line-height:1.3;margin:1.1em 0 .5em}.sds-rte-content h3{font-size:1.1em;font-weight:600;line-height:1.35;margin:1em 0 .4em}.sds-rte-content ul{list-style:disc;padding-left:1.5em}.sds-rte-content ol{list-style:decimal;padding-left:1.5em}.sds-rte-content li>p{margin:0}.sds-rte-content li+li{margin-top:.25em}.sds-rte-content li>ul,.sds-rte-content li>ol{margin:.25em 0 0}.sds-rte-content blockquote{border-left:3px solid var(--color-border);padding-left:.875em;color:var(--color-ink-soft)}.sds-rte-content a{color:var(--color-brand-700, var(--color-accent-blue));text-decoration:underline;text-underline-offset:2px}.dark .sds-rte-content a{color:var(--sds-dark-on-tint, var(--color-ink))}.sds-rte{overflow-x:auto}.sds-rte-content table{border-collapse:collapse;width:100%;margin:0 0 .75em}.sds-rte-content th,.sds-rte-content td{border:1px solid var(--color-border);padding:.375em .5em;vertical-align:top;text-align:left;min-width:4em}.sds-rte-content th{background:var(--color-muted);font-weight:600}.sds-rte-content th>p,.sds-rte-content td>p{margin:0}.sds-rte-content .selectedCell{background:color-mix(in oklab,var(--color-brand-500, var(--color-accent-blue)) 14%,transparent)}.sds-rte-content hr{border:0;border-top:1px solid var(--color-border);margin:1em 0}.sds-rte-content hr.ProseMirror-selectednode{outline:2px solid var(--color-brand-500, var(--color-accent-blue));outline-offset:2px}.sds-rte-content p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:var(--color-ink-faint);float:left;height:0;pointer-events:none}.sds-rte-content[contenteditable=false]{cursor:default}\n"] }]
|
|
959
1150
|
}], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], contentLanguage: [{ type: i0.Input, args: [{ isSignal: true, alias: "contentLanguage", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], tools: [{ type: i0.Input, args: [{ isSignal: true, alias: "tools", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], maxRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxRows", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], showCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCount", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], contentHost: [{ type: i0.ViewChild, args: ['content', { isSignal: true }] }], linkInput: [{ type: i0.ViewChild, args: ['linkInput', { isSignal: true }] }], toolButtons: [{ type: i0.ViewChildren, args: ['toolButton', { isSignal: true }] }], toolbarEnd: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsRichTextToolbarEnd), { isSignal: true }] }] } });
|
|
1151
|
+
/** The tag names (lower case) of every element in an HTML fragment. */
|
|
1152
|
+
function tagsOf(html) {
|
|
1153
|
+
const body = new DOMParser().parseFromString(html, 'text/html').body;
|
|
1154
|
+
return new Set(Array.from(body.querySelectorAll('*'), (el) => el.tagName.toLowerCase()));
|
|
1155
|
+
}
|
|
1156
|
+
/** Whether the table around the selection starts with a row of header cells. */
|
|
1157
|
+
function firstRowIsHeader(editor) {
|
|
1158
|
+
const { $from } = editor.state.selection;
|
|
1159
|
+
for (let depth = $from.depth; depth > 0; depth--) {
|
|
1160
|
+
const node = $from.node(depth);
|
|
1161
|
+
if (node.type.name === 'table') {
|
|
1162
|
+
return node.firstChild?.firstChild?.type.name === 'tableHeader';
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return false;
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* A document holding nothing but one empty paragraph. (Tiptap's `isEmpty` only looks at the text, so a fresh
|
|
1169
|
+
* empty table or a lone divider would count as empty and be dropped from the value.)
|
|
1170
|
+
*/
|
|
1171
|
+
function isBlank(editor) {
|
|
1172
|
+
const doc = editor.state.doc;
|
|
1173
|
+
return (doc.childCount === 0 ||
|
|
1174
|
+
(doc.childCount === 1 &&
|
|
1175
|
+
doc.firstChild.type.name === 'paragraph' &&
|
|
1176
|
+
doc.firstChild.content.size === 0));
|
|
1177
|
+
}
|
|
960
1178
|
|
|
961
1179
|
let nextId = 0;
|
|
962
1180
|
const TAB_BASE = 'flex items-center gap-1 rounded px-1.5 py-1 text-[11px] font-semibold uppercase leading-none transition-colors ' +
|