@spfx-fast/sharepoint-ui 0.4.3 → 0.5.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.
- package/QUICKSTART.md +13 -128
- package/README.md +19 -26
- package/components.json +240 -5834
- package/dist/accordion.d.ts +11 -0
- package/dist/accordion.js +4 -0
- package/dist/alert.d.ts +3 -0
- package/dist/alert.js +4 -0
- package/dist/attachment-list.d.ts +15 -0
- package/dist/attachment-list.js +14 -0
- package/dist/avatar-group.d.ts +12 -0
- package/dist/avatar-group.js +7 -0
- package/dist/avatar.d.ts +6 -0
- package/dist/avatar.js +5 -0
- package/dist/badge.d.ts +3 -0
- package/dist/badge.js +4 -0
- package/dist/breadcrumb.d.ts +10 -0
- package/dist/breadcrumb.js +9 -0
- package/dist/button-group.d.ts +6 -0
- package/dist/button-group.js +4 -0
- package/dist/button.d.ts +3 -0
- package/dist/button.js +4 -0
- package/dist/card.d.ts +3 -0
- package/dist/card.js +4 -0
- package/dist/carousel.d.ts +11 -0
- package/dist/carousel.js +6 -0
- package/dist/checkbox.d.ts +3 -0
- package/dist/checkbox.js +4 -0
- package/dist/combobox.d.ts +9 -0
- package/dist/combobox.js +6 -0
- package/dist/command-bar.d.ts +6 -0
- package/dist/command-bar.js +4 -0
- package/dist/copy-button.d.ts +7 -0
- package/dist/copy-button.js +19 -0
- package/dist/data-grid.d.ts +25 -0
- package/dist/data-grid.js +39 -0
- package/dist/date-range-picker.d.ts +12 -0
- package/dist/date-range-picker.js +4 -0
- package/dist/dialog.d.ts +9 -0
- package/dist/dialog.js +21 -0
- package/dist/divider.d.ts +3 -0
- package/dist/divider.js +4 -0
- package/dist/drawer.d.ts +6 -0
- package/dist/drawer.js +5 -0
- package/dist/empty-state.d.ts +7 -0
- package/dist/empty-state.js +5 -0
- package/dist/field.d.ts +15 -0
- package/dist/field.js +11 -0
- package/dist/file-dropzone.d.ts +15 -0
- package/dist/file-dropzone.js +21 -0
- package/dist/filter-bar.d.ts +9 -0
- package/dist/filter-bar.js +9 -0
- package/dist/image.d.ts +5 -0
- package/dist/image.js +4 -0
- package/dist/index.d.ts +68 -78
- package/dist/index.js +60 -68
- package/dist/input.d.ts +3 -0
- package/dist/input.js +4 -0
- package/dist/label.d.ts +3 -0
- package/dist/label.js +4 -0
- package/dist/link.d.ts +3 -0
- package/dist/link.js +9 -0
- package/dist/list-view.d.ts +29 -0
- package/dist/list-view.js +14 -0
- package/dist/list.d.ts +6 -0
- package/dist/list.js +5 -0
- package/dist/lookup-picker.d.ts +12 -0
- package/dist/lookup-picker.js +4 -0
- package/dist/menu.d.ts +6 -0
- package/dist/menu.js +4 -0
- package/dist/nav.d.ts +12 -0
- package/dist/nav.js +4 -0
- package/dist/pagination.d.ts +11 -0
- package/dist/pagination.js +5 -0
- package/dist/person-picker.d.ts +13 -0
- package/dist/person-picker.js +4 -0
- package/dist/persona.d.ts +7 -0
- package/dist/persona.js +5 -0
- package/dist/popover.d.ts +6 -0
- package/dist/popover.js +4 -0
- package/dist/progress-bar.d.ts +5 -0
- package/dist/progress-bar.js +5 -0
- package/dist/radio-group.d.ts +6 -0
- package/dist/radio-group.js +4 -0
- package/dist/radio.d.ts +3 -0
- package/dist/radio.js +4 -0
- package/dist/rating.d.ts +8 -0
- package/dist/rating.js +5 -0
- package/dist/react.d.ts +1 -4715
- package/dist/react.js +1 -7097
- package/dist/select.d.ts +3 -0
- package/dist/select.js +4 -0
- package/dist/sharepoint-field.d.ts +18 -0
- package/dist/sharepoint-field.js +54 -0
- package/dist/skeleton.d.ts +6 -0
- package/dist/skeleton.js +9 -0
- package/dist/spinner.d.ts +5 -0
- package/dist/spinner.js +4 -0
- package/dist/swatch-picker.d.ts +17 -0
- package/dist/swatch-picker.js +11 -0
- package/dist/switch.d.ts +3 -0
- package/dist/switch.js +4 -0
- package/dist/table.d.ts +6 -0
- package/dist/table.js +4 -0
- package/dist/tabs.d.ts +14 -0
- package/dist/tabs.js +30 -0
- package/dist/tag-group.d.ts +6 -0
- package/dist/tag-group.js +4 -0
- package/dist/tag-picker.d.ts +12 -0
- package/dist/tag-picker.js +4 -0
- package/dist/tag.d.ts +7 -0
- package/dist/tag.js +5 -0
- package/dist/text.d.ts +5 -0
- package/dist/text.js +4 -0
- package/dist/textarea.d.ts +3 -0
- package/dist/textarea.js +4 -0
- package/dist/theme.d.ts +15 -175
- package/dist/theme.js +36 -239
- package/dist/toast.d.ts +9 -0
- package/dist/toast.js +12 -0
- package/dist/tree.d.ts +13 -0
- package/dist/tree.js +9 -0
- package/dist/view-card.d.ts +8 -0
- package/dist/view-card.js +4 -0
- package/dist/view-switcher.d.ts +13 -0
- package/dist/view-switcher.js +7 -0
- package/dist/virtual-list.d.ts +11 -0
- package/dist/virtual-list.js +25 -0
- package/measurements.json +365 -0
- package/package.json +14 -46
- package/src/theme.css +125 -0
- package/AGENTS.md +0 -231
- package/dist/appearance.d.ts +0 -6
- package/dist/appearance.js +0 -217
- package/dist/elements/accordion.d.ts +0 -9
- package/dist/elements/accordion.js +0 -338
- package/dist/elements/alert.d.ts +0 -2
- package/dist/elements/alert.js +0 -86
- package/dist/elements/avatar-group-primitives.d.ts +0 -3
- package/dist/elements/avatar-group-primitives.js +0 -189
- package/dist/elements/avatar-group.d.ts +0 -2
- package/dist/elements/avatar-group.js +0 -110
- package/dist/elements/avatar.d.ts +0 -4
- package/dist/elements/avatar.js +0 -251
- package/dist/elements/badge.d.ts +0 -8
- package/dist/elements/badge.js +0 -182
- package/dist/elements/breadcrumb.d.ts +0 -4
- package/dist/elements/breadcrumb.js +0 -233
- package/dist/elements/button-group.d.ts +0 -1
- package/dist/elements/button-group.js +0 -20
- package/dist/elements/button-variants.d.ts +0 -6
- package/dist/elements/button-variants.js +0 -339
- package/dist/elements/button.d.ts +0 -5
- package/dist/elements/button.js +0 -233
- package/dist/elements/card-primitives.d.ts +0 -3
- package/dist/elements/card-primitives.js +0 -83
- package/dist/elements/card.d.ts +0 -5
- package/dist/elements/card.js +0 -213
- package/dist/elements/carousel.d.ts +0 -92
- package/dist/elements/carousel.js +0 -1115
- package/dist/elements/color-picker.d.ts +0 -24
- package/dist/elements/color-picker.js +0 -1004
- package/dist/elements/combobox.d.ts +0 -1
- package/dist/elements/combobox.js +0 -1012
- package/dist/elements/data-grid-model.d.ts +0 -120
- package/dist/elements/data-grid-model.js +0 -294
- package/dist/elements/data-grid.d.ts +0 -30
- package/dist/elements/data-grid.js +0 -1576
- package/dist/elements/date-picker.d.ts +0 -1
- package/dist/elements/date-picker.js +0 -486
- package/dist/elements/dialog.d.ts +0 -23
- package/dist/elements/dialog.js +0 -409
- package/dist/elements/divider.d.ts +0 -2
- package/dist/elements/divider.js +0 -66
- package/dist/elements/drawer.d.ts +0 -3
- package/dist/elements/drawer.js +0 -308
- package/dist/elements/field-composition.d.ts +0 -26
- package/dist/elements/field-composition.js +0 -133
- package/dist/elements/field.d.ts +0 -1
- package/dist/elements/field.js +0 -67
- package/dist/elements/file-upload.d.ts +0 -6
- package/dist/elements/file-upload.js +0 -211
- package/dist/elements/floating-position.d.ts +0 -36
- package/dist/elements/floating-position.js +0 -92
- package/dist/elements/image.d.ts +0 -28
- package/dist/elements/image.js +0 -38
- package/dist/elements/info.d.ts +0 -9
- package/dist/elements/info.js +0 -102
- package/dist/elements/input.d.ts +0 -5
- package/dist/elements/input.js +0 -255
- package/dist/elements/label.d.ts +0 -15
- package/dist/elements/label.js +0 -56
- package/dist/elements/link.d.ts +0 -1
- package/dist/elements/link.js +0 -92
- package/dist/elements/list.d.ts +0 -19
- package/dist/elements/list.js +0 -297
- package/dist/elements/menu.d.ts +0 -33
- package/dist/elements/menu.js +0 -1103
- package/dist/elements/message-bar.d.ts +0 -10
- package/dist/elements/message-bar.js +0 -327
- package/dist/elements/nav.d.ts +0 -32
- package/dist/elements/nav.js +0 -794
- package/dist/elements/overflow.d.ts +0 -10
- package/dist/elements/overflow.js +0 -218
- package/dist/elements/persona.d.ts +0 -3
- package/dist/elements/persona.js +0 -125
- package/dist/elements/popover.d.ts +0 -77
- package/dist/elements/popover.js +0 -840
- package/dist/elements/progress-bar.d.ts +0 -4
- package/dist/elements/progress-bar.js +0 -82
- package/dist/elements/radio-group.d.ts +0 -1
- package/dist/elements/radio-group.js +0 -303
- package/dist/elements/rating-display.d.ts +0 -2
- package/dist/elements/rating-display.js +0 -125
- package/dist/elements/rating.d.ts +0 -2
- package/dist/elements/rating.js +0 -206
- package/dist/elements/rich-text-editor.d.ts +0 -32
- package/dist/elements/rich-text-editor.js +0 -1367
- package/dist/elements/search.d.ts +0 -3
- package/dist/elements/search.js +0 -200
- package/dist/elements/select.d.ts +0 -6
- package/dist/elements/select.js +0 -291
- package/dist/elements/shared.d.ts +0 -22
- package/dist/elements/shared.js +0 -409
- package/dist/elements/sharepoint-field-basic-controls.d.ts +0 -1
- package/dist/elements/sharepoint-field-basic-controls.js +0 -14
- package/dist/elements/sharepoint-fields.d.ts +0 -29
- package/dist/elements/sharepoint-fields.js +0 -460
- package/dist/elements/skeleton.d.ts +0 -6
- package/dist/elements/skeleton.js +0 -91
- package/dist/elements/slider.d.ts +0 -1
- package/dist/elements/slider.js +0 -248
- package/dist/elements/spin-button.d.ts +0 -7
- package/dist/elements/spin-button.js +0 -499
- package/dist/elements/spinner.d.ts +0 -4
- package/dist/elements/spinner.js +0 -124
- package/dist/elements/swatch-picker.d.ts +0 -5
- package/dist/elements/swatch-picker.js +0 -363
- package/dist/elements/table.d.ts +0 -21
- package/dist/elements/table.js +0 -261
- package/dist/elements/tabs.d.ts +0 -1
- package/dist/elements/tabs.js +0 -276
- package/dist/elements/tag-picker.d.ts +0 -29
- package/dist/elements/tag-picker.js +0 -715
- package/dist/elements/tags.d.ts +0 -21
- package/dist/elements/tags.js +0 -421
- package/dist/elements/teaching-popover.d.ts +0 -32
- package/dist/elements/teaching-popover.js +0 -412
- package/dist/elements/text.d.ts +0 -9
- package/dist/elements/text.js +0 -75
- package/dist/elements/textarea.d.ts +0 -1
- package/dist/elements/textarea.js +0 -182
- package/dist/elements/toast.d.ts +0 -25
- package/dist/elements/toast.js +0 -337
- package/dist/elements/toaster.d.ts +0 -6
- package/dist/elements/toaster.js +0 -357
- package/dist/elements/toggle.d.ts +0 -2
- package/dist/elements/toggle.js +0 -246
- package/dist/elements/toolbar.d.ts +0 -12
- package/dist/elements/toolbar.js +0 -381
- package/dist/elements/tooltip.d.ts +0 -76
- package/dist/elements/tooltip.js +0 -527
- package/dist/elements/tree-model.d.ts +0 -19
- package/dist/elements/tree-model.js +0 -34
- package/dist/elements/tree.d.ts +0 -5
- package/dist/elements/tree.js +0 -273
- package/dist/elements/view-card.d.ts +0 -2
- package/dist/elements/view-card.js +0 -78
- package/dist/elements/virtual-list.d.ts +0 -41
- package/dist/elements/virtual-list.js +0 -369
- package/dist/elements.d.ts +0 -73
- package/dist/elements.js +0 -210
- package/dist/sharepoint-fields.d.ts +0 -135
- package/dist/sharepoint-fields.js +0 -402
- package/dist/virtualizer-hooks.d.ts +0 -80
- package/dist/virtualizer-hooks.js +0 -253
- package/scripts/generate-token-index.mjs +0 -43
- package/scripts/sync-component-catalog-signatures.mjs +0 -55
- package/scripts/validate-component-catalog.mjs +0 -44
- package/skills/sharepoint-ui/SKILL.md +0 -12
- package/skills/sharepoint-ui/references/choosing-components.md +0 -20
- package/skills/sharepoint-ui/references/sharepoint-patterns.md +0 -17
- package/tokens.json +0 -3777
|
@@ -1,1367 +0,0 @@
|
|
|
1
|
-
import { base, syncHostAccessibleName } from "./shared.js";
|
|
2
|
-
export { getRichTextEditorFieldOptions } from "../sharepoint-fields.js";
|
|
3
|
-
const toolbarIconPaths = {
|
|
4
|
-
bold: "M6 4h5a3 3 0 0 1 0 6H6z M6 10h6a3 3 0 0 1 0 6H6z", italic: "M10 4h6 M8 16h6 M13 4 9 16", underline: "M6 4v5a4 4 0 0 0 8 0V4 M5 16h10", strikeThrough: "M3 10h14 M6 5c1-2 7-2 8 0 M6 15c1 2 7 2 8 0",
|
|
5
|
-
subscript: "M5 4l8 8m0-8L5 12m9 3h4m-3 0v3", superscript: "M5 4l8 8m0-8L5 12m9-5h4m-3 0V4",
|
|
6
|
-
justifyLeft: "M3 4h14M3 8h9M3 12h14M3 16h9", justifyCenter: "M4 4h12M6 8h8M4 12h12M6 16h8", justifyRight: "M3 4h14M8 8h9M3 12h14M8 16h9", justifyFull: "M3 4h14M3 8h14M3 12h14M3 16h14",
|
|
7
|
-
removeFormat: "M4 4l12 12 M6 6l4 4m2 2 2 2 M8 4h8 M5 16h5",
|
|
8
|
-
insertUnorderedList: "M8 5h9 M8 10h9 M8 15h9 M4 5h.01 M4 10h.01 M4 15h.01", insertOrderedList: "M9 5h8 M9 10h8 M9 15h8 M4 4h1v3 M3.5 7h2 M3.5 10.5c0-1 2-1 2 0 0 .6-2 1.5-2 2.5h2 M4 14h1a1 1 0 0 1 0 2H4",
|
|
9
|
-
undo: "M9 7 5 10l4 3 M5 10h7a4 4 0 0 1 4 4", redo: "M11 7 15 10l-4 3 M15 10H8a4 4 0 0 0-4 4",
|
|
10
|
-
insertTable: "M3 4h14v12H3z M3 8h14 M3 12h14 M8 4v12 M13 4v12", insertImage: "M3 4h14v12H3z M4 14l4-4 3 3 2-2 3 3 M7 7h.01",
|
|
11
|
-
insertRowAbove: "M3 9h14v7H3z M3 12h14 M10 3v4 M8 5h4", insertRowBelow: "M3 4h14v7H3z M3 8h14 M10 13v4 M8 15h4",
|
|
12
|
-
insertColumnBefore: "M6 4h11v12H6z M10 4v12 M14 4v12 M3 10h3 M4.5 8.5v3", insertColumnAfter: "M3 4h11v12H3z M7 4v12 M11 4v12 M14 10h3 M15.5 8.5v3",
|
|
13
|
-
deleteRow: "M3 5h14v10H3z M3 10h14 M8 5v5 M13 5v5 M8 13h4", deleteColumn: "M5 3v14h10V3z M10 3v14 M5 8h5 M5 13h5 M12 8v4", deleteTable: "M3 4h14v12H3z M3 8h14 M3 12h14 M8 4v12 M13 4v12 M10 9v4 M8.5 11h3",
|
|
14
|
-
createLink: "M7 10h6 M9 7l1-1a3 3 0 0 1 4 4l-2 2a3 3 0 0 1-4 0 M11 13l-1 1a3 3 0 0 1-4-4l2-2a3 3 0 0 1 4 0"
|
|
15
|
-
};
|
|
16
|
-
function createToolbarIcon(command) {
|
|
17
|
-
const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
18
|
-
icon.setAttribute("viewBox", "0 0 20 20");
|
|
19
|
-
icon.setAttribute("width", "20");
|
|
20
|
-
icon.setAttribute("height", "20");
|
|
21
|
-
icon.setAttribute("aria-hidden", "true");
|
|
22
|
-
icon.setAttribute("focusable", "false");
|
|
23
|
-
icon.setAttribute("fill", "none");
|
|
24
|
-
icon.setAttribute("stroke", "currentColor");
|
|
25
|
-
icon.setAttribute("stroke-width", "1.7");
|
|
26
|
-
icon.setAttribute("stroke-linecap", "round");
|
|
27
|
-
icon.setAttribute("stroke-linejoin", "round");
|
|
28
|
-
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
29
|
-
path.setAttribute("d", toolbarIconPaths[command] ?? "M4 10h12");
|
|
30
|
-
icon.append(path);
|
|
31
|
-
return icon;
|
|
32
|
-
}
|
|
33
|
-
const ALLOWED = new Set(["P", "DIV", "SPAN", "BR", "HR", "STRONG", "B", "EM", "I", "U", "S", "STRIKE", "DEL", "INS", "UL", "OL", "LI", "A", "H1", "H2", "H3", "H4", "H5", "H6", "BLOCKQUOTE", "PRE", "TABLE", "THEAD", "TBODY", "TFOOT", "TR", "TH", "TD", "CAPTION", "SUP", "SUB", "CODE", "IMG"]);
|
|
34
|
-
const HTML_ONLY = new Set(["MAIN", "SECTION", "ARTICLE", "HEADER", "FOOTER", "ASIDE", "FIGURE", "FIGCAPTION", "DETAILS", "SUMMARY", "DL", "DT", "DD", "ABBR", "CITE", "Q", "MARK", "SMALL", "TIME", "WBR"]);
|
|
35
|
-
const RESTRICTED = new Set(["P", "DIV", "SPAN", "BR", "STRONG", "B", "EM", "I", "U", "S", "STRIKE", "DEL", "INS", "UL", "OL", "LI", "H1", "H2", "H3", "H4", "H5", "H6", "BLOCKQUOTE", "PRE", "SUP", "SUB", "CODE"]);
|
|
36
|
-
const DROP_CONTENT = new Set(["SCRIPT", "STYLE", "IFRAME", "OBJECT", "EMBED", "TEMPLATE"]);
|
|
37
|
-
const HTML_STYLE_PROPERTIES = new Set(["background-color", "color", "direction", "font-family", "font-size", "font-style", "font-weight", "letter-spacing", "line-height", "list-style-type", "margin-block-end", "margin-block-start", "margin-inline-end", "margin-inline-start", "text-align", "text-decoration", "text-indent", "text-transform", "vertical-align", "white-space"]);
|
|
38
|
-
const RICH_STYLE_PROPERTIES = new Set(["background-color", "color", "font-family", "font-size", "font-style", "font-weight", "text-align", "text-decoration"]);
|
|
39
|
-
const FONT_SIZE_PX = { "1": "10px", "2": "12px", "3": "16px", "4": "18px", "5": "24px", "6": "32px", "7": "48px" };
|
|
40
|
-
function safeHref(value) {
|
|
41
|
-
if (/[\u0000-\u001f\u007f]/.test(value))
|
|
42
|
-
return false;
|
|
43
|
-
try {
|
|
44
|
-
return ["http:", "https:", "mailto:"].includes(new URL(value.trim(), document.baseURI).protocol);
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
function safeImageSrc(value) {
|
|
51
|
-
try {
|
|
52
|
-
return ["http:", "https:"].includes(new URL(value.trim(), document.baseURI).protocol);
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
function safeLanguageTag(value) {
|
|
59
|
-
const tag = value.trim();
|
|
60
|
-
if (!tag || tag.length > 128)
|
|
61
|
-
return false;
|
|
62
|
-
if (typeof Intl !== "undefined" && typeof Intl.getCanonicalLocales === "function") {
|
|
63
|
-
try {
|
|
64
|
-
return Intl.getCanonicalLocales(tag).length === 1;
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return /^[a-z]{2,8}(?:-[a-z0-9]{1,8})*$/i.test(tag);
|
|
71
|
-
}
|
|
72
|
-
function plainText(html) {
|
|
73
|
-
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
74
|
-
doc.body.querySelectorAll([...DROP_CONTENT].join(",")).forEach(element => element.remove());
|
|
75
|
-
const read = (node) => [...node.childNodes].map(child => {
|
|
76
|
-
if (child.nodeType === Node.TEXT_NODE)
|
|
77
|
-
return child.textContent ?? "";
|
|
78
|
-
if (child.nodeType !== Node.ELEMENT_NODE)
|
|
79
|
-
return "";
|
|
80
|
-
if (child.tagName === "BR")
|
|
81
|
-
return "\n";
|
|
82
|
-
const value = read(child);
|
|
83
|
-
return /^(ADDRESS|ARTICLE|BLOCKQUOTE|DIV|H[1-6]|LI|P|PRE|SECTION)$/.test(child.tagName) ? `${value}\n` : value;
|
|
84
|
-
}).join("");
|
|
85
|
-
return read(doc.body).replace(/[\t ]*\n[\t ]*/g, "\n").replace(/\n{3,}/g, "\n\n").replace(/\n$/, "");
|
|
86
|
-
}
|
|
87
|
-
function cleanHtml(html, mode = "html", allowHyperlink = true) {
|
|
88
|
-
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
89
|
-
if (mode === "plain")
|
|
90
|
-
return plainText(html);
|
|
91
|
-
const allowedTags = mode === "restricted" ? RESTRICTED : mode === "html" ? new Set([...ALLOWED, ...HTML_ONLY]) : ALLOWED;
|
|
92
|
-
const clean = (node) => {
|
|
93
|
-
for (const child of [...node.childNodes]) {
|
|
94
|
-
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
95
|
-
const element = child;
|
|
96
|
-
if (DROP_CONTENT.has(element.tagName)) {
|
|
97
|
-
child.remove();
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
if (element.tagName === "FONT" && (mode === "rich" || mode === "html")) {
|
|
101
|
-
const span = doc.createElement("span");
|
|
102
|
-
const face = element.getAttribute("face")?.trim();
|
|
103
|
-
const color = element.getAttribute("color")?.trim();
|
|
104
|
-
const size = element.getAttribute("size")?.trim();
|
|
105
|
-
if (face && (typeof CSS === "undefined" || CSS.supports("font-family", face)))
|
|
106
|
-
span.style.fontFamily = face;
|
|
107
|
-
if (color && (typeof CSS === "undefined" || CSS.supports("color", color)))
|
|
108
|
-
span.style.color = color;
|
|
109
|
-
if (size && FONT_SIZE_PX[size])
|
|
110
|
-
span.style.fontSize = FONT_SIZE_PX[size];
|
|
111
|
-
if (element.hasAttribute("style"))
|
|
112
|
-
span.setAttribute("style", [span.getAttribute("style"), element.getAttribute("style")].filter(Boolean).join(";"));
|
|
113
|
-
span.append(...[...element.childNodes]);
|
|
114
|
-
child.replaceWith(span);
|
|
115
|
-
clean(span);
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
if (!allowedTags.has(element.tagName) || (element.tagName === "A" && !allowHyperlink)) {
|
|
119
|
-
clean(element);
|
|
120
|
-
child.replaceWith(...[...child.childNodes]);
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
for (const attr of [...element.attributes]) {
|
|
124
|
-
if (attr.name === "style" && (mode === "html" || mode === "rich")) {
|
|
125
|
-
const allowedStyles = mode === "html" ? HTML_STYLE_PROPERTIES : RICH_STYLE_PROPERTIES;
|
|
126
|
-
const safeStyles = [...element.style].filter(property => {
|
|
127
|
-
const value = element.style.getPropertyValue(property).trim();
|
|
128
|
-
return allowedStyles.has(property) && !/(?:url|expression|@import|javascript:|var\s*\()/i.test(value) && (typeof CSS === "undefined" || CSS.supports(property, value));
|
|
129
|
-
}).map(property => `${property}: ${element.style.getPropertyValue(property).trim()}`).join("; ");
|
|
130
|
-
if (safeStyles)
|
|
131
|
-
element.setAttribute("style", safeStyles);
|
|
132
|
-
else
|
|
133
|
-
element.removeAttribute("style");
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
const link = element.tagName === "A" && attr.name === "href" && safeHref(attr.value);
|
|
137
|
-
const linkTitle = element.tagName === "A" && attr.name === "title" && attr.value.length <= 512;
|
|
138
|
-
const linkTarget = element.tagName === "A" && attr.name === "target" && attr.value === "_blank";
|
|
139
|
-
const linkRel = element.tagName === "A" && attr.name === "rel" && /^(noopener noreferrer|noreferrer noopener)$/i.test(attr.value.trim());
|
|
140
|
-
const span = ["TD", "TH"].includes(element.tagName) && ["colspan", "rowspan"].includes(attr.name) && /^\d{1,2}$/.test(attr.value) && Number(attr.value) > 0;
|
|
141
|
-
const scope = element.tagName === "TH" && attr.name === "scope" && ["row", "col", "rowgroup", "colgroup"].includes(attr.value);
|
|
142
|
-
const image = element.tagName === "IMG" && ((attr.name === "src" && safeImageSrc(attr.value)) || attr.name === "alt" || ((attr.name === "width" || attr.name === "height") && /^\d{1,4}$/.test(attr.value)));
|
|
143
|
-
const direction = attr.name === "dir" && ["ltr", "rtl", "auto"].includes(attr.value.trim().toLowerCase());
|
|
144
|
-
const language = attr.name === "lang" && safeLanguageTag(attr.value);
|
|
145
|
-
if (!link && !linkTitle && !linkTarget && !linkRel && !span && !scope && !image && !direction && !language)
|
|
146
|
-
element.removeAttribute(attr.name);
|
|
147
|
-
}
|
|
148
|
-
if (element.tagName === "A") {
|
|
149
|
-
if (element.hasAttribute("href") && element.getAttribute("target") === "_blank")
|
|
150
|
-
element.setAttribute("rel", "noopener noreferrer");
|
|
151
|
-
else {
|
|
152
|
-
element.removeAttribute("target");
|
|
153
|
-
element.removeAttribute("rel");
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
clean(element);
|
|
157
|
-
}
|
|
158
|
-
else if (child.nodeType !== Node.TEXT_NODE)
|
|
159
|
-
child.remove();
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
clean(doc.body);
|
|
163
|
-
return doc.body.innerHTML;
|
|
164
|
-
}
|
|
165
|
-
export function defineRichTextEditor() {
|
|
166
|
-
if (customElements.get("spui-rich-text-editor"))
|
|
167
|
-
return;
|
|
168
|
-
class SpuiRichTextEditor extends HTMLElement {
|
|
169
|
-
static formAssociated = true;
|
|
170
|
-
static observedAttributes = ["label", "editor-label", "placeholder", "content-mode", "allow-hyperlink", "disabled", "read-only", "required", "required-message", "value", "name", "aria-label", "aria-labelledby", "aria-description", "aria-describedby", "aria-invalid", "bold-label", "italic-label", "underline-label", "strike-label", "subscript-label", "superscript-label", "align-left-label", "align-center-label", "align-right-label", "align-justify-label", "clear-formatting-label", "format-block-label", "font-family-label", "font-size-label", "paragraph-label", "heading-1-label", "heading-2-label", "heading-3-label", "blockquote-label", "preformatted-label", "bullet-list-label", "number-list-label", "link-label", "text-color-label", "highlight-color-label", "custom-color-label", "theme-accent-label", "color-swatch-label", "undo-label", "redo-label", "insert-table-label", "insert-image-label", "image-input-label", "insert-row-above-label", "insert-row-below-label", "insert-column-before-label", "insert-column-after-label", "delete-row-label", "delete-column-label", "delete-table-label", "toolbar-label", "link-prompt", "link-dialog-label", "link-text-label", "link-title-label", "link-apply-label", "link-remove-label", "link-cancel-label", "link-new-tab-label", "link-invalid-message", "link-picker-label", "link-picker-loading-label", "link-picker-error-message"];
|
|
171
|
-
editor;
|
|
172
|
-
internals;
|
|
173
|
-
formDisabled = false;
|
|
174
|
-
pendingValue;
|
|
175
|
-
customError = "";
|
|
176
|
-
savedRange;
|
|
177
|
-
preserveToolbarSelection = false;
|
|
178
|
-
probingSelection = false;
|
|
179
|
-
probedSelection;
|
|
180
|
-
/** Uploads to a durable asset store and returns the image URL to persist in editor HTML. */
|
|
181
|
-
imageUploader;
|
|
182
|
-
get uploadImage() { return this.imageUploader; }
|
|
183
|
-
set uploadImage(value) { this.imageUploader = value; this.syncImageButton(); }
|
|
184
|
-
linkPickerValue;
|
|
185
|
-
linkPickerButton;
|
|
186
|
-
pickingLink = false;
|
|
187
|
-
linkEditorVersion = 0;
|
|
188
|
-
get linkPicker() { return this.linkPickerValue; }
|
|
189
|
-
set linkPicker(value) { this.linkPickerValue = value; this.syncLinkPickerButton(); }
|
|
190
|
-
imageInput;
|
|
191
|
-
linkForm;
|
|
192
|
-
linkUrlInput;
|
|
193
|
-
linkTextInput;
|
|
194
|
-
linkTitleInput;
|
|
195
|
-
linkNewTabInput;
|
|
196
|
-
linkError;
|
|
197
|
-
linkPickerStatus;
|
|
198
|
-
linkRemoveButton;
|
|
199
|
-
uploadingImage = false;
|
|
200
|
-
savedCell;
|
|
201
|
-
selectedTablePath;
|
|
202
|
-
events;
|
|
203
|
-
get contentMode() { const mode = this.getAttribute("content-mode"); return mode === "plain" || mode === "restricted" || mode === "rich" || mode === "html" ? mode : "html"; }
|
|
204
|
-
get allowHyperlinks() { return this.contentMode !== "plain" && this.contentMode !== "restricted" && this.getAttribute("allow-hyperlink") !== "false"; }
|
|
205
|
-
constructor() { super(); try {
|
|
206
|
-
this.internals = this.attachInternals();
|
|
207
|
-
}
|
|
208
|
-
catch { /* Editing remains available in engines without form association. */ } }
|
|
209
|
-
connectedCallback() {
|
|
210
|
-
this.events?.abort();
|
|
211
|
-
this.events = new AbortController();
|
|
212
|
-
this.render();
|
|
213
|
-
document.addEventListener("selectionchange", this.onSelectionChange, { signal: this.events.signal });
|
|
214
|
-
if (this.pendingValue !== undefined)
|
|
215
|
-
this.value = this.pendingValue;
|
|
216
|
-
else if (this.hasAttribute("value"))
|
|
217
|
-
this.value = this.getAttribute("value") ?? "";
|
|
218
|
-
this.syncForm();
|
|
219
|
-
}
|
|
220
|
-
disconnectedCallback() { this.events?.abort(); }
|
|
221
|
-
attributeChangedCallback(name) {
|
|
222
|
-
if (!this.isConnected)
|
|
223
|
-
return;
|
|
224
|
-
if (name === "value")
|
|
225
|
-
this.value = this.getAttribute("value") ?? "";
|
|
226
|
-
else if ((name === "content-mode" || name === "allow-hyperlink") && this.editor) {
|
|
227
|
-
this.value = this.value;
|
|
228
|
-
this.syncContentMode();
|
|
229
|
-
}
|
|
230
|
-
else if ((name === "disabled" || name === "read-only") && this.editor) {
|
|
231
|
-
this.syncDisabled();
|
|
232
|
-
this.syncForm();
|
|
233
|
-
}
|
|
234
|
-
else if (name === "required" && this.editor) {
|
|
235
|
-
this.editor.setAttribute("aria-required", String(this.hasAttribute("required")));
|
|
236
|
-
this.syncForm();
|
|
237
|
-
}
|
|
238
|
-
else if (name === "required-message" && this.editor)
|
|
239
|
-
this.syncForm();
|
|
240
|
-
else if (name === "placeholder" && this.editor)
|
|
241
|
-
this.editor.dataset.placeholder = this.getAttribute("placeholder") ?? "Write something…";
|
|
242
|
-
else if (name === "label" && this.editor) {
|
|
243
|
-
const label = this.shadowRoot?.querySelector("label");
|
|
244
|
-
if (label) {
|
|
245
|
-
label.textContent = this.getAttribute("label") ?? "";
|
|
246
|
-
label.hidden = !this.hasAttribute("label");
|
|
247
|
-
}
|
|
248
|
-
this.syncEditorLabel();
|
|
249
|
-
this.refreshStrings();
|
|
250
|
-
}
|
|
251
|
-
else if (name === "editor-label" && this.editor)
|
|
252
|
-
this.syncEditorLabel();
|
|
253
|
-
else if (["aria-label", "aria-labelledby", "aria-description", "aria-describedby"].includes(name) && this.editor)
|
|
254
|
-
syncHostAccessibleName(this, this.editor);
|
|
255
|
-
else if (name === "aria-invalid" && this.editor) {
|
|
256
|
-
const value = this.getAttribute(name);
|
|
257
|
-
if (value === null)
|
|
258
|
-
this.editor.removeAttribute(name);
|
|
259
|
-
else
|
|
260
|
-
this.editor.setAttribute(name, value);
|
|
261
|
-
}
|
|
262
|
-
else
|
|
263
|
-
this.refreshStrings();
|
|
264
|
-
}
|
|
265
|
-
syncEditorLabel() { this.editor.setAttribute("aria-label", this.getAttribute("aria-label") ?? this.getAttribute("label") ?? this.getAttribute("editor-label") ?? "Rich text"); }
|
|
266
|
-
get value() { return this.editor ? (this.contentMode === "plain" ? plainText(this.editor.innerHTML) : cleanHtml(this.editor.innerHTML, this.contentMode, this.allowHyperlinks)) : this.pendingValue ?? this.getAttribute("value") ?? ""; }
|
|
267
|
-
set value(value) {
|
|
268
|
-
this.pendingValue = value;
|
|
269
|
-
if (!this.editor)
|
|
270
|
-
return;
|
|
271
|
-
const normalized = this.contentMode === "plain" ? plainText(value) : cleanHtml(value, this.contentMode, this.allowHyperlinks);
|
|
272
|
-
if (this.value !== normalized) {
|
|
273
|
-
if (this.contentMode === "plain")
|
|
274
|
-
this.editor.textContent = normalized;
|
|
275
|
-
else
|
|
276
|
-
this.editor.innerHTML = normalized;
|
|
277
|
-
}
|
|
278
|
-
this.syncForm(normalized);
|
|
279
|
-
}
|
|
280
|
-
get form() { return this.internals?.form ?? null; }
|
|
281
|
-
get validity() { return this.internals?.validity; }
|
|
282
|
-
get validationMessage() { return this.internals?.validationMessage ?? ""; }
|
|
283
|
-
get willValidate() { return Boolean(this.editor && !this.formDisabled && !this.hasAttribute("disabled") && !this.hasAttribute("read-only") && (this.internals?.willValidate ?? true)); }
|
|
284
|
-
checkValidity() { return this.willValidate ? this.internals?.checkValidity() ?? true : true; }
|
|
285
|
-
reportValidity() { return this.willValidate ? this.internals?.reportValidity() ?? true : true; }
|
|
286
|
-
setCustomValidity(message) { this.customError = String(message ?? ""); this.syncForm(); }
|
|
287
|
-
formResetCallback() { this.value = this.getAttribute("value") ?? ""; }
|
|
288
|
-
formStateRestoreCallback(state) { if (typeof state === "string")
|
|
289
|
-
this.value = state; }
|
|
290
|
-
formDisabledCallback(disabled) { this.formDisabled = disabled; if (this.editor) {
|
|
291
|
-
this.syncDisabled();
|
|
292
|
-
this.syncForm();
|
|
293
|
-
} }
|
|
294
|
-
syncDisabled() {
|
|
295
|
-
const disabled = this.formDisabled || this.hasAttribute("disabled");
|
|
296
|
-
const readOnly = disabled || this.hasAttribute("read-only");
|
|
297
|
-
this.editor.contentEditable = readOnly ? "false" : this.contentMode === "plain" ? "plaintext-only" : "true";
|
|
298
|
-
this.editor.setAttribute("aria-readonly", String(this.hasAttribute("read-only")));
|
|
299
|
-
this.editor.setAttribute("aria-disabled", String(disabled));
|
|
300
|
-
this.shadowRoot?.querySelectorAll("[role=toolbar] button,[role=toolbar] select,[role=toolbar] input[type=color]").forEach(control => { control.disabled = readOnly; });
|
|
301
|
-
this.shadowRoot?.querySelectorAll("[role=toolbar] .toolbar-color").forEach(picker => {
|
|
302
|
-
const summary = picker.querySelector("summary");
|
|
303
|
-
if (summary) {
|
|
304
|
-
summary.tabIndex = readOnly ? -1 : 0;
|
|
305
|
-
summary.setAttribute("aria-disabled", String(readOnly));
|
|
306
|
-
}
|
|
307
|
-
if (readOnly)
|
|
308
|
-
picker.open = false;
|
|
309
|
-
});
|
|
310
|
-
if (this.linkForm) {
|
|
311
|
-
if (readOnly)
|
|
312
|
-
this.linkForm.hidden = true;
|
|
313
|
-
this.linkForm.querySelectorAll("input,button").forEach(control => { control.disabled = readOnly; });
|
|
314
|
-
}
|
|
315
|
-
this.syncImageButton();
|
|
316
|
-
this.syncLinkPickerButton();
|
|
317
|
-
}
|
|
318
|
-
syncImageButton() {
|
|
319
|
-
const button = this.shadowRoot?.querySelector('[data-command="insertImage"]');
|
|
320
|
-
if (button) {
|
|
321
|
-
button.hidden = !this.imageUploader || this.contentMode === "plain" || this.contentMode === "restricted";
|
|
322
|
-
button.disabled = this.formDisabled || this.hasAttribute("disabled") || this.hasAttribute("read-only") || !this.imageUploader || this.uploadingImage;
|
|
323
|
-
}
|
|
324
|
-
this.syncToolbarGroups();
|
|
325
|
-
}
|
|
326
|
-
syncToolbarGroups() {
|
|
327
|
-
this.shadowRoot?.querySelectorAll(".toolbar-group:not(.toolbar-group--table):not(.toolbar-group--color):not(.toolbar-group--fonts)").forEach(group => { group.hidden = !group.querySelector("button:not([hidden])"); });
|
|
328
|
-
const colorGroup = this.shadowRoot?.querySelector(".toolbar-group--color");
|
|
329
|
-
if (colorGroup)
|
|
330
|
-
colorGroup.hidden = !["rich", "html"].includes(this.contentMode);
|
|
331
|
-
const fontGroup = this.shadowRoot?.querySelector(".toolbar-group--fonts");
|
|
332
|
-
if (fontGroup)
|
|
333
|
-
fontGroup.hidden = !["rich", "html"].includes(this.contentMode);
|
|
334
|
-
}
|
|
335
|
-
syncContentMode() {
|
|
336
|
-
const toolbar = this.shadowRoot?.querySelector('[role="toolbar"]');
|
|
337
|
-
if (toolbar) {
|
|
338
|
-
toolbar.hidden = this.contentMode === "plain";
|
|
339
|
-
toolbar.querySelectorAll("button[data-command]").forEach(button => {
|
|
340
|
-
const command = button.dataset.command;
|
|
341
|
-
button.hidden = (this.contentMode === "restricted" && (command === "insertTable" || command === "insertImage" || button.hasAttribute("data-table-command"))) || (this.contentMode !== "html" && ["subscript", "superscript", "justifyLeft", "justifyCenter", "justifyRight", "justifyFull"].includes(command ?? "")) || (command === "createLink" && !this.allowHyperlinks);
|
|
342
|
-
});
|
|
343
|
-
const colorGroup = toolbar.querySelector(".toolbar-group--color");
|
|
344
|
-
if (colorGroup)
|
|
345
|
-
colorGroup.hidden = !["rich", "html"].includes(this.contentMode);
|
|
346
|
-
const fontGroup = toolbar.querySelector(".toolbar-group--fonts");
|
|
347
|
-
if (fontGroup)
|
|
348
|
-
fontGroup.hidden = !["rich", "html"].includes(this.contentMode);
|
|
349
|
-
}
|
|
350
|
-
if (this.editor)
|
|
351
|
-
this.editor.style.whiteSpace = this.contentMode === "plain" ? "pre-wrap" : "";
|
|
352
|
-
this.syncDisabled();
|
|
353
|
-
this.syncTableActions();
|
|
354
|
-
this.syncToolbarGroups();
|
|
355
|
-
}
|
|
356
|
-
syncLinkPickerButton() {
|
|
357
|
-
if (this.linkPickerButton) {
|
|
358
|
-
this.linkPickerButton.hidden = !this.linkPickerValue || !this.allowHyperlinks;
|
|
359
|
-
this.linkPickerButton.disabled = this.formDisabled || this.hasAttribute("disabled") || this.hasAttribute("read-only");
|
|
360
|
-
this.linkPickerButton.setAttribute("aria-disabled", String(this.pickingLink));
|
|
361
|
-
this.linkPickerButton.setAttribute("aria-busy", String(this.pickingLink));
|
|
362
|
-
this.linkPickerButton.textContent = this.pickingLink
|
|
363
|
-
? this.getAttribute("link-picker-loading-label") ?? "Opening picker…"
|
|
364
|
-
: this.getAttribute("link-picker-label") ?? "Browse SharePoint";
|
|
365
|
-
}
|
|
366
|
-
if (this.linkPickerStatus)
|
|
367
|
-
this.linkPickerStatus.textContent = this.pickingLink ? this.getAttribute("link-picker-loading-label") ?? "Opening picker…" : "";
|
|
368
|
-
}
|
|
369
|
-
syncForm(value = this.value) { this.pendingValue = value; this.internals?.setFormValue(value || null); const active = this.willValidate; const missing = active && this.hasAttribute("required") && !this.editor?.textContent?.trim(); const customError = active && Boolean(this.customError); const flags = { ...(missing ? { valueMissing: true } : {}), ...(customError ? { customError: true } : {}) }; const message = customError ? this.customError : missing ? this.getAttribute("required-message") ?? "Enter some text." : ""; try {
|
|
370
|
-
this.internals?.setValidity(flags, message);
|
|
371
|
-
}
|
|
372
|
-
catch { /* Engines without form association remain editable. */ } }
|
|
373
|
-
commitEditorValue(eventName = "spui-input") {
|
|
374
|
-
const value = this.sanitizeEditor();
|
|
375
|
-
this.onSelectionChange();
|
|
376
|
-
this.syncForm(value);
|
|
377
|
-
const event = eventName === "spui-change"
|
|
378
|
-
? new CustomEvent("spui-change", { detail: { value }, bubbles: true, composed: true })
|
|
379
|
-
: new CustomEvent("spui-input", { detail: { value }, bubbles: true, composed: true });
|
|
380
|
-
this.dispatchEvent(event);
|
|
381
|
-
}
|
|
382
|
-
sanitizeEditor() {
|
|
383
|
-
const html = this.editor.innerHTML;
|
|
384
|
-
const value = cleanHtml(html, this.contentMode, this.allowHyperlinks);
|
|
385
|
-
if (this.contentMode === "plain") {
|
|
386
|
-
if (this.editor.textContent !== value)
|
|
387
|
-
this.editor.textContent = value;
|
|
388
|
-
return value;
|
|
389
|
-
}
|
|
390
|
-
if (value === html)
|
|
391
|
-
return value;
|
|
392
|
-
const selection = this.captureTextSelection();
|
|
393
|
-
const cell = this.selectedCell();
|
|
394
|
-
const tableIndex = cell ? [...this.editor.querySelectorAll("table")].indexOf(cell.closest("table")) : -1;
|
|
395
|
-
const rowIndex = cell?.closest("tr")?.rowIndex ?? -1;
|
|
396
|
-
const cellIndex = cell?.cellIndex ?? -1;
|
|
397
|
-
this.editor.innerHTML = value;
|
|
398
|
-
if (selection)
|
|
399
|
-
this.restoreTextSelection(selection);
|
|
400
|
-
const restoredCell = tableIndex >= 0 && rowIndex >= 0 && cellIndex >= 0
|
|
401
|
-
? this.editor.querySelectorAll("table")[tableIndex]?.rows[rowIndex]?.cells[cellIndex]
|
|
402
|
-
: undefined;
|
|
403
|
-
this.savedCell = restoredCell ?? this.selectedCell();
|
|
404
|
-
if (!selection && restoredCell) {
|
|
405
|
-
const range = document.createRange();
|
|
406
|
-
range.selectNodeContents(restoredCell);
|
|
407
|
-
range.collapse(true);
|
|
408
|
-
this.savedRange = range;
|
|
409
|
-
}
|
|
410
|
-
else if (!selection)
|
|
411
|
-
this.savedRange = undefined;
|
|
412
|
-
this.syncTableActions();
|
|
413
|
-
return value;
|
|
414
|
-
}
|
|
415
|
-
captureTextSelection() {
|
|
416
|
-
const selection = this.selection();
|
|
417
|
-
const anchorNode = selection?.anchorNode;
|
|
418
|
-
const focusNode = selection?.focusNode;
|
|
419
|
-
if (anchorNode && focusNode && this.editor.contains(anchorNode) && this.editor.contains(focusNode)) {
|
|
420
|
-
return { anchor: this.selectionPoint(anchorNode, selection.anchorOffset), focus: this.selectionPoint(focusNode, selection.focusOffset) };
|
|
421
|
-
}
|
|
422
|
-
const range = this.savedRange;
|
|
423
|
-
if (!range || !this.editor.contains(range.commonAncestorContainer))
|
|
424
|
-
return undefined;
|
|
425
|
-
return { anchor: this.selectionPoint(range.startContainer, range.startOffset), focus: this.selectionPoint(range.endContainer, range.endOffset) };
|
|
426
|
-
}
|
|
427
|
-
selectionPoint(node, nodeOffset) {
|
|
428
|
-
const path = [];
|
|
429
|
-
for (let current = node; current !== this.editor;) {
|
|
430
|
-
const parent = current.parentNode;
|
|
431
|
-
if (!parent)
|
|
432
|
-
break;
|
|
433
|
-
path.unshift(Array.from(parent.childNodes).findIndex(child => child === current));
|
|
434
|
-
current = parent;
|
|
435
|
-
}
|
|
436
|
-
return {
|
|
437
|
-
offset: this.textOffset(node, nodeOffset), path, nodeOffset,
|
|
438
|
-
...(node.nodeType === Node.TEXT_NODE ? { text: node.textContent ?? "" } : {}),
|
|
439
|
-
...(node instanceof Element ? { tag: node.tagName } : {})
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
textOffset(node, offset) {
|
|
443
|
-
const range = document.createRange();
|
|
444
|
-
range.selectNodeContents(this.editor);
|
|
445
|
-
range.setEnd(node, offset);
|
|
446
|
-
return range.toString().length;
|
|
447
|
-
}
|
|
448
|
-
textPoint(offset) {
|
|
449
|
-
const walker = document.createTreeWalker(this.editor, NodeFilter.SHOW_TEXT);
|
|
450
|
-
let remaining = Math.max(0, offset);
|
|
451
|
-
let last;
|
|
452
|
-
while (walker.nextNode()) {
|
|
453
|
-
const node = walker.currentNode;
|
|
454
|
-
const length = node.textContent?.length ?? 0;
|
|
455
|
-
last = node;
|
|
456
|
-
if (remaining <= length)
|
|
457
|
-
return { node, offset: remaining };
|
|
458
|
-
remaining -= length;
|
|
459
|
-
}
|
|
460
|
-
return last ? { node: last, offset: last.textContent?.length ?? 0 } : { node: this.editor, offset: 0 };
|
|
461
|
-
}
|
|
462
|
-
restoreTextSelection(snapshot) {
|
|
463
|
-
const anchor = this.restoreSelectionPoint(snapshot.anchor);
|
|
464
|
-
const focus = this.restoreSelectionPoint(snapshot.focus);
|
|
465
|
-
const selection = this.selection();
|
|
466
|
-
if (selection?.setBaseAndExtent) {
|
|
467
|
-
selection.setBaseAndExtent(anchor.node, anchor.offset, focus.node, focus.offset);
|
|
468
|
-
this.savedRange = selection.rangeCount ? selection.getRangeAt(0).cloneRange() : undefined;
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
const range = document.createRange();
|
|
472
|
-
const forward = snapshot.anchor.offset <= snapshot.focus.offset;
|
|
473
|
-
const start = forward ? anchor : focus;
|
|
474
|
-
const end = forward ? focus : anchor;
|
|
475
|
-
range.setStart(start.node, start.offset);
|
|
476
|
-
range.setEnd(end.node, end.offset);
|
|
477
|
-
selection?.removeAllRanges();
|
|
478
|
-
selection?.addRange(range);
|
|
479
|
-
this.savedRange = range.cloneRange();
|
|
480
|
-
}
|
|
481
|
-
restoreSelectionPoint(point) {
|
|
482
|
-
let node = this.editor;
|
|
483
|
-
for (const index of point.path)
|
|
484
|
-
node = node?.childNodes[index];
|
|
485
|
-
if (node && point.text !== undefined && node.nodeType === Node.TEXT_NODE && node.textContent === point.text) {
|
|
486
|
-
return { node, offset: Math.min(point.nodeOffset, point.text.length) };
|
|
487
|
-
}
|
|
488
|
-
if (node && point.text === undefined && node instanceof Element && node.tagName === point.tag && point.nodeOffset <= node.childNodes.length) {
|
|
489
|
-
return { node, offset: point.nodeOffset };
|
|
490
|
-
}
|
|
491
|
-
return this.textPoint(point.offset);
|
|
492
|
-
}
|
|
493
|
-
selectedCell() {
|
|
494
|
-
const node = this.savedRange?.startContainer;
|
|
495
|
-
const element = node instanceof Element ? node : node?.parentElement;
|
|
496
|
-
const cell = element?.closest("td,th");
|
|
497
|
-
if (cell && this.editor.contains(cell)) {
|
|
498
|
-
this.rememberCell(cell);
|
|
499
|
-
return cell;
|
|
500
|
-
}
|
|
501
|
-
if (this.savedCell && this.editor.contains(this.savedCell))
|
|
502
|
-
return this.savedCell;
|
|
503
|
-
const path = this.selectedTablePath;
|
|
504
|
-
return path ? this.editor.querySelectorAll("table")[path.table]?.rows[path.row]?.cells[path.cell] : undefined;
|
|
505
|
-
}
|
|
506
|
-
rememberCell(cell) {
|
|
507
|
-
const table = cell.closest("table");
|
|
508
|
-
this.selectedTablePath = table ? { table: [...this.editor.querySelectorAll("table")].indexOf(table), row: cell.parentElement.rowIndex, cell: cell.cellIndex } : undefined;
|
|
509
|
-
}
|
|
510
|
-
selection() {
|
|
511
|
-
const root = this.editor.getRootNode();
|
|
512
|
-
return root instanceof ShadowRoot ? root.getSelection?.() ?? this.editor.ownerDocument.getSelection() : this.editor.ownerDocument.getSelection();
|
|
513
|
-
}
|
|
514
|
-
onSelectionChange = () => {
|
|
515
|
-
const selection = this.selection();
|
|
516
|
-
let range = selection?.rangeCount ? selection.getRangeAt(0) : undefined;
|
|
517
|
-
if (!range || !this.editor.contains(range.commonAncestorContainer)) {
|
|
518
|
-
const InputEventConstructor = this.editor.ownerDocument.defaultView?.InputEvent;
|
|
519
|
-
const touchFirst = this.editor.ownerDocument.defaultView?.matchMedia("(pointer: coarse)").matches ?? false;
|
|
520
|
-
if (!touchFirst && this.editor.matches(":focus") && typeof InputEventConstructor?.prototype.getTargetRanges === "function" && !this.probingSelection) {
|
|
521
|
-
this.probedSelection = undefined;
|
|
522
|
-
this.probingSelection = true;
|
|
523
|
-
try {
|
|
524
|
-
this.editor.ownerDocument.execCommand("indent");
|
|
525
|
-
}
|
|
526
|
-
finally {
|
|
527
|
-
this.probingSelection = false;
|
|
528
|
-
}
|
|
529
|
-
range = this.probedSelection;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
if (!range || !this.editor.contains(range.commonAncestorContainer))
|
|
533
|
-
return;
|
|
534
|
-
if (range.collapsed && this.preserveToolbarSelection && this.savedRange && !this.savedRange.collapsed)
|
|
535
|
-
return;
|
|
536
|
-
if (!range.collapsed)
|
|
537
|
-
this.preserveToolbarSelection = false;
|
|
538
|
-
this.savedRange = range.cloneRange();
|
|
539
|
-
const node = range.startContainer;
|
|
540
|
-
const element = node instanceof Element ? node : node.parentElement;
|
|
541
|
-
const cell = element?.closest("td,th");
|
|
542
|
-
if (cell && this.editor.contains(cell)) {
|
|
543
|
-
this.savedCell = cell;
|
|
544
|
-
this.rememberCell(cell);
|
|
545
|
-
}
|
|
546
|
-
this.syncToolbarState();
|
|
547
|
-
this.syncTableActions();
|
|
548
|
-
};
|
|
549
|
-
syncToolbarState() {
|
|
550
|
-
const document = this.editor.ownerDocument;
|
|
551
|
-
const toolbar = this.shadowRoot?.querySelector('[role="toolbar"]');
|
|
552
|
-
if (!toolbar)
|
|
553
|
-
return;
|
|
554
|
-
for (const button of toolbar.querySelectorAll('button[data-command="bold"],button[data-command="italic"],button[data-command="underline"],button[data-command="strikeThrough"],button[data-command="subscript"],button[data-command="superscript"],button[data-command^="justify"],button[data-command="insertUnorderedList"],button[data-command="insertOrderedList"]')) {
|
|
555
|
-
try {
|
|
556
|
-
button.setAttribute("aria-pressed", String(document.queryCommandState(button.dataset.command)));
|
|
557
|
-
}
|
|
558
|
-
catch {
|
|
559
|
-
button.setAttribute("aria-pressed", "false");
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
const format = toolbar.querySelector('select[data-command="formatBlock"]');
|
|
563
|
-
if (format) {
|
|
564
|
-
try {
|
|
565
|
-
const block = document.queryCommandValue("formatBlock").replace(/[<>]/g, "").toUpperCase();
|
|
566
|
-
format.value = [...format.options].some(option => option.value === block) ? block : "P";
|
|
567
|
-
}
|
|
568
|
-
catch {
|
|
569
|
-
format.value = "P";
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
const selection = this.selection();
|
|
573
|
-
const node = selection?.anchorNode && this.editor.contains(selection.anchorNode) ? selection.anchorNode : this.savedRange?.startContainer;
|
|
574
|
-
const element = node instanceof Element ? node : node?.parentElement;
|
|
575
|
-
if (element && this.editor.contains(element)) {
|
|
576
|
-
const computed = this.editor.ownerDocument.defaultView?.getComputedStyle(element);
|
|
577
|
-
const family = toolbar.querySelector('select[data-command="fontFamily"]');
|
|
578
|
-
const size = toolbar.querySelector('select[data-command="fontSize"]');
|
|
579
|
-
if (family && computed)
|
|
580
|
-
family.value = [...family.options].some(option => option.value === computed.fontFamily) ? computed.fontFamily : "";
|
|
581
|
-
if (size && computed)
|
|
582
|
-
size.value = Object.entries(FONT_SIZE_PX).find(([, px]) => px === computed.fontSize)?.[0] ?? "";
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
onEditorClick = (event) => {
|
|
586
|
-
const cell = event.composedPath().find((node) => node instanceof HTMLTableCellElement && this.editor.contains(node));
|
|
587
|
-
if (cell) {
|
|
588
|
-
this.savedCell = cell;
|
|
589
|
-
this.rememberCell(cell);
|
|
590
|
-
}
|
|
591
|
-
this.onSelectionChange();
|
|
592
|
-
};
|
|
593
|
-
syncTableActions() {
|
|
594
|
-
const cell = this.selectedCell();
|
|
595
|
-
const table = cell?.closest("table");
|
|
596
|
-
const hasSpans = Boolean(table && [...table.querySelectorAll("td,th")].some(cell => cell.rowSpan !== 1 || cell.colSpan !== 1));
|
|
597
|
-
const rowCount = table?.rows.length ?? 0;
|
|
598
|
-
const columnCount = table ? Math.min(...[...table.rows].map(row => row.cells.length)) : 0;
|
|
599
|
-
const tableGroup = this.shadowRoot?.querySelector(".toolbar-group--table");
|
|
600
|
-
if (tableGroup)
|
|
601
|
-
tableGroup.hidden = this.contentMode === "restricted" || this.contentMode === "plain" || !cell || !table;
|
|
602
|
-
this.shadowRoot?.querySelectorAll("button[data-table-command]").forEach(button => {
|
|
603
|
-
const command = button.dataset.tableCommand;
|
|
604
|
-
button.disabled = !cell || !table || hasSpans || (command === "deleteRow" && rowCount <= 1) || (command === "deleteColumn" && columnCount <= 1);
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
restoreSelection() {
|
|
608
|
-
const selection = this.selection();
|
|
609
|
-
const liveRange = selection?.rangeCount ? selection.getRangeAt(0) : undefined;
|
|
610
|
-
if ((!liveRange || !this.editor.contains(liveRange.commonAncestorContainer)) && this.editor.matches(":focus"))
|
|
611
|
-
return;
|
|
612
|
-
const keepUnfocusedSelection = Boolean(this.savedRange && !this.savedRange.collapsed && !this.editor.matches(":focus"));
|
|
613
|
-
if (!keepUnfocusedSelection)
|
|
614
|
-
this.editor.focus();
|
|
615
|
-
if (!selection || !this.savedRange || !this.editor.contains(this.savedRange.commonAncestorContainer))
|
|
616
|
-
return;
|
|
617
|
-
selection.removeAllRanges();
|
|
618
|
-
selection.addRange(this.savedRange);
|
|
619
|
-
}
|
|
620
|
-
wrapUnfocusedSelection(range, createWrapper) {
|
|
621
|
-
const selectionRoot = range.commonAncestorContainer.nodeType === Node.TEXT_NODE
|
|
622
|
-
? range.commonAncestorContainer.parentNode ?? this.editor
|
|
623
|
-
: range.commonAncestorContainer;
|
|
624
|
-
const walker = this.editor.ownerDocument.createTreeWalker(selectionRoot, NodeFilter.SHOW_TEXT);
|
|
625
|
-
const nodes = [];
|
|
626
|
-
while (walker.nextNode()) {
|
|
627
|
-
const node = walker.currentNode;
|
|
628
|
-
if (range.intersectsNode(node))
|
|
629
|
-
nodes.push(node);
|
|
630
|
-
}
|
|
631
|
-
const wrappers = [];
|
|
632
|
-
for (const node of nodes.reverse()) {
|
|
633
|
-
const start = node === range.startContainer ? range.startOffset : 0;
|
|
634
|
-
const end = node === range.endContainer ? range.endOffset : node.length;
|
|
635
|
-
if (end <= start)
|
|
636
|
-
continue;
|
|
637
|
-
const part = this.editor.ownerDocument.createRange();
|
|
638
|
-
part.setStart(node, start);
|
|
639
|
-
part.setEnd(node, end);
|
|
640
|
-
const wrapper = createWrapper();
|
|
641
|
-
wrapper.append(part.extractContents());
|
|
642
|
-
part.insertNode(wrapper);
|
|
643
|
-
wrappers.push(wrapper);
|
|
644
|
-
}
|
|
645
|
-
if (!wrappers.length)
|
|
646
|
-
return false;
|
|
647
|
-
wrappers.reverse();
|
|
648
|
-
const selected = this.editor.ownerDocument.createRange();
|
|
649
|
-
selected.setStartBefore(wrappers[0]);
|
|
650
|
-
selected.setEndAfter(wrappers[wrappers.length - 1]);
|
|
651
|
-
const selection = this.selection();
|
|
652
|
-
selection?.removeAllRanges();
|
|
653
|
-
selection?.addRange(selected);
|
|
654
|
-
this.savedRange = selected.cloneRange();
|
|
655
|
-
return true;
|
|
656
|
-
}
|
|
657
|
-
applyStyleToUnfocusedSelection(range, property, value) {
|
|
658
|
-
return this.wrapUnfocusedSelection(range, () => {
|
|
659
|
-
const span = this.editor.ownerDocument.createElement("span");
|
|
660
|
-
span.style.setProperty(property, value);
|
|
661
|
-
return span;
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
applyInlineStyle(property, value) {
|
|
665
|
-
const editorFocused = this.editor.matches(":focus");
|
|
666
|
-
if (editorFocused)
|
|
667
|
-
this.restoreSelection();
|
|
668
|
-
const selection = this.selection();
|
|
669
|
-
const liveRange = selection?.rangeCount ? selection.getRangeAt(0) : undefined;
|
|
670
|
-
const range = liveRange && this.editor.contains(liveRange.commonAncestorContainer) ? liveRange : this.savedRange;
|
|
671
|
-
if (!range || range.collapsed || !this.editor.contains(range.commonAncestorContainer))
|
|
672
|
-
return;
|
|
673
|
-
if (!editorFocused) {
|
|
674
|
-
const cssValue = property === "font-size" ? FONT_SIZE_PX[value] ?? value : value;
|
|
675
|
-
if (this.applyStyleToUnfocusedSelection(range, property, cssValue))
|
|
676
|
-
this.commitEditorValue();
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
const command = property === "color" ? "foreColor" : property === "background-color" ? "hiliteColor" : property === "font-family" ? "fontName" : "fontSize";
|
|
680
|
-
const document = this.editor.ownerDocument;
|
|
681
|
-
document.execCommand("styleWithCSS", false, "true");
|
|
682
|
-
const applied = document.execCommand(command, false, value);
|
|
683
|
-
if (command === "hiliteColor" && !applied)
|
|
684
|
-
document.execCommand("backColor", false, value);
|
|
685
|
-
this.commitEditorValue();
|
|
686
|
-
}
|
|
687
|
-
editTable(command) {
|
|
688
|
-
const cell = this.selectedCell();
|
|
689
|
-
const table = cell?.closest("table");
|
|
690
|
-
const merged = Boolean(table && [...table.querySelectorAll("td,th")].some(item => item.rowSpan !== 1 || item.colSpan !== 1));
|
|
691
|
-
if (!cell || !table || merged)
|
|
692
|
-
return;
|
|
693
|
-
let focusCell;
|
|
694
|
-
if (command === "deleteTable") {
|
|
695
|
-
table.remove();
|
|
696
|
-
this.savedRange = undefined;
|
|
697
|
-
this.savedCell = undefined;
|
|
698
|
-
this.selectedTablePath = undefined;
|
|
699
|
-
}
|
|
700
|
-
else if (command === "insertRowAbove" || command === "insertRowBelow") {
|
|
701
|
-
const source = cell.parentElement;
|
|
702
|
-
const row = source.cloneNode(true);
|
|
703
|
-
for (const clone of [...row.cells]) {
|
|
704
|
-
clone.replaceChildren(document.createElement("br"));
|
|
705
|
-
}
|
|
706
|
-
source.parentElement?.insertBefore(row, command === "insertRowAbove" ? source : source.nextSibling);
|
|
707
|
-
focusCell = row.cells[cell.cellIndex];
|
|
708
|
-
}
|
|
709
|
-
else if (command === "insertColumnBefore" || command === "insertColumnAfter") {
|
|
710
|
-
const index = cell.cellIndex + (command === "insertColumnAfter" ? 1 : 0);
|
|
711
|
-
for (const row of [...table.rows]) {
|
|
712
|
-
const isHeader = row.parentElement?.tagName === "THEAD";
|
|
713
|
-
const next = document.createElement(isHeader ? "th" : "td");
|
|
714
|
-
if (isHeader)
|
|
715
|
-
next.setAttribute("scope", "col");
|
|
716
|
-
next.append(document.createElement("br"));
|
|
717
|
-
row.insertBefore(next, row.cells[index] ?? null);
|
|
718
|
-
if (row === cell.parentElement)
|
|
719
|
-
focusCell = next;
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
else if (command === "deleteRow") {
|
|
723
|
-
const row = cell.parentElement;
|
|
724
|
-
const index = row.rowIndex;
|
|
725
|
-
const cellIndex = cell.cellIndex;
|
|
726
|
-
row.remove();
|
|
727
|
-
const next = table.rows[Math.min(index, table.rows.length - 1)];
|
|
728
|
-
focusCell = next?.cells[Math.min(cellIndex, next.cells.length - 1)];
|
|
729
|
-
}
|
|
730
|
-
else if (command === "deleteColumn") {
|
|
731
|
-
const index = cell.cellIndex;
|
|
732
|
-
const row = cell.parentElement;
|
|
733
|
-
for (const row of [...table.rows])
|
|
734
|
-
row.cells[index]?.remove();
|
|
735
|
-
focusCell = row.cells[Math.min(index, row.cells.length - 1)];
|
|
736
|
-
}
|
|
737
|
-
if (focusCell) {
|
|
738
|
-
this.savedCell = focusCell;
|
|
739
|
-
this.rememberCell(focusCell);
|
|
740
|
-
const range = document.createRange();
|
|
741
|
-
range.selectNodeContents(focusCell);
|
|
742
|
-
range.collapse(true);
|
|
743
|
-
this.savedRange = range;
|
|
744
|
-
this.restoreSelection();
|
|
745
|
-
}
|
|
746
|
-
else
|
|
747
|
-
this.editor.focus();
|
|
748
|
-
this.commitEditorValue();
|
|
749
|
-
}
|
|
750
|
-
command(command, value) {
|
|
751
|
-
if ((this.contentMode === "plain" && !["undo", "redo"].includes(command)) || (this.contentMode === "restricted" && ["insertTable", "insertImage", "insertRowAbove", "insertRowBelow", "insertColumnBefore", "insertColumnAfter", "deleteRow", "deleteColumn", "deleteTable", "foreColor", "hiliteColor", "fontFamily", "fontSize"].includes(command)) || (this.contentMode !== "html" && ["subscript", "superscript", "justifyLeft", "justifyCenter", "justifyRight", "justifyFull"].includes(command)) || (command === "createLink" && !this.allowHyperlinks))
|
|
752
|
-
return;
|
|
753
|
-
if (command === "formatBlock" && !["P", "H1", "H2", "H3", "BLOCKQUOTE", "PRE"].includes(value?.toUpperCase() ?? ""))
|
|
754
|
-
return;
|
|
755
|
-
if (command === "insertImage") {
|
|
756
|
-
this.imageInput?.click();
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
if (["insertRowAbove", "insertRowBelow", "insertColumnBefore", "insertColumnAfter", "deleteRow", "deleteColumn", "deleteTable"].includes(command)) {
|
|
760
|
-
this.editTable(command);
|
|
761
|
-
return;
|
|
762
|
-
}
|
|
763
|
-
if (command === "createLink") {
|
|
764
|
-
this.openLinkEditor();
|
|
765
|
-
return;
|
|
766
|
-
}
|
|
767
|
-
const editorFocused = this.editor.matches(":focus");
|
|
768
|
-
this.restoreSelection();
|
|
769
|
-
if (command === "insertTable")
|
|
770
|
-
value = '<table><caption><br></caption><thead><tr><th scope="col"><br></th><th scope="col"><br></th></tr></thead><tbody><tr><td><br></td><td><br></td></tr><tr><td><br></td><td><br></td></tr></tbody></table><p><br></p>';
|
|
771
|
-
const document = this.editor.ownerDocument;
|
|
772
|
-
const applied = document.execCommand(command === "insertTable" ? "insertHTML" : command, false, command === "formatBlock" ? `<${value?.toLowerCase() ?? "p"}>` : value);
|
|
773
|
-
const tag = { bold: "strong", italic: "em", underline: "u", strikeThrough: "s" }[command];
|
|
774
|
-
if (!editorFocused && !applied && tag && this.savedRange && !this.savedRange.collapsed && this.wrapUnfocusedSelection(this.savedRange, () => document.createElement(tag))) {
|
|
775
|
-
this.commitEditorValue();
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
if (command === "hiliteColor" && !applied)
|
|
779
|
-
this.editor.ownerDocument.execCommand("backColor", false, value);
|
|
780
|
-
this.commitEditorValue();
|
|
781
|
-
}
|
|
782
|
-
linkAtRange(range) {
|
|
783
|
-
const node = range?.startContainer;
|
|
784
|
-
const element = node instanceof Element ? node : node?.parentElement;
|
|
785
|
-
const link = element?.closest("a");
|
|
786
|
-
return link && this.editor.contains(link) ? link : undefined;
|
|
787
|
-
}
|
|
788
|
-
openLinkEditor() {
|
|
789
|
-
if (!this.linkForm || this.formDisabled || this.hasAttribute("disabled") || this.hasAttribute("read-only"))
|
|
790
|
-
return;
|
|
791
|
-
const selection = this.selection();
|
|
792
|
-
const activeRange = selection?.rangeCount ? selection.getRangeAt(0) : undefined;
|
|
793
|
-
if (activeRange && this.editor.contains(activeRange.commonAncestorContainer))
|
|
794
|
-
this.savedRange = activeRange.cloneRange();
|
|
795
|
-
if (!this.savedRange || !this.editor.contains(this.savedRange.commonAncestorContainer)) {
|
|
796
|
-
const range = this.editor.ownerDocument.createRange();
|
|
797
|
-
range.selectNodeContents(this.editor);
|
|
798
|
-
range.collapse(false);
|
|
799
|
-
this.savedRange = range;
|
|
800
|
-
}
|
|
801
|
-
const link = this.linkAtRange(this.savedRange);
|
|
802
|
-
this.linkUrlInput.value = link?.getAttribute("href") ?? "";
|
|
803
|
-
this.linkTextInput.value = link?.textContent ?? this.savedRange.toString();
|
|
804
|
-
this.linkTitleInput.value = link?.getAttribute("title") ?? "";
|
|
805
|
-
this.linkNewTabInput.checked = link?.getAttribute("target") === "_blank";
|
|
806
|
-
this.linkRemoveButton.hidden = !link;
|
|
807
|
-
this.linkError.hidden = true;
|
|
808
|
-
this.linkUrlInput.removeAttribute("aria-invalid");
|
|
809
|
-
this.linkForm.hidden = false;
|
|
810
|
-
this.linkEditorVersion++;
|
|
811
|
-
this.linkUrlInput.focus();
|
|
812
|
-
this.linkUrlInput.select();
|
|
813
|
-
}
|
|
814
|
-
closeLinkEditor(restoreEditor) {
|
|
815
|
-
if (!this.linkForm)
|
|
816
|
-
return;
|
|
817
|
-
this.linkForm.hidden = true;
|
|
818
|
-
this.linkEditorVersion++;
|
|
819
|
-
this.linkError.hidden = true;
|
|
820
|
-
this.linkUrlInput.removeAttribute("aria-invalid");
|
|
821
|
-
if (restoreEditor)
|
|
822
|
-
this.restoreSelection();
|
|
823
|
-
else
|
|
824
|
-
this.shadowRoot?.querySelector('button[data-command="createLink"]')?.focus();
|
|
825
|
-
}
|
|
826
|
-
async browseLink() {
|
|
827
|
-
const picker = this.linkPickerValue;
|
|
828
|
-
if (!picker || this.pickingLink || this.formDisabled || this.hasAttribute("disabled") || this.hasAttribute("read-only"))
|
|
829
|
-
return;
|
|
830
|
-
const version = this.linkEditorVersion;
|
|
831
|
-
const existing = this.linkAtRange(this.savedRange);
|
|
832
|
-
const currentLink = existing ? {
|
|
833
|
-
href: existing.getAttribute("href") ?? "",
|
|
834
|
-
text: this.linkTextInput?.value ?? existing.textContent ?? "",
|
|
835
|
-
title: this.linkTitleInput?.value ?? existing.getAttribute("title") ?? "",
|
|
836
|
-
openInNewTab: this.linkNewTabInput?.checked ?? existing.getAttribute("target") === "_blank",
|
|
837
|
-
} : null;
|
|
838
|
-
this.pickingLink = true;
|
|
839
|
-
this.linkError.hidden = true;
|
|
840
|
-
this.syncLinkPickerButton();
|
|
841
|
-
try {
|
|
842
|
-
const result = await picker(currentLink);
|
|
843
|
-
if (!result || !this.isConnected || version !== this.linkEditorVersion || this.linkForm?.hidden)
|
|
844
|
-
return;
|
|
845
|
-
if (typeof result.href !== "string" || !safeHref(result.href))
|
|
846
|
-
throw new Error(this.getAttribute("link-invalid-message") ?? "Enter a valid HTTP, HTTPS, or mailto link.");
|
|
847
|
-
this.linkUrlInput.value = result.href;
|
|
848
|
-
if (typeof result.text === "string")
|
|
849
|
-
this.linkTextInput.value = result.text;
|
|
850
|
-
if (typeof result.title === "string")
|
|
851
|
-
this.linkTitleInput.value = result.title.slice(0, 512);
|
|
852
|
-
if (typeof result.openInNewTab === "boolean")
|
|
853
|
-
this.linkNewTabInput.checked = result.openInNewTab;
|
|
854
|
-
this.linkTextInput.focus();
|
|
855
|
-
}
|
|
856
|
-
catch (error) {
|
|
857
|
-
if (!this.isConnected || version !== this.linkEditorVersion || this.linkForm?.hidden)
|
|
858
|
-
return;
|
|
859
|
-
this.linkError.textContent = this.getAttribute("link-picker-error-message") ?? "Unable to open the SharePoint link picker.";
|
|
860
|
-
this.linkError.hidden = false;
|
|
861
|
-
this.dispatchEvent(new CustomEvent("spui-link-picker-error", { detail: { error }, bubbles: true, composed: true }));
|
|
862
|
-
}
|
|
863
|
-
finally {
|
|
864
|
-
this.pickingLink = false;
|
|
865
|
-
this.syncLinkPickerButton();
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
applyLink() {
|
|
869
|
-
const href = this.linkUrlInput.value.trim();
|
|
870
|
-
if (!safeHref(href)) {
|
|
871
|
-
this.linkError.textContent = this.getAttribute("link-invalid-message") ?? "Enter a valid HTTP, HTTPS, or mailto link.";
|
|
872
|
-
this.linkError.hidden = false;
|
|
873
|
-
this.linkUrlInput.setAttribute("aria-invalid", "true");
|
|
874
|
-
this.linkUrlInput.focus();
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
const normalizedHref = new URL(href, this.editor.ownerDocument.baseURI).href;
|
|
878
|
-
const displayText = this.linkTextInput.value.trim() || href;
|
|
879
|
-
const title = this.linkTitleInput.value.trim();
|
|
880
|
-
const range = this.savedRange?.cloneRange();
|
|
881
|
-
const existing = this.linkAtRange(range);
|
|
882
|
-
let link = existing;
|
|
883
|
-
if (existing) {
|
|
884
|
-
existing.setAttribute("href", normalizedHref);
|
|
885
|
-
}
|
|
886
|
-
else if (range?.collapsed) {
|
|
887
|
-
link = this.editor.ownerDocument.createElement("a");
|
|
888
|
-
link.href = normalizedHref;
|
|
889
|
-
range.insertNode(link);
|
|
890
|
-
}
|
|
891
|
-
else {
|
|
892
|
-
this.restoreSelection();
|
|
893
|
-
const created = this.editor.ownerDocument.execCommand("createLink", false, normalizedHref);
|
|
894
|
-
const selection = this.selection();
|
|
895
|
-
link = this.linkAtRange(range) ?? this.linkAtRange(selection?.rangeCount ? selection.getRangeAt(0) : undefined);
|
|
896
|
-
if (!created && !link && range && !range.collapsed) {
|
|
897
|
-
link = this.editor.ownerDocument.createElement("a");
|
|
898
|
-
link.href = normalizedHref;
|
|
899
|
-
link.append(range.extractContents());
|
|
900
|
-
range.insertNode(link);
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
if (link) {
|
|
904
|
-
if (link.textContent !== displayText)
|
|
905
|
-
link.textContent = displayText;
|
|
906
|
-
if (title)
|
|
907
|
-
link.setAttribute("title", title);
|
|
908
|
-
else
|
|
909
|
-
link.removeAttribute("title");
|
|
910
|
-
if (this.linkNewTabInput.checked) {
|
|
911
|
-
link.setAttribute("target", "_blank");
|
|
912
|
-
link.setAttribute("rel", "noopener noreferrer");
|
|
913
|
-
}
|
|
914
|
-
else {
|
|
915
|
-
link.removeAttribute("target");
|
|
916
|
-
link.removeAttribute("rel");
|
|
917
|
-
}
|
|
918
|
-
const caret = this.editor.ownerDocument.createRange();
|
|
919
|
-
caret.selectNodeContents(link);
|
|
920
|
-
caret.collapse(false);
|
|
921
|
-
this.savedRange = caret;
|
|
922
|
-
}
|
|
923
|
-
this.closeLinkEditor(true);
|
|
924
|
-
this.commitEditorValue();
|
|
925
|
-
}
|
|
926
|
-
removeLink() {
|
|
927
|
-
const link = this.linkAtRange(this.savedRange);
|
|
928
|
-
if (!link)
|
|
929
|
-
return;
|
|
930
|
-
link.replaceWith(...link.childNodes);
|
|
931
|
-
this.closeLinkEditor(true);
|
|
932
|
-
this.commitEditorValue();
|
|
933
|
-
}
|
|
934
|
-
async insertUploadedImage(file) {
|
|
935
|
-
if (!this.imageUploader || !file.type.startsWith("image/") || file.size > 10 * 1024 * 1024 || this.uploadingImage)
|
|
936
|
-
return;
|
|
937
|
-
this.uploadingImage = true;
|
|
938
|
-
this.syncImageButton();
|
|
939
|
-
try {
|
|
940
|
-
const uploaded = await this.imageUploader(file);
|
|
941
|
-
const src = typeof uploaded === "string" ? uploaded : uploaded.src;
|
|
942
|
-
if (typeof uploaded !== "string" && typeof uploaded.alt !== "string")
|
|
943
|
-
throw new Error("Image upload results must include alt text; use an empty string only for decorative images.");
|
|
944
|
-
if (!safeImageSrc(src))
|
|
945
|
-
throw new Error("Image upload must return an HTTP or HTTPS URL.");
|
|
946
|
-
this.restoreSelection();
|
|
947
|
-
const image = document.createElement("img");
|
|
948
|
-
image.src = new URL(src, document.baseURI).href;
|
|
949
|
-
image.alt = (typeof uploaded === "string" ? file.name.replace(/\.[^.]*$/, "") : uploaded.alt).slice(0, 200);
|
|
950
|
-
const selection = this.selection();
|
|
951
|
-
const range = selection?.rangeCount ? selection.getRangeAt(0) : undefined;
|
|
952
|
-
if (range && this.editor.contains(range.commonAncestorContainer)) {
|
|
953
|
-
range.deleteContents();
|
|
954
|
-
range.insertNode(image);
|
|
955
|
-
range.setStartAfter(image);
|
|
956
|
-
range.collapse(true);
|
|
957
|
-
selection.removeAllRanges();
|
|
958
|
-
selection.addRange(range);
|
|
959
|
-
}
|
|
960
|
-
else
|
|
961
|
-
this.editor.append(image);
|
|
962
|
-
this.savedRange = range?.cloneRange();
|
|
963
|
-
this.commitEditorValue();
|
|
964
|
-
}
|
|
965
|
-
catch (error) {
|
|
966
|
-
this.dispatchEvent(new CustomEvent("spui-image-upload-error", { detail: { fileName: file.name, error }, bubbles: true, composed: true }));
|
|
967
|
-
}
|
|
968
|
-
finally {
|
|
969
|
-
this.uploadingImage = false;
|
|
970
|
-
this.syncImageButton();
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
render() {
|
|
974
|
-
const shadow = this.shadowRoot ?? this.attachShadow({ mode: "open" });
|
|
975
|
-
const label = document.createElement("label");
|
|
976
|
-
label.textContent = this.getAttribute("label") ?? "";
|
|
977
|
-
label.hidden = !this.hasAttribute("label");
|
|
978
|
-
label.part.add("label");
|
|
979
|
-
const toolbar = document.createElement("div");
|
|
980
|
-
toolbar.setAttribute("role", "toolbar");
|
|
981
|
-
toolbar.setAttribute("aria-orientation", "horizontal");
|
|
982
|
-
toolbar.part.add("toolbar");
|
|
983
|
-
toolbar.addEventListener("pointerdown", () => {
|
|
984
|
-
const selection = this.selection();
|
|
985
|
-
const range = selection?.rangeCount ? selection.getRangeAt(0) : undefined;
|
|
986
|
-
if (range && !range.collapsed && this.editor.contains(range.commonAncestorContainer)) {
|
|
987
|
-
this.savedRange = range.cloneRange();
|
|
988
|
-
this.preserveToolbarSelection = true;
|
|
989
|
-
}
|
|
990
|
-
}, true);
|
|
991
|
-
const format = document.createElement("select");
|
|
992
|
-
format.className = "toolbar-format";
|
|
993
|
-
format.dataset.command = "formatBlock";
|
|
994
|
-
format.dataset.labelAttribute = "format-block-label";
|
|
995
|
-
format.setAttribute("aria-label", this.getAttribute("format-block-label") ?? "Text style");
|
|
996
|
-
for (const [value, attribute, fallback] of [["P", "paragraph-label", "Paragraph"], ["H1", "heading-1-label", "Heading 1"], ["H2", "heading-2-label", "Heading 2"], ["H3", "heading-3-label", "Heading 3"], ["BLOCKQUOTE", "blockquote-label", "Quote"], ["PRE", "preformatted-label", "Preformatted"]]) {
|
|
997
|
-
const option = document.createElement("option");
|
|
998
|
-
option.value = value;
|
|
999
|
-
option.dataset.labelAttribute = attribute;
|
|
1000
|
-
option.textContent = this.getAttribute(attribute) ?? fallback;
|
|
1001
|
-
format.append(option);
|
|
1002
|
-
}
|
|
1003
|
-
format.addEventListener("change", () => this.command("formatBlock", format.value));
|
|
1004
|
-
toolbar.append(format);
|
|
1005
|
-
const fontGroup = document.createElement("div");
|
|
1006
|
-
fontGroup.className = "toolbar-group toolbar-group--fonts";
|
|
1007
|
-
const addFontSelect = (command, labelAttribute, fallback, options) => {
|
|
1008
|
-
const select = document.createElement("select");
|
|
1009
|
-
select.className = "toolbar-format toolbar-font-select";
|
|
1010
|
-
select.dataset.command = command;
|
|
1011
|
-
select.dataset.labelAttribute = labelAttribute;
|
|
1012
|
-
select.setAttribute("aria-label", this.getAttribute(labelAttribute) ?? fallback);
|
|
1013
|
-
for (const [value, text] of options) {
|
|
1014
|
-
const option = document.createElement("option");
|
|
1015
|
-
option.value = value;
|
|
1016
|
-
option.textContent = text;
|
|
1017
|
-
if (!value) {
|
|
1018
|
-
option.disabled = true;
|
|
1019
|
-
option.selected = true;
|
|
1020
|
-
}
|
|
1021
|
-
select.append(option);
|
|
1022
|
-
}
|
|
1023
|
-
select.addEventListener("change", () => this.applyInlineStyle(command === "fontFamily" ? "font-family" : "font-size", select.value));
|
|
1024
|
-
fontGroup.append(select);
|
|
1025
|
-
};
|
|
1026
|
-
addFontSelect("fontFamily", "font-family-label", "Font family", [["", "Font family"], ["Arial", "Arial"], ["Calibri", "Calibri"], ["Georgia", "Georgia"], ["Segoe UI", "Segoe UI"], ["Tahoma", "Tahoma"], ["Times New Roman", "Times New Roman"], ["Verdana", "Verdana"]]);
|
|
1027
|
-
addFontSelect("fontSize", "font-size-label", "Font size", [["", "Font size"], ["1", "10 px"], ["2", "12 px"], ["3", "16 px"], ["4", "18 px"], ["5", "24 px"], ["6", "32 px"], ["7", "48 px"]]);
|
|
1028
|
-
toolbar.append(fontGroup);
|
|
1029
|
-
const actions = [["Bold", "bold"], ["Italic", "italic"], ["Underline", "underline"], ["Strikethrough", "strikeThrough", undefined, undefined, "strike-label"], ["Subscript", "subscript", undefined, undefined, "subscript-label"], ["Superscript", "superscript", undefined, undefined, "superscript-label"], ["Align left", "justifyLeft", undefined, undefined, "align-left-label"], ["Align center", "justifyCenter", undefined, undefined, "align-center-label"], ["Align right", "justifyRight", undefined, undefined, "align-right-label"], ["Justify text", "justifyFull", undefined, undefined, "align-justify-label"], ["Bulleted list", "insertUnorderedList", undefined, "• List"], ["Numbered list", "insertOrderedList", undefined, "1. List"], ["Clear formatting", "removeFormat", undefined, undefined, "clear-formatting-label"], ["Undo", "undo"], ["Redo", "redo"], ["Insert table", "insertTable"], ["Insert image", "insertImage", undefined, undefined, "insert-image-label"], ["Insert row above", "insertRowAbove", undefined, undefined, "insert-row-above-label"], ["Insert row below", "insertRowBelow", undefined, undefined, "insert-row-below-label"], ["Insert column before", "insertColumnBefore", undefined, undefined, "insert-column-before-label"], ["Insert column after", "insertColumnAfter", undefined, undefined, "insert-column-after-label"], ["Delete row", "deleteRow", undefined, undefined, "delete-row-label"], ["Delete column", "deleteColumn", undefined, undefined, "delete-column-label"], ["Delete table", "deleteTable", undefined, undefined, "delete-table-label"], ["Link", "createLink"]];
|
|
1030
|
-
const toolbarGroups = new Map();
|
|
1031
|
-
for (const [title, command, value, , labelAttribute] of actions) {
|
|
1032
|
-
const isTableAction = command.startsWith("insertRow") || command.startsWith("insertColumn") || command.startsWith("delete");
|
|
1033
|
-
const groupName = isTableAction ? "table" : ["bold", "italic", "underline", "strikeThrough", "subscript", "superscript", "removeFormat"].includes(command) ? "formatting" : command.startsWith("justify") ? "paragraph" : ["insertUnorderedList", "insertOrderedList"].includes(command) ? "lists" : ["undo", "redo"].includes(command) ? "history" : "insert";
|
|
1034
|
-
let group = toolbarGroups.get(groupName);
|
|
1035
|
-
if (!group) {
|
|
1036
|
-
group = document.createElement("div");
|
|
1037
|
-
group.className = `toolbar-group toolbar-group--${groupName}`;
|
|
1038
|
-
if (groupName === "table")
|
|
1039
|
-
group.hidden = true;
|
|
1040
|
-
toolbarGroups.set(groupName, group);
|
|
1041
|
-
toolbar.append(group);
|
|
1042
|
-
}
|
|
1043
|
-
const button = document.createElement("button");
|
|
1044
|
-
button.type = "button";
|
|
1045
|
-
button.dataset.command = command;
|
|
1046
|
-
button.dataset.fallback = title;
|
|
1047
|
-
if (labelAttribute)
|
|
1048
|
-
button.dataset.labelAttribute = labelAttribute;
|
|
1049
|
-
if (isTableAction)
|
|
1050
|
-
button.dataset.tableCommand = command;
|
|
1051
|
-
button.tabIndex = toolbar.querySelector("button[data-command],select[data-command]") ? -1 : 0;
|
|
1052
|
-
button.append(createToolbarIcon(command));
|
|
1053
|
-
button.setAttribute("aria-label", title);
|
|
1054
|
-
button.title = title;
|
|
1055
|
-
button.addEventListener("mousedown", event => event.preventDefault());
|
|
1056
|
-
button.addEventListener("click", () => this.command(command, value));
|
|
1057
|
-
group.append(button);
|
|
1058
|
-
}
|
|
1059
|
-
const colorGroup = document.createElement("div");
|
|
1060
|
-
colorGroup.className = "toolbar-group toolbar-group--color";
|
|
1061
|
-
for (const [attribute, fallback, command, color, colors] of [
|
|
1062
|
-
["text-color-label", "Text color", "foreColor", "#323130", ["#000000", "#323130", "#605e5c", "#a4262c", "#d83b01", "#ca5010", "#8e562e", "#498205", "#038387", "#0078d4", "#5c2d91", "#b4009e"]],
|
|
1063
|
-
["highlight-color-label", "Highlight color", "hiliteColor", "#fff2cc", ["#fff2cc", "#fff4ce", "#fed9cc", "#fce100", "#e2f2d5", "#c8e6e0", "#cfe4fa", "#e7d8f5", "#f4d5ef", "#f3f2f1", "#d2d0ce", "#ffffff"]]
|
|
1064
|
-
]) {
|
|
1065
|
-
const picker = document.createElement("details");
|
|
1066
|
-
picker.className = "toolbar-color";
|
|
1067
|
-
picker.dataset.labelAttribute = attribute;
|
|
1068
|
-
picker.dataset.command = command;
|
|
1069
|
-
picker.part.add(command === "foreColor" ? "text-color" : "highlight-color");
|
|
1070
|
-
const summary = document.createElement("summary");
|
|
1071
|
-
summary.setAttribute("aria-label", this.getAttribute(attribute) ?? fallback);
|
|
1072
|
-
summary.title = this.getAttribute(attribute) ?? fallback;
|
|
1073
|
-
const glyph = document.createElement("span");
|
|
1074
|
-
glyph.className = "toolbar-color__glyph";
|
|
1075
|
-
glyph.setAttribute("aria-hidden", "true");
|
|
1076
|
-
glyph.textContent = command === "foreColor" ? "A" : "▰";
|
|
1077
|
-
const swatch = document.createElement("span");
|
|
1078
|
-
swatch.className = "toolbar-color__current";
|
|
1079
|
-
swatch.style.setProperty("--spui-picked-color", color);
|
|
1080
|
-
swatch.setAttribute("aria-hidden", "true");
|
|
1081
|
-
const name = document.createElement("span");
|
|
1082
|
-
name.className = "toolbar-color__name";
|
|
1083
|
-
name.textContent = this.getAttribute(attribute) ?? fallback;
|
|
1084
|
-
summary.append(glyph, swatch, name);
|
|
1085
|
-
summary.addEventListener("mousedown", this.onSelectionChange);
|
|
1086
|
-
picker.append(summary);
|
|
1087
|
-
const palette = document.createElement("div");
|
|
1088
|
-
palette.className = "toolbar-color__palette";
|
|
1089
|
-
palette.setAttribute("role", "group");
|
|
1090
|
-
palette.setAttribute("aria-label", this.getAttribute(attribute) ?? fallback);
|
|
1091
|
-
for (const value of ["theme-accent", ...colors]) {
|
|
1092
|
-
const isThemeAccent = value === "theme-accent";
|
|
1093
|
-
const option = document.createElement("button");
|
|
1094
|
-
option.type = "button";
|
|
1095
|
-
option.className = "toolbar-color__swatch";
|
|
1096
|
-
option.style.setProperty("--spui-picked-color", isThemeAccent ? "var(--spui-ui-accent,#0078d4)" : value);
|
|
1097
|
-
option.dataset.color = value;
|
|
1098
|
-
option.addEventListener("mousedown", this.onSelectionChange);
|
|
1099
|
-
option.addEventListener("click", () => { const selected = isThemeAccent ? getComputedStyle(this).getPropertyValue("--spui-ui-accent").trim() || "#0078d4" : value; swatch.style.setProperty("--spui-picked-color", selected); this.command(command, selected); picker.open = false; });
|
|
1100
|
-
palette.append(option);
|
|
1101
|
-
}
|
|
1102
|
-
const custom = document.createElement("label");
|
|
1103
|
-
custom.className = "toolbar-color__custom";
|
|
1104
|
-
custom.dataset.labelAttribute = "custom-color-label";
|
|
1105
|
-
custom.append(document.createTextNode(this.getAttribute("custom-color-label") ?? "Custom color"));
|
|
1106
|
-
const input = document.createElement("input");
|
|
1107
|
-
input.type = "color";
|
|
1108
|
-
input.value = color;
|
|
1109
|
-
input.dataset.command = command;
|
|
1110
|
-
input.dataset.labelAttribute = attribute;
|
|
1111
|
-
input.setAttribute("aria-label", `${this.getAttribute(attribute) ?? fallback}: ${this.getAttribute("custom-color-label") ?? "custom color"}`);
|
|
1112
|
-
input.addEventListener("mousedown", this.onSelectionChange);
|
|
1113
|
-
input.addEventListener("input", () => swatch.style.setProperty("--spui-picked-color", input.value));
|
|
1114
|
-
input.addEventListener("change", () => { swatch.style.setProperty("--spui-picked-color", input.value); this.command(command, input.value); picker.open = false; });
|
|
1115
|
-
custom.append(input);
|
|
1116
|
-
palette.append(custom);
|
|
1117
|
-
picker.append(palette);
|
|
1118
|
-
colorGroup.append(picker);
|
|
1119
|
-
}
|
|
1120
|
-
toolbar.append(colorGroup);
|
|
1121
|
-
this.imageInput = document.createElement("input");
|
|
1122
|
-
this.imageInput.type = "file";
|
|
1123
|
-
this.imageInput.accept = "image/*";
|
|
1124
|
-
this.imageInput.hidden = true;
|
|
1125
|
-
this.imageInput.setAttribute("aria-label", this.getAttribute("image-input-label") ?? "Choose image");
|
|
1126
|
-
this.imageInput.addEventListener("change", () => { const file = this.imageInput?.files?.[0]; if (file)
|
|
1127
|
-
void this.insertUploadedImage(file); this.imageInput.value = ""; });
|
|
1128
|
-
toolbar.append(this.imageInput);
|
|
1129
|
-
const linkForm = document.createElement("form");
|
|
1130
|
-
this.linkForm = linkForm;
|
|
1131
|
-
linkForm.className = "link-editor";
|
|
1132
|
-
linkForm.part.add("link-editor");
|
|
1133
|
-
linkForm.hidden = true;
|
|
1134
|
-
linkForm.setAttribute("aria-label", this.getAttribute("link-dialog-label") ?? "Edit link");
|
|
1135
|
-
const makeLinkField = (attribute, fallback, type = "text") => {
|
|
1136
|
-
const field = document.createElement("label");
|
|
1137
|
-
field.className = "link-field";
|
|
1138
|
-
field.dataset.labelAttribute = attribute;
|
|
1139
|
-
field.append(document.createTextNode(this.getAttribute(attribute) ?? fallback));
|
|
1140
|
-
const input = document.createElement("input");
|
|
1141
|
-
input.type = type;
|
|
1142
|
-
input.part.add(attribute === "link-prompt" ? "link-url" : attribute === "link-text-label" ? "link-text" : "link-title");
|
|
1143
|
-
field.append(input);
|
|
1144
|
-
linkForm.append(field);
|
|
1145
|
-
return input;
|
|
1146
|
-
};
|
|
1147
|
-
this.linkUrlInput = makeLinkField("link-prompt", "Link URL");
|
|
1148
|
-
this.linkUrlInput.inputMode = "url";
|
|
1149
|
-
this.linkUrlInput.setAttribute("autocomplete", "url");
|
|
1150
|
-
this.linkUrlInput.placeholder = "https://";
|
|
1151
|
-
this.linkTextInput = makeLinkField("link-text-label", "Display text");
|
|
1152
|
-
this.linkTitleInput = makeLinkField("link-title-label", "Title");
|
|
1153
|
-
this.linkTitleInput.maxLength = 512;
|
|
1154
|
-
const newTabLabel = document.createElement("label");
|
|
1155
|
-
newTabLabel.className = "link-new-tab";
|
|
1156
|
-
newTabLabel.part.add("link-new-tab");
|
|
1157
|
-
newTabLabel.dataset.labelAttribute = "link-new-tab-label";
|
|
1158
|
-
this.linkNewTabInput = document.createElement("input");
|
|
1159
|
-
this.linkNewTabInput.type = "checkbox";
|
|
1160
|
-
this.linkNewTabInput.part.add("link-new-tab-checkbox");
|
|
1161
|
-
const newTabText = document.createElement("span");
|
|
1162
|
-
newTabText.textContent = this.getAttribute("link-new-tab-label") ?? "Open in new tab";
|
|
1163
|
-
newTabLabel.append(this.linkNewTabInput, newTabText);
|
|
1164
|
-
linkForm.append(newTabLabel);
|
|
1165
|
-
this.linkError = document.createElement("div");
|
|
1166
|
-
this.linkError.className = "link-error";
|
|
1167
|
-
this.linkError.part.add("link-error");
|
|
1168
|
-
this.linkError.id = `rich-text-link-error-${crypto.randomUUID()}`;
|
|
1169
|
-
this.linkError.setAttribute("role", "alert");
|
|
1170
|
-
this.linkError.hidden = true;
|
|
1171
|
-
linkForm.append(this.linkError);
|
|
1172
|
-
this.linkPickerStatus = document.createElement("div");
|
|
1173
|
-
this.linkPickerStatus.className = "link-picker-status";
|
|
1174
|
-
this.linkPickerStatus.setAttribute("role", "status");
|
|
1175
|
-
this.linkPickerStatus.setAttribute("aria-live", "polite");
|
|
1176
|
-
linkForm.append(this.linkPickerStatus);
|
|
1177
|
-
this.linkUrlInput.setAttribute("aria-describedby", this.linkError.id);
|
|
1178
|
-
const linkActions = document.createElement("div");
|
|
1179
|
-
linkActions.className = "link-actions";
|
|
1180
|
-
const makeLinkAction = (action, attribute, fallback, onClick) => {
|
|
1181
|
-
const button = document.createElement("button");
|
|
1182
|
-
button.type = action === "apply" ? "submit" : "button";
|
|
1183
|
-
button.dataset.linkAction = action;
|
|
1184
|
-
button.dataset.labelAttribute = attribute;
|
|
1185
|
-
button.part.add(`link-${action}`);
|
|
1186
|
-
button.textContent = this.getAttribute(attribute) ?? fallback;
|
|
1187
|
-
button.addEventListener("click", onClick);
|
|
1188
|
-
linkActions.append(button);
|
|
1189
|
-
return button;
|
|
1190
|
-
};
|
|
1191
|
-
makeLinkAction("apply", "link-apply-label", "Apply link", () => { });
|
|
1192
|
-
this.linkRemoveButton = makeLinkAction("remove", "link-remove-label", "Remove link", () => this.removeLink());
|
|
1193
|
-
this.linkPickerButton = makeLinkAction("browse", "link-picker-label", "Browse SharePoint", () => { void this.browseLink(); });
|
|
1194
|
-
makeLinkAction("cancel", "link-cancel-label", "Cancel", () => this.closeLinkEditor(false));
|
|
1195
|
-
linkForm.append(linkActions);
|
|
1196
|
-
linkForm.addEventListener("submit", event => { event.preventDefault(); this.applyLink(); });
|
|
1197
|
-
linkForm.addEventListener("keydown", event => { if (event.key === "Escape") {
|
|
1198
|
-
event.preventDefault();
|
|
1199
|
-
event.stopPropagation();
|
|
1200
|
-
this.closeLinkEditor(false);
|
|
1201
|
-
} });
|
|
1202
|
-
toolbar.querySelectorAll("button[data-command],select[data-command],input[type=color][data-command]").forEach((control, index) => { control.tabIndex = index === 0 ? 0 : -1; });
|
|
1203
|
-
toolbar.addEventListener("focusin", event => { const active = event.target; toolbar.querySelectorAll("button[data-command],select[data-command],input[type=color][data-command]").forEach(control => { control.tabIndex = control === active ? 0 : -1; }); });
|
|
1204
|
-
toolbar.addEventListener("keydown", event => {
|
|
1205
|
-
const key = event.key;
|
|
1206
|
-
if ((event.target instanceof HTMLSelectElement || event.target instanceof HTMLInputElement) && ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(key))
|
|
1207
|
-
return;
|
|
1208
|
-
const controls = [...toolbar.querySelectorAll("button[data-command]:not(:disabled):not([hidden]),select[data-command]:not(:disabled):not([hidden]),input[type=color][data-command]:not(:disabled):not([hidden])")];
|
|
1209
|
-
const current = controls.indexOf(event.target);
|
|
1210
|
-
const rtl = getComputedStyle(this).direction === "rtl";
|
|
1211
|
-
const step = key === "ArrowRight" ? (rtl ? -1 : 1) : key === "ArrowLeft" ? (rtl ? 1 : -1) : 0;
|
|
1212
|
-
const index = key === "Home" ? 0 : key === "End" ? controls.length - 1 : step ? (current + step + controls.length) % controls.length : -1;
|
|
1213
|
-
if (index < 0 || !controls[index])
|
|
1214
|
-
return;
|
|
1215
|
-
event.preventDefault();
|
|
1216
|
-
controls[index].tabIndex = 0;
|
|
1217
|
-
controls.forEach((control, controlIndex) => { if (controlIndex !== index)
|
|
1218
|
-
control.tabIndex = -1; });
|
|
1219
|
-
controls[index].focus();
|
|
1220
|
-
});
|
|
1221
|
-
this.editor = document.createElement("div");
|
|
1222
|
-
this.editor.setAttribute("role", "textbox");
|
|
1223
|
-
this.editor.setAttribute("aria-multiline", "true");
|
|
1224
|
-
this.syncEditorLabel();
|
|
1225
|
-
const invalid = this.getAttribute("aria-invalid");
|
|
1226
|
-
if (invalid !== null)
|
|
1227
|
-
this.editor.setAttribute("aria-invalid", invalid);
|
|
1228
|
-
this.editor.contentEditable = this.hasAttribute("disabled") || this.hasAttribute("read-only") ? "false" : this.contentMode === "plain" ? "plaintext-only" : "true";
|
|
1229
|
-
this.editor.setAttribute("aria-readonly", String(this.hasAttribute("read-only")));
|
|
1230
|
-
this.editor.setAttribute("aria-disabled", String(this.hasAttribute("disabled")));
|
|
1231
|
-
this.editor.setAttribute("aria-required", String(this.hasAttribute("required")));
|
|
1232
|
-
this.editor.dataset.placeholder = this.getAttribute("placeholder") ?? "Write something…";
|
|
1233
|
-
this.editor.part.add("editor");
|
|
1234
|
-
this.editor.addEventListener("keydown", event => { const key = event.key.toLowerCase(); const shortcut = event.metaKey || event.ctrlKey; if (!shortcut)
|
|
1235
|
-
return; if (key === "z") {
|
|
1236
|
-
event.preventDefault();
|
|
1237
|
-
this.command(event.shiftKey ? "redo" : "undo");
|
|
1238
|
-
}
|
|
1239
|
-
else if (key === "y") {
|
|
1240
|
-
event.preventDefault();
|
|
1241
|
-
this.command("redo");
|
|
1242
|
-
} });
|
|
1243
|
-
this.editor.addEventListener("beforeinput", event => {
|
|
1244
|
-
if (!this.probingSelection)
|
|
1245
|
-
return;
|
|
1246
|
-
const targetRange = event.getTargetRanges?.()[0];
|
|
1247
|
-
if (targetRange && this.editor.contains(targetRange.startContainer) && this.editor.contains(targetRange.endContainer)) {
|
|
1248
|
-
const range = this.editor.ownerDocument.createRange();
|
|
1249
|
-
range.setStart(targetRange.startContainer, targetRange.startOffset);
|
|
1250
|
-
range.setEnd(targetRange.endContainer, targetRange.endOffset);
|
|
1251
|
-
this.probedSelection = range;
|
|
1252
|
-
}
|
|
1253
|
-
event.preventDefault();
|
|
1254
|
-
event.stopImmediatePropagation();
|
|
1255
|
-
}, true);
|
|
1256
|
-
this.editor.addEventListener("input", () => this.commitEditorValue());
|
|
1257
|
-
this.editor.addEventListener("pointerdown", event => {
|
|
1258
|
-
this.preserveToolbarSelection = false;
|
|
1259
|
-
});
|
|
1260
|
-
this.editor.addEventListener("mouseup", this.onSelectionChange);
|
|
1261
|
-
this.editor.addEventListener("keyup", this.onSelectionChange);
|
|
1262
|
-
this.editor.addEventListener("click", this.onEditorClick);
|
|
1263
|
-
this.editor.addEventListener("paste", event => {
|
|
1264
|
-
event.preventDefault();
|
|
1265
|
-
const clipboard = event.clipboardData;
|
|
1266
|
-
if (this.contentMode === "plain" || this.contentMode === "restricted") {
|
|
1267
|
-
this.editor.ownerDocument.execCommand("insertText", false, clipboard?.getData("text/plain") ?? plainText(clipboard?.getData("text/html") ?? ""));
|
|
1268
|
-
this.commitEditorValue();
|
|
1269
|
-
return;
|
|
1270
|
-
}
|
|
1271
|
-
const imageFile = [...(clipboard?.files ?? [])].find(file => file.type.startsWith("image/"));
|
|
1272
|
-
if (imageFile) {
|
|
1273
|
-
void this.insertUploadedImage(imageFile);
|
|
1274
|
-
return;
|
|
1275
|
-
}
|
|
1276
|
-
const html = cleanHtml(clipboard?.getData("text/html") ?? clipboard?.getData("text/plain") ?? "", this.contentMode, this.allowHyperlinks);
|
|
1277
|
-
const text = clipboard?.getData("text/plain") ?? "";
|
|
1278
|
-
if (html)
|
|
1279
|
-
this.editor.ownerDocument.execCommand("insertHTML", false, html);
|
|
1280
|
-
else
|
|
1281
|
-
this.editor.ownerDocument.execCommand("insertText", false, text);
|
|
1282
|
-
this.commitEditorValue();
|
|
1283
|
-
});
|
|
1284
|
-
this.editor.addEventListener("blur", () => this.commitEditorValue("spui-change"));
|
|
1285
|
-
const style = document.createElement("style");
|
|
1286
|
-
style.textContent = `${base}:host{display:grid;min-width:0;max-width:100%;container-type:inline-size;gap:6px}label{font-weight:600;color:var(--spui-ui-muted,var(--spui-semantic-body-text,inherit))}[role=toolbar]{display:flex;flex-wrap:wrap;align-items:center;gap:4px;padding:6px;border:1px solid var(--spui-control-border,var(--spui-ui-stroke,#c8c8c8));border-radius:var(--spui-control-radius,6px);background:var(--spui-ui-surface,#fff)}[role=toolbar][hidden],button[hidden]{display:none}.toolbar-group{display:flex;flex:0 1 auto;flex-wrap:wrap;align-items:center;gap:2px;min-width:0;padding-inline-start:6px;border-inline-start:1px solid var(--spui-ui-stroke,#c8c8c8)}.toolbar-group[hidden]{display:none}button{box-sizing:border-box;display:inline-grid;place-items:center;flex:none;width:36px;min-width:36px;min-height:36px;padding:0;border:0;border-radius:var(--spui-control-radius,4px);background:transparent;color:var(--spui-ui-text,inherit);font:inherit;cursor:pointer}.toolbar-format{box-sizing:border-box;max-width:172px;min-width:116px;min-height:36px;padding:0 8px;border:1px solid var(--spui-control-border,var(--spui-ui-stroke,#c8c8c8));border-radius:var(--spui-control-radius,4px);background:var(--spui-control-background,var(--spui-ui-surface,#fff));color:var(--spui-ui-text,inherit);font:inherit}.toolbar-format:hover{background:var(--spui-ui-surface-hover,#eee)}button svg{display:block;width:18px;height:18px}button:hover:not(:disabled){background:var(--spui-ui-surface-hover,#eee)}button[aria-pressed=true]{background:color-mix(in srgb,var(--spui-ui-accent,#0078d4) 14%,var(--spui-ui-surface,#fff));color:var(--spui-ui-text,var(--spui-semantic-body-text,#323130))}button:disabled{opacity:.42;cursor:not-allowed}button:focus-visible,select:focus-visible,[role=textbox]:focus-visible,input:focus-visible{outline:2px solid var(--spui-focus-ring,var(--spui-ui-accent,#005a9e));outline-offset:2px}[role=textbox]{min-width:0;min-height:150px;box-sizing:border-box;padding:10px 12px;border:1px solid var(--spui-control-border,var(--spui-ui-stroke,#c8c8c8));border-radius:var(--spui-control-radius,6px);background:var(--spui-control-background,var(--spui-ui-surface,#fff));color:var(--spui-ui-text,inherit);font:inherit;line-height:1.5;overflow:auto;caret-color:var(--spui-focus-ring,var(--spui-ui-accent,#005a9e))}[role=textbox]:empty:before{content:attr(data-placeholder);color:var(--spui-ui-muted,#666);pointer-events:none}table{border-collapse:collapse;max-width:100%;margin-block:8px}caption{text-align:start;font-weight:600}td,th{min-width:56px;padding:6px 9px;border:1px solid var(--spui-ui-stroke,#c8c8c8);text-align:start}th{background:var(--spui-ui-surface-hover,#f5f5f5);font-weight:600}a{color:var(--spui-semantic-link,#0f6cbd)}@media(pointer:coarse){[role=toolbar] button{width:44px;min-width:44px;min-height:44px}[role=toolbar] select{min-height:44px}}@container (max-width: 64rem){[role=toolbar]{display:flex;flex-wrap:wrap;align-items:center;gap:6px;padding:8px}.toolbar-format{flex:1 0 100%;width:100%;max-width:none;min-width:0;min-height:40px}.toolbar-group{display:flex;flex:0 1 auto;flex-wrap:wrap;align-items:center;gap:4px;min-width:0;padding:0;border:0}.toolbar-group--fonts{display:grid;flex:1 0 100%;grid-template-columns:repeat(2,minmax(0,1fr))}.toolbar-font-select{width:100%;max-width:none;min-width:0;min-height:44px}.toolbar-group button{width:44px;min-width:44px;min-height:44px}.toolbar-group--table{flex:1 0 100%}[role=textbox]{min-height:12rem;overflow-wrap:anywhere}table{display:block;overflow-x:auto}}@media(forced-colors:active){select{border:1px solid ButtonText;background:Field;color:FieldText}button{border:1px solid ButtonText}button[aria-pressed=true]{color:Highlight;border-color:Highlight}td,th{border-color:CanvasText}}`;
|
|
1287
|
-
style.textContent += `.link-editor{display:flex;flex-wrap:wrap;align-items:end;gap:8px;padding:8px;border:1px solid var(--spui-ui-stroke,#c8c8c8);border-radius:var(--spui-control-radius,6px);background:var(--spui-ui-surface-hover,var(--spui-ui-surface,#fff))}.link-editor[hidden]{display:none}.link-field{display:grid;flex:1 1 150px;gap:4px;min-width:0;color:var(--spui-ui-text,inherit);font-size:13px}.link-field input{box-sizing:border-box;width:100%;min-width:0;min-height:32px;padding:5px 8px;border:1px solid var(--spui-control-border,var(--spui-ui-stroke,#8a8886));border-radius:var(--spui-control-radius,4px);background:var(--spui-control-background,var(--spui-ui-surface,#fff));color:var(--spui-ui-text,inherit)}.link-actions{display:flex;flex-wrap:wrap;gap:4px}.link-actions button{border:1px solid var(--spui-ui-stroke,#c8c8c8);border-radius:var(--spui-control-radius,4px);background:var(--spui-ui-surface,#fff)}.link-actions [data-link-action=apply]{border-color:transparent;background:var(--spui-semantic-primary-button-background,var(--spui-ui-accent,#0078d4));color:var(--spui-ui-accent-text,#fff)}.link-error{flex:1 0 100%;color:var(--spui-ui-danger,var(--spui-semantic-error-text,#c50f1f));font-size:13px}`;
|
|
1288
|
-
style.textContent += `[role=textbox],[role=textbox] *{-webkit-user-select:text!important;user-select:text!important;-webkit-touch-callout:default;touch-action:auto}`;
|
|
1289
|
-
style.textContent += `.toolbar-group--fonts{gap:4px}.toolbar-font-select{width:100%;min-width:0}.toolbar-group--color{gap:6px;padding-inline:8px}.toolbar-color{position:relative;min-width:0}.toolbar-color>summary{box-sizing:border-box;display:flex;align-items:center;gap:5px;min-height:36px;padding:4px 7px;border:1px solid var(--spui-control-border,var(--spui-ui-stroke,#c8c8c8));border-radius:var(--spui-control-radius,5px);background:var(--spui-control-background,var(--spui-ui-surface,#fff));color:var(--spui-ui-text,inherit);font-size:11px;font-weight:600;cursor:pointer;list-style:none}.toolbar-color>summary::-webkit-details-marker{display:none}.toolbar-color__glyph{font-size:17px;line-height:1}.toolbar-color[data-command=foreColor] .toolbar-color__glyph{border-bottom:3px solid var(--spui-picked-color)}.toolbar-color[data-command=hiliteColor] .toolbar-color__glyph{color:var(--spui-picked-color);text-shadow:0 0 0 var(--spui-picked-color)}.toolbar-color__current{width:16px;height:16px;border:1px solid color-mix(in srgb,var(--spui-ui-text,#323130) 22%,transparent);border-radius:4px;background:var(--spui-picked-color);flex:none}.toolbar-color__name{white-space:nowrap}.toolbar-color__palette{position:absolute;z-index:10;inset-block-start:calc(100% + 4px);inset-inline-start:0;display:grid;grid-template-columns:repeat(6,28px);gap:4px;min-width:196px;padding:8px;border:1px solid var(--spui-ui-stroke,#c8c8c8);border-radius:8px;background:var(--spui-ui-surface,#fff);box-shadow:0 8px 24px #0003}.toolbar-color__swatch{width:28px;min-width:28px;height:28px;min-height:28px;padding:0;border:1px solid color-mix(in srgb,var(--spui-ui-text,#323130) 22%,transparent);border-radius:50%;background:var(--spui-picked-color);cursor:pointer}.toolbar-color__swatch:hover{outline:2px solid var(--spui-ui-accent,#0078d4);outline-offset:1px}.toolbar-color__custom{grid-column:1/-1;display:flex;align-items:center;gap:8px;padding-block-start:6px;border-block-start:1px solid var(--spui-ui-stroke,#ddd);font-size:12px;font-weight:500}.toolbar-color__custom input{box-sizing:border-box;width:42px;height:34px;min-height:34px;padding:2px;border:1px solid var(--spui-control-border,var(--spui-ui-stroke,#c8c8c8));border-radius:5px;background:var(--spui-control-background,var(--spui-ui-surface,#fff));cursor:pointer}.toolbar-color input:disabled{opacity:.42;cursor:not-allowed}@container (max-width:64rem){.toolbar-group--fonts{grid-template-columns:repeat(2,minmax(0,1fr))}.toolbar-font-select{width:100%;min-width:0;max-width:none;min-height:44px}.toolbar-group--color{display:flex;flex-wrap:wrap;justify-content:start;padding:0}.toolbar-color>summary{min-height:44px;padding-inline:9px}.toolbar-color__palette{grid-template-columns:repeat(6,32px);min-width:220px}.toolbar-color__swatch{width:32px;min-width:32px;height:32px;min-height:32px}}`;
|
|
1290
|
-
style.textContent += `.link-new-tab{display:inline-flex;flex:1 0 100%;align-items:center;gap:8px;color:var(--spui-ui-text,inherit);font-size:13px}.link-new-tab input{width:16px;height:16px;min-height:16px;margin:0;accent-color:var(--spui-ui-accent,#0078d4)}`;
|
|
1291
|
-
style.textContent += `.link-picker-status{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}.toolbar-color>summary[aria-disabled=true]{opacity:.42;cursor:not-allowed}`;
|
|
1292
|
-
style.textContent += `.toolbar-color>summary:focus-visible,.toolbar-color__swatch:focus-visible{outline:2px solid var(--spui-focus-ring,var(--spui-ui-accent,#005a9e));outline-offset:2px}`;
|
|
1293
|
-
shadow.replaceChildren(style, label, toolbar, linkForm, this.editor);
|
|
1294
|
-
syncHostAccessibleName(this, this.editor);
|
|
1295
|
-
this.syncDisabled();
|
|
1296
|
-
this.refreshStrings();
|
|
1297
|
-
this.syncContentMode();
|
|
1298
|
-
this.onSelectionChange();
|
|
1299
|
-
}
|
|
1300
|
-
refreshStrings() {
|
|
1301
|
-
const toolbar = this.shadowRoot?.querySelector('[role="toolbar"]');
|
|
1302
|
-
if (!toolbar)
|
|
1303
|
-
return;
|
|
1304
|
-
this.imageInput?.setAttribute("aria-label", this.getAttribute("image-input-label") ?? "Choose image");
|
|
1305
|
-
const label = this.getAttribute("label") ?? "Rich text";
|
|
1306
|
-
toolbar.setAttribute("aria-label", this.getAttribute("toolbar-label") ?? `${label} formatting`);
|
|
1307
|
-
const attrs = ["bold-label", "italic-label", "underline-label", "strike-label", "bullet-list-label", "number-list-label", "clear-formatting-label", "undo-label", "redo-label", "insert-table-label", "insert-image-label", "insert-row-above-label", "insert-row-below-label", "insert-column-before-label", "insert-column-after-label", "delete-row-label", "delete-column-label", "delete-table-label", "link-label"];
|
|
1308
|
-
toolbar.querySelectorAll("button[data-command]").forEach((button, index) => {
|
|
1309
|
-
const attribute = button.dataset.labelAttribute ?? attrs[index];
|
|
1310
|
-
const text = this.getAttribute(attribute) ?? button.dataset.fallback ?? "";
|
|
1311
|
-
button.setAttribute("aria-label", text);
|
|
1312
|
-
button.title = text;
|
|
1313
|
-
});
|
|
1314
|
-
const format = toolbar.querySelector('select[data-command="formatBlock"]');
|
|
1315
|
-
if (format) {
|
|
1316
|
-
format.setAttribute("aria-label", this.getAttribute("format-block-label") ?? "Text style");
|
|
1317
|
-
const labels = { P: ["paragraph-label", "Paragraph"], H1: ["heading-1-label", "Heading 1"], H2: ["heading-2-label", "Heading 2"], H3: ["heading-3-label", "Heading 3"], BLOCKQUOTE: ["blockquote-label", "Quote"], PRE: ["preformatted-label", "Preformatted"] };
|
|
1318
|
-
format.querySelectorAll("option").forEach(option => { const [attribute, fallback] = labels[option.value]; option.textContent = this.getAttribute(attribute) ?? fallback; });
|
|
1319
|
-
}
|
|
1320
|
-
toolbar.querySelectorAll(".toolbar-font-select").forEach(select => select.setAttribute("aria-label", this.getAttribute(select.dataset.labelAttribute ?? "") ?? (select.dataset.command === "fontFamily" ? "Font family" : "Font size")));
|
|
1321
|
-
toolbar.querySelectorAll(".toolbar-color[data-label-attribute]").forEach(picker => {
|
|
1322
|
-
const text = this.getAttribute(picker.dataset.labelAttribute ?? "") ?? (picker.dataset.command === "foreColor" ? "Text color" : "Highlight color");
|
|
1323
|
-
const summary = picker.querySelector("summary");
|
|
1324
|
-
if (summary) {
|
|
1325
|
-
summary.setAttribute("aria-label", text);
|
|
1326
|
-
summary.title = text;
|
|
1327
|
-
}
|
|
1328
|
-
picker.querySelector(".toolbar-color__palette")?.setAttribute("aria-label", text);
|
|
1329
|
-
picker.querySelector('input[type="color"]')?.setAttribute("aria-label", `${text}: ${this.getAttribute("custom-color-label") ?? "Custom color"}`);
|
|
1330
|
-
const themeAccent = picker.querySelector('.toolbar-color__swatch[data-color="theme-accent"]');
|
|
1331
|
-
if (themeAccent) {
|
|
1332
|
-
const accentLabel = this.getAttribute("theme-accent-label") ?? "SharePoint theme accent";
|
|
1333
|
-
themeAccent.setAttribute("aria-label", accentLabel);
|
|
1334
|
-
themeAccent.title = accentLabel;
|
|
1335
|
-
}
|
|
1336
|
-
const swatchLabel = this.getAttribute("color-swatch-label") ?? "Select {color}";
|
|
1337
|
-
picker.querySelectorAll('.toolbar-color__swatch:not([data-color="theme-accent"])').forEach(swatch => { const label = swatchLabel.replace(/\{color\}/g, swatch.dataset.color ?? ""); swatch.setAttribute("aria-label", label); swatch.title = label; });
|
|
1338
|
-
const customLabel = picker.querySelector(".toolbar-color__custom");
|
|
1339
|
-
if (customLabel?.firstChild?.nodeType === Node.TEXT_NODE)
|
|
1340
|
-
customLabel.firstChild.textContent = this.getAttribute("custom-color-label") ?? "Custom color";
|
|
1341
|
-
const name = picker.querySelector(".toolbar-color__name");
|
|
1342
|
-
if (name)
|
|
1343
|
-
name.textContent = text;
|
|
1344
|
-
});
|
|
1345
|
-
if (this.linkForm) {
|
|
1346
|
-
this.linkForm.setAttribute("aria-label", this.getAttribute("link-dialog-label") ?? "Edit link");
|
|
1347
|
-
const fieldFallbacks = { "link-prompt": "Link URL", "link-text-label": "Display text", "link-title-label": "Title" };
|
|
1348
|
-
this.linkForm.querySelectorAll(".link-field[data-label-attribute]").forEach(field => {
|
|
1349
|
-
const attribute = field.dataset.labelAttribute;
|
|
1350
|
-
if (field.firstChild)
|
|
1351
|
-
field.firstChild.textContent = this.getAttribute(attribute) ?? fieldFallbacks[attribute] ?? "";
|
|
1352
|
-
});
|
|
1353
|
-
const actionFallbacks = { "link-apply-label": "Apply link", "link-remove-label": "Remove link", "link-cancel-label": "Cancel", "link-picker-label": "Browse SharePoint" };
|
|
1354
|
-
this.linkForm.querySelectorAll("button[data-label-attribute]").forEach(button => {
|
|
1355
|
-
const attribute = button.dataset.labelAttribute;
|
|
1356
|
-
button.textContent = this.getAttribute(attribute) ?? actionFallbacks[attribute] ?? "";
|
|
1357
|
-
});
|
|
1358
|
-
const newTabText = this.linkForm.querySelector(".link-new-tab span");
|
|
1359
|
-
if (newTabText)
|
|
1360
|
-
newTabText.textContent = this.getAttribute("link-new-tab-label") ?? "Open in new tab";
|
|
1361
|
-
}
|
|
1362
|
-
this.syncLinkPickerButton();
|
|
1363
|
-
this.syncTableActions();
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
customElements.define("spui-rich-text-editor", SpuiRichTextEditor);
|
|
1367
|
-
}
|