@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.
Files changed (282) hide show
  1. package/QUICKSTART.md +13 -128
  2. package/README.md +19 -26
  3. package/components.json +240 -5834
  4. package/dist/accordion.d.ts +11 -0
  5. package/dist/accordion.js +4 -0
  6. package/dist/alert.d.ts +3 -0
  7. package/dist/alert.js +4 -0
  8. package/dist/attachment-list.d.ts +15 -0
  9. package/dist/attachment-list.js +14 -0
  10. package/dist/avatar-group.d.ts +12 -0
  11. package/dist/avatar-group.js +7 -0
  12. package/dist/avatar.d.ts +6 -0
  13. package/dist/avatar.js +5 -0
  14. package/dist/badge.d.ts +3 -0
  15. package/dist/badge.js +4 -0
  16. package/dist/breadcrumb.d.ts +10 -0
  17. package/dist/breadcrumb.js +9 -0
  18. package/dist/button-group.d.ts +6 -0
  19. package/dist/button-group.js +4 -0
  20. package/dist/button.d.ts +3 -0
  21. package/dist/button.js +4 -0
  22. package/dist/card.d.ts +3 -0
  23. package/dist/card.js +4 -0
  24. package/dist/carousel.d.ts +11 -0
  25. package/dist/carousel.js +6 -0
  26. package/dist/checkbox.d.ts +3 -0
  27. package/dist/checkbox.js +4 -0
  28. package/dist/combobox.d.ts +9 -0
  29. package/dist/combobox.js +6 -0
  30. package/dist/command-bar.d.ts +6 -0
  31. package/dist/command-bar.js +4 -0
  32. package/dist/copy-button.d.ts +7 -0
  33. package/dist/copy-button.js +19 -0
  34. package/dist/data-grid.d.ts +25 -0
  35. package/dist/data-grid.js +39 -0
  36. package/dist/date-range-picker.d.ts +12 -0
  37. package/dist/date-range-picker.js +4 -0
  38. package/dist/dialog.d.ts +9 -0
  39. package/dist/dialog.js +21 -0
  40. package/dist/divider.d.ts +3 -0
  41. package/dist/divider.js +4 -0
  42. package/dist/drawer.d.ts +6 -0
  43. package/dist/drawer.js +5 -0
  44. package/dist/empty-state.d.ts +7 -0
  45. package/dist/empty-state.js +5 -0
  46. package/dist/field.d.ts +15 -0
  47. package/dist/field.js +11 -0
  48. package/dist/file-dropzone.d.ts +15 -0
  49. package/dist/file-dropzone.js +21 -0
  50. package/dist/filter-bar.d.ts +9 -0
  51. package/dist/filter-bar.js +9 -0
  52. package/dist/image.d.ts +5 -0
  53. package/dist/image.js +4 -0
  54. package/dist/index.d.ts +68 -78
  55. package/dist/index.js +60 -68
  56. package/dist/input.d.ts +3 -0
  57. package/dist/input.js +4 -0
  58. package/dist/label.d.ts +3 -0
  59. package/dist/label.js +4 -0
  60. package/dist/link.d.ts +3 -0
  61. package/dist/link.js +9 -0
  62. package/dist/list-view.d.ts +29 -0
  63. package/dist/list-view.js +14 -0
  64. package/dist/list.d.ts +6 -0
  65. package/dist/list.js +5 -0
  66. package/dist/lookup-picker.d.ts +12 -0
  67. package/dist/lookup-picker.js +4 -0
  68. package/dist/menu.d.ts +6 -0
  69. package/dist/menu.js +4 -0
  70. package/dist/nav.d.ts +12 -0
  71. package/dist/nav.js +4 -0
  72. package/dist/pagination.d.ts +11 -0
  73. package/dist/pagination.js +5 -0
  74. package/dist/person-picker.d.ts +13 -0
  75. package/dist/person-picker.js +4 -0
  76. package/dist/persona.d.ts +7 -0
  77. package/dist/persona.js +5 -0
  78. package/dist/popover.d.ts +6 -0
  79. package/dist/popover.js +4 -0
  80. package/dist/progress-bar.d.ts +5 -0
  81. package/dist/progress-bar.js +5 -0
  82. package/dist/radio-group.d.ts +6 -0
  83. package/dist/radio-group.js +4 -0
  84. package/dist/radio.d.ts +3 -0
  85. package/dist/radio.js +4 -0
  86. package/dist/rating.d.ts +8 -0
  87. package/dist/rating.js +5 -0
  88. package/dist/react.d.ts +1 -4715
  89. package/dist/react.js +1 -7097
  90. package/dist/select.d.ts +3 -0
  91. package/dist/select.js +4 -0
  92. package/dist/sharepoint-field.d.ts +18 -0
  93. package/dist/sharepoint-field.js +54 -0
  94. package/dist/skeleton.d.ts +6 -0
  95. package/dist/skeleton.js +9 -0
  96. package/dist/spinner.d.ts +5 -0
  97. package/dist/spinner.js +4 -0
  98. package/dist/swatch-picker.d.ts +17 -0
  99. package/dist/swatch-picker.js +11 -0
  100. package/dist/switch.d.ts +3 -0
  101. package/dist/switch.js +4 -0
  102. package/dist/table.d.ts +6 -0
  103. package/dist/table.js +4 -0
  104. package/dist/tabs.d.ts +14 -0
  105. package/dist/tabs.js +30 -0
  106. package/dist/tag-group.d.ts +6 -0
  107. package/dist/tag-group.js +4 -0
  108. package/dist/tag-picker.d.ts +12 -0
  109. package/dist/tag-picker.js +4 -0
  110. package/dist/tag.d.ts +7 -0
  111. package/dist/tag.js +5 -0
  112. package/dist/text.d.ts +5 -0
  113. package/dist/text.js +4 -0
  114. package/dist/textarea.d.ts +3 -0
  115. package/dist/textarea.js +4 -0
  116. package/dist/theme.d.ts +15 -175
  117. package/dist/theme.js +36 -239
  118. package/dist/toast.d.ts +9 -0
  119. package/dist/toast.js +12 -0
  120. package/dist/tree.d.ts +13 -0
  121. package/dist/tree.js +9 -0
  122. package/dist/view-card.d.ts +8 -0
  123. package/dist/view-card.js +4 -0
  124. package/dist/view-switcher.d.ts +13 -0
  125. package/dist/view-switcher.js +7 -0
  126. package/dist/virtual-list.d.ts +11 -0
  127. package/dist/virtual-list.js +25 -0
  128. package/measurements.json +365 -0
  129. package/package.json +14 -46
  130. package/src/theme.css +125 -0
  131. package/AGENTS.md +0 -231
  132. package/dist/appearance.d.ts +0 -6
  133. package/dist/appearance.js +0 -217
  134. package/dist/elements/accordion.d.ts +0 -9
  135. package/dist/elements/accordion.js +0 -338
  136. package/dist/elements/alert.d.ts +0 -2
  137. package/dist/elements/alert.js +0 -86
  138. package/dist/elements/avatar-group-primitives.d.ts +0 -3
  139. package/dist/elements/avatar-group-primitives.js +0 -189
  140. package/dist/elements/avatar-group.d.ts +0 -2
  141. package/dist/elements/avatar-group.js +0 -110
  142. package/dist/elements/avatar.d.ts +0 -4
  143. package/dist/elements/avatar.js +0 -251
  144. package/dist/elements/badge.d.ts +0 -8
  145. package/dist/elements/badge.js +0 -182
  146. package/dist/elements/breadcrumb.d.ts +0 -4
  147. package/dist/elements/breadcrumb.js +0 -233
  148. package/dist/elements/button-group.d.ts +0 -1
  149. package/dist/elements/button-group.js +0 -20
  150. package/dist/elements/button-variants.d.ts +0 -6
  151. package/dist/elements/button-variants.js +0 -339
  152. package/dist/elements/button.d.ts +0 -5
  153. package/dist/elements/button.js +0 -233
  154. package/dist/elements/card-primitives.d.ts +0 -3
  155. package/dist/elements/card-primitives.js +0 -83
  156. package/dist/elements/card.d.ts +0 -5
  157. package/dist/elements/card.js +0 -213
  158. package/dist/elements/carousel.d.ts +0 -92
  159. package/dist/elements/carousel.js +0 -1115
  160. package/dist/elements/color-picker.d.ts +0 -24
  161. package/dist/elements/color-picker.js +0 -1004
  162. package/dist/elements/combobox.d.ts +0 -1
  163. package/dist/elements/combobox.js +0 -1012
  164. package/dist/elements/data-grid-model.d.ts +0 -120
  165. package/dist/elements/data-grid-model.js +0 -294
  166. package/dist/elements/data-grid.d.ts +0 -30
  167. package/dist/elements/data-grid.js +0 -1576
  168. package/dist/elements/date-picker.d.ts +0 -1
  169. package/dist/elements/date-picker.js +0 -486
  170. package/dist/elements/dialog.d.ts +0 -23
  171. package/dist/elements/dialog.js +0 -409
  172. package/dist/elements/divider.d.ts +0 -2
  173. package/dist/elements/divider.js +0 -66
  174. package/dist/elements/drawer.d.ts +0 -3
  175. package/dist/elements/drawer.js +0 -308
  176. package/dist/elements/field-composition.d.ts +0 -26
  177. package/dist/elements/field-composition.js +0 -133
  178. package/dist/elements/field.d.ts +0 -1
  179. package/dist/elements/field.js +0 -67
  180. package/dist/elements/file-upload.d.ts +0 -6
  181. package/dist/elements/file-upload.js +0 -211
  182. package/dist/elements/floating-position.d.ts +0 -36
  183. package/dist/elements/floating-position.js +0 -92
  184. package/dist/elements/image.d.ts +0 -28
  185. package/dist/elements/image.js +0 -38
  186. package/dist/elements/info.d.ts +0 -9
  187. package/dist/elements/info.js +0 -102
  188. package/dist/elements/input.d.ts +0 -5
  189. package/dist/elements/input.js +0 -255
  190. package/dist/elements/label.d.ts +0 -15
  191. package/dist/elements/label.js +0 -56
  192. package/dist/elements/link.d.ts +0 -1
  193. package/dist/elements/link.js +0 -92
  194. package/dist/elements/list.d.ts +0 -19
  195. package/dist/elements/list.js +0 -297
  196. package/dist/elements/menu.d.ts +0 -33
  197. package/dist/elements/menu.js +0 -1103
  198. package/dist/elements/message-bar.d.ts +0 -10
  199. package/dist/elements/message-bar.js +0 -327
  200. package/dist/elements/nav.d.ts +0 -32
  201. package/dist/elements/nav.js +0 -794
  202. package/dist/elements/overflow.d.ts +0 -10
  203. package/dist/elements/overflow.js +0 -218
  204. package/dist/elements/persona.d.ts +0 -3
  205. package/dist/elements/persona.js +0 -125
  206. package/dist/elements/popover.d.ts +0 -77
  207. package/dist/elements/popover.js +0 -840
  208. package/dist/elements/progress-bar.d.ts +0 -4
  209. package/dist/elements/progress-bar.js +0 -82
  210. package/dist/elements/radio-group.d.ts +0 -1
  211. package/dist/elements/radio-group.js +0 -303
  212. package/dist/elements/rating-display.d.ts +0 -2
  213. package/dist/elements/rating-display.js +0 -125
  214. package/dist/elements/rating.d.ts +0 -2
  215. package/dist/elements/rating.js +0 -206
  216. package/dist/elements/rich-text-editor.d.ts +0 -32
  217. package/dist/elements/rich-text-editor.js +0 -1367
  218. package/dist/elements/search.d.ts +0 -3
  219. package/dist/elements/search.js +0 -200
  220. package/dist/elements/select.d.ts +0 -6
  221. package/dist/elements/select.js +0 -291
  222. package/dist/elements/shared.d.ts +0 -22
  223. package/dist/elements/shared.js +0 -409
  224. package/dist/elements/sharepoint-field-basic-controls.d.ts +0 -1
  225. package/dist/elements/sharepoint-field-basic-controls.js +0 -14
  226. package/dist/elements/sharepoint-fields.d.ts +0 -29
  227. package/dist/elements/sharepoint-fields.js +0 -460
  228. package/dist/elements/skeleton.d.ts +0 -6
  229. package/dist/elements/skeleton.js +0 -91
  230. package/dist/elements/slider.d.ts +0 -1
  231. package/dist/elements/slider.js +0 -248
  232. package/dist/elements/spin-button.d.ts +0 -7
  233. package/dist/elements/spin-button.js +0 -499
  234. package/dist/elements/spinner.d.ts +0 -4
  235. package/dist/elements/spinner.js +0 -124
  236. package/dist/elements/swatch-picker.d.ts +0 -5
  237. package/dist/elements/swatch-picker.js +0 -363
  238. package/dist/elements/table.d.ts +0 -21
  239. package/dist/elements/table.js +0 -261
  240. package/dist/elements/tabs.d.ts +0 -1
  241. package/dist/elements/tabs.js +0 -276
  242. package/dist/elements/tag-picker.d.ts +0 -29
  243. package/dist/elements/tag-picker.js +0 -715
  244. package/dist/elements/tags.d.ts +0 -21
  245. package/dist/elements/tags.js +0 -421
  246. package/dist/elements/teaching-popover.d.ts +0 -32
  247. package/dist/elements/teaching-popover.js +0 -412
  248. package/dist/elements/text.d.ts +0 -9
  249. package/dist/elements/text.js +0 -75
  250. package/dist/elements/textarea.d.ts +0 -1
  251. package/dist/elements/textarea.js +0 -182
  252. package/dist/elements/toast.d.ts +0 -25
  253. package/dist/elements/toast.js +0 -337
  254. package/dist/elements/toaster.d.ts +0 -6
  255. package/dist/elements/toaster.js +0 -357
  256. package/dist/elements/toggle.d.ts +0 -2
  257. package/dist/elements/toggle.js +0 -246
  258. package/dist/elements/toolbar.d.ts +0 -12
  259. package/dist/elements/toolbar.js +0 -381
  260. package/dist/elements/tooltip.d.ts +0 -76
  261. package/dist/elements/tooltip.js +0 -527
  262. package/dist/elements/tree-model.d.ts +0 -19
  263. package/dist/elements/tree-model.js +0 -34
  264. package/dist/elements/tree.d.ts +0 -5
  265. package/dist/elements/tree.js +0 -273
  266. package/dist/elements/view-card.d.ts +0 -2
  267. package/dist/elements/view-card.js +0 -78
  268. package/dist/elements/virtual-list.d.ts +0 -41
  269. package/dist/elements/virtual-list.js +0 -369
  270. package/dist/elements.d.ts +0 -73
  271. package/dist/elements.js +0 -210
  272. package/dist/sharepoint-fields.d.ts +0 -135
  273. package/dist/sharepoint-fields.js +0 -402
  274. package/dist/virtualizer-hooks.d.ts +0 -80
  275. package/dist/virtualizer-hooks.js +0 -253
  276. package/scripts/generate-token-index.mjs +0 -43
  277. package/scripts/sync-component-catalog-signatures.mjs +0 -55
  278. package/scripts/validate-component-catalog.mjs +0 -44
  279. package/skills/sharepoint-ui/SKILL.md +0 -12
  280. package/skills/sharepoint-ui/references/choosing-components.md +0 -20
  281. package/skills/sharepoint-ui/references/sharepoint-patterns.md +0 -17
  282. 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
- }