@aquera/nile-elements 1.9.8 → 1.9.9

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 (232) hide show
  1. package/README.md +3 -0
  2. package/dist/{index-249de90a.esm.js → index-289706c7.esm.js} +1 -1
  3. package/dist/{index-df196058.cjs.js → index-d7e6f1c6.cjs.js} +2 -2
  4. package/dist/index-d7e6f1c6.cjs.js.map +1 -0
  5. package/dist/index.cjs.js +1 -1
  6. package/dist/index.esm.js +1 -1
  7. package/dist/index.js +1577 -1275
  8. package/dist/katex-44476ab4.esm.js +1 -0
  9. package/dist/katex-f4186f87.cjs.js +2 -0
  10. package/dist/katex-f4186f87.cjs.js.map +1 -0
  11. package/dist/nile-color-picker/index.cjs.js +1 -1
  12. package/dist/nile-color-picker/index.esm.js +1 -1
  13. package/dist/nile-color-picker/nile-color-picker.cjs.js +1 -1
  14. package/dist/nile-color-picker/nile-color-picker.cjs.js.map +1 -1
  15. package/dist/nile-color-picker/nile-color-picker.esm.js +1 -1
  16. package/dist/nile-wysiwyg-editor/engine/commands.cjs.js +1 -1
  17. package/dist/nile-wysiwyg-editor/engine/commands.cjs.js.map +1 -1
  18. package/dist/nile-wysiwyg-editor/engine/commands.esm.js +1 -1
  19. package/dist/nile-wysiwyg-editor/engine/create-state.cjs.js +1 -1
  20. package/dist/nile-wysiwyg-editor/engine/create-state.cjs.js.map +1 -1
  21. package/dist/nile-wysiwyg-editor/engine/create-state.esm.js +1 -1
  22. package/dist/nile-wysiwyg-editor/engine/index.cjs.js +1 -1
  23. package/dist/nile-wysiwyg-editor/engine/index.esm.js +1 -1
  24. package/dist/nile-wysiwyg-editor/engine/inputrules.cjs.js +1 -1
  25. package/dist/nile-wysiwyg-editor/engine/keymap.cjs.js +1 -1
  26. package/dist/nile-wysiwyg-editor/engine/keymap.esm.js +1 -1
  27. package/dist/nile-wysiwyg-editor/engine/plugins/code-highlight.cjs.js +1 -1
  28. package/dist/nile-wysiwyg-editor/engine/plugins/code-highlight.cjs.js.map +1 -1
  29. package/dist/nile-wysiwyg-editor/engine/plugins/code-highlight.esm.js +1 -1
  30. package/dist/nile-wysiwyg-editor/engine/plugins/embed-view.cjs.js +1 -1
  31. package/dist/nile-wysiwyg-editor/engine/plugins/embed-view.cjs.js.map +1 -1
  32. package/dist/nile-wysiwyg-editor/engine/plugins/embed-view.esm.js +1 -1
  33. package/dist/nile-wysiwyg-editor/engine/plugins/embeds.cjs.js +1 -1
  34. package/dist/nile-wysiwyg-editor/engine/plugins/embeds.cjs.js.map +1 -1
  35. package/dist/nile-wysiwyg-editor/engine/plugins/embeds.esm.js +1 -1
  36. package/dist/nile-wysiwyg-editor/engine/plugins/math-view.cjs.js +1 -1
  37. package/dist/nile-wysiwyg-editor/engine/plugins/math-view.cjs.js.map +1 -1
  38. package/dist/nile-wysiwyg-editor/engine/plugins/math-view.esm.js +1 -1
  39. package/dist/nile-wysiwyg-editor/engine/plugins/style-block-view.cjs.js +2 -0
  40. package/dist/nile-wysiwyg-editor/engine/plugins/style-block-view.cjs.js.map +1 -0
  41. package/dist/nile-wysiwyg-editor/engine/plugins/style-block-view.esm.js +1 -0
  42. package/dist/nile-wysiwyg-editor/engine/plugins/tables.cjs.js +1 -1
  43. package/dist/nile-wysiwyg-editor/engine/plugins/tables.cjs.js.map +1 -1
  44. package/dist/nile-wysiwyg-editor/engine/plugins/tables.esm.js +1 -1
  45. package/dist/nile-wysiwyg-editor/engine/sanitize.cjs.js +1 -1
  46. package/dist/nile-wysiwyg-editor/engine/sanitize.cjs.js.map +1 -1
  47. package/dist/nile-wysiwyg-editor/engine/sanitize.esm.js +1 -1
  48. package/dist/nile-wysiwyg-editor/engine/schema.cjs.js +1 -1
  49. package/dist/nile-wysiwyg-editor/engine/schema.cjs.js.map +1 -1
  50. package/dist/nile-wysiwyg-editor/engine/schema.esm.js +1 -1
  51. package/dist/nile-wysiwyg-editor/engine/selection-state.cjs.js +1 -1
  52. package/dist/nile-wysiwyg-editor/engine/selection-state.cjs.js.map +1 -1
  53. package/dist/nile-wysiwyg-editor/engine/selection-state.esm.js +1 -1
  54. package/dist/nile-wysiwyg-editor/index.cjs.js +1 -1
  55. package/dist/nile-wysiwyg-editor/index.esm.js +1 -1
  56. package/dist/nile-wysiwyg-editor/math/katex-renderer.cjs.js +2 -0
  57. package/dist/nile-wysiwyg-editor/math/katex-renderer.cjs.js.map +1 -0
  58. package/dist/nile-wysiwyg-editor/math/katex-renderer.esm.js +1 -0
  59. package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.cjs.js +1 -1
  60. package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.cjs.js.map +1 -1
  61. package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.css.cjs.js +1 -1
  62. package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.css.cjs.js.map +1 -1
  63. package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.css.esm.js +22 -3
  64. package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.esm.js +7 -6
  65. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.cjs.js +1 -1
  66. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.esm.js +1 -1
  67. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar-item.cjs.js +2 -0
  68. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar-item.cjs.js.map +1 -0
  69. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar-item.esm.js +1 -0
  70. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.cjs.js +1 -1
  71. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.cjs.js.map +1 -1
  72. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.cjs.js +1 -1
  73. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.cjs.js.map +1 -1
  74. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.esm.js +33 -54
  75. package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.esm.js +83 -95
  76. package/dist/nile-wysiwyg-editor/types.cjs.js +1 -1
  77. package/dist/nile-wysiwyg-editor/types.cjs.js.map +1 -1
  78. package/dist/nile-wysiwyg-editor/types.esm.js +1 -1
  79. package/dist/nile-wysiwyg-editor/ui/bubble-item.cjs.js +2 -0
  80. package/dist/nile-wysiwyg-editor/ui/bubble-item.cjs.js.map +1 -0
  81. package/dist/nile-wysiwyg-editor/ui/bubble-item.esm.js +1 -0
  82. package/dist/nile-wysiwyg-editor/ui/bubble-menu.cjs.js +1 -1
  83. package/dist/nile-wysiwyg-editor/ui/bubble-menu.cjs.js.map +1 -1
  84. package/dist/nile-wysiwyg-editor/ui/bubble-menu.esm.js +23 -15
  85. package/dist/nile-wysiwyg-editor/ui/code-block-menu.cjs.js +1 -1
  86. package/dist/nile-wysiwyg-editor/ui/code-block-menu.cjs.js.map +1 -1
  87. package/dist/nile-wysiwyg-editor/ui/code-block-menu.esm.js +3 -3
  88. package/dist/nile-wysiwyg-editor/ui/embed-panel.cjs.js +1 -1
  89. package/dist/nile-wysiwyg-editor/ui/embed-panel.cjs.js.map +1 -1
  90. package/dist/nile-wysiwyg-editor/ui/embed-panel.esm.js +2 -2
  91. package/dist/nile-wysiwyg-editor/ui/math-panel.cjs.js +1 -1
  92. package/dist/nile-wysiwyg-editor/ui/math-panel.cjs.js.map +1 -1
  93. package/dist/nile-wysiwyg-editor/ui/math-panel.esm.js +0 -3
  94. package/dist/nile-wysiwyg-editor/ui/preview.cjs.js +2 -0
  95. package/dist/nile-wysiwyg-editor/ui/preview.cjs.js.map +1 -0
  96. package/dist/nile-wysiwyg-editor/ui/preview.esm.js +8 -0
  97. package/dist/nile-wysiwyg-editor/ui/source-view.cjs.js +1 -1
  98. package/dist/nile-wysiwyg-editor/ui/source-view.cjs.js.map +1 -1
  99. package/dist/nile-wysiwyg-editor/ui/source-view.esm.js +225 -24
  100. package/dist/nile-wysiwyg-editor/ui/suggest-list.cjs.js +1 -1
  101. package/dist/nile-wysiwyg-editor/ui/suggest-list.cjs.js.map +1 -1
  102. package/dist/nile-wysiwyg-editor/ui/suggest-list.esm.js +9 -9
  103. package/dist/nile-wysiwyg-editor/ui/table-grid-picker.cjs.js +1 -1
  104. package/dist/nile-wysiwyg-editor/ui/table-grid-picker.cjs.js.map +1 -1
  105. package/dist/nile-wysiwyg-editor/ui/table-grid-picker.esm.js +115 -13
  106. package/dist/nile-wysiwyg-editor/ui/table-menu.cjs.js +1 -1
  107. package/dist/nile-wysiwyg-editor/ui/table-menu.cjs.js.map +1 -1
  108. package/dist/nile-wysiwyg-editor/ui/table-menu.esm.js +13 -12
  109. package/dist/src/index.d.ts +5 -0
  110. package/dist/src/index.js +3 -0
  111. package/dist/src/index.js.map +1 -1
  112. package/dist/src/nile-color-picker/nile-color-picker.d.ts +1 -0
  113. package/dist/src/nile-color-picker/nile-color-picker.js +1 -0
  114. package/dist/src/nile-color-picker/nile-color-picker.js.map +1 -1
  115. package/dist/src/nile-wysiwyg-editor/engine/index.d.ts +1 -0
  116. package/dist/src/nile-wysiwyg-editor/engine/index.js +1 -0
  117. package/dist/src/nile-wysiwyg-editor/engine/index.js.map +1 -1
  118. package/dist/src/nile-wysiwyg-editor/engine/plugins/embed-view.js +3 -0
  119. package/dist/src/nile-wysiwyg-editor/engine/plugins/embed-view.js.map +1 -1
  120. package/dist/src/nile-wysiwyg-editor/engine/plugins/embeds.d.ts +19 -4
  121. package/dist/src/nile-wysiwyg-editor/engine/plugins/embeds.js +23 -18
  122. package/dist/src/nile-wysiwyg-editor/engine/plugins/embeds.js.map +1 -1
  123. package/dist/src/nile-wysiwyg-editor/engine/plugins/math-view.d.ts +8 -3
  124. package/dist/src/nile-wysiwyg-editor/engine/plugins/math-view.js +41 -4
  125. package/dist/src/nile-wysiwyg-editor/engine/plugins/math-view.js.map +1 -1
  126. package/dist/src/nile-wysiwyg-editor/engine/plugins/style-block-view.d.ts +17 -0
  127. package/dist/src/nile-wysiwyg-editor/engine/plugins/style-block-view.js +174 -0
  128. package/dist/src/nile-wysiwyg-editor/engine/plugins/style-block-view.js.map +1 -0
  129. package/dist/src/nile-wysiwyg-editor/engine/plugins/tables.js +33 -2
  130. package/dist/src/nile-wysiwyg-editor/engine/plugins/tables.js.map +1 -1
  131. package/dist/src/nile-wysiwyg-editor/engine/pm.test.js +49 -8
  132. package/dist/src/nile-wysiwyg-editor/engine/pm.test.js.map +1 -1
  133. package/dist/src/nile-wysiwyg-editor/engine/sanitize.d.ts +12 -0
  134. package/dist/src/nile-wysiwyg-editor/engine/sanitize.js +76 -8
  135. package/dist/src/nile-wysiwyg-editor/engine/sanitize.js.map +1 -1
  136. package/dist/src/nile-wysiwyg-editor/engine/schema.d.ts +2 -0
  137. package/dist/src/nile-wysiwyg-editor/engine/schema.js +154 -2
  138. package/dist/src/nile-wysiwyg-editor/engine/schema.js.map +1 -1
  139. package/dist/src/nile-wysiwyg-editor/index.d.ts +5 -0
  140. package/dist/src/nile-wysiwyg-editor/index.js +3 -0
  141. package/dist/src/nile-wysiwyg-editor/index.js.map +1 -1
  142. package/dist/src/nile-wysiwyg-editor/math/katex-renderer.d.ts +38 -0
  143. package/dist/src/nile-wysiwyg-editor/math/katex-renderer.js +40 -0
  144. package/dist/src/nile-wysiwyg-editor/math/katex-renderer.js.map +1 -0
  145. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.css.js +22 -3
  146. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.css.js.map +1 -1
  147. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.d.ts +36 -0
  148. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.js +125 -8
  149. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.js.map +1 -1
  150. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.test.js +5 -2
  151. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.test.js.map +1 -1
  152. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.d.ts +2 -0
  153. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.js +1 -0
  154. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.js.map +1 -1
  155. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar-item.d.ts +55 -0
  156. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar-item.js +79 -0
  157. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar-item.js.map +1 -0
  158. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.js +33 -54
  159. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.js.map +1 -1
  160. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.d.ts +50 -2
  161. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.js +158 -102
  162. package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.js.map +1 -1
  163. package/dist/src/nile-wysiwyg-editor/types.d.ts +4 -2
  164. package/dist/src/nile-wysiwyg-editor/types.js +2 -2
  165. package/dist/src/nile-wysiwyg-editor/types.js.map +1 -1
  166. package/dist/src/nile-wysiwyg-editor/ui/bubble-item.d.ts +48 -0
  167. package/dist/src/nile-wysiwyg-editor/ui/bubble-item.js +69 -0
  168. package/dist/src/nile-wysiwyg-editor/ui/bubble-item.js.map +1 -0
  169. package/dist/src/nile-wysiwyg-editor/ui/bubble-menu.d.ts +17 -2
  170. package/dist/src/nile-wysiwyg-editor/ui/bubble-menu.js +128 -35
  171. package/dist/src/nile-wysiwyg-editor/ui/bubble-menu.js.map +1 -1
  172. package/dist/src/nile-wysiwyg-editor/ui/embed-panel.d.ts +2 -1
  173. package/dist/src/nile-wysiwyg-editor/ui/embed-panel.js +4 -3
  174. package/dist/src/nile-wysiwyg-editor/ui/embed-panel.js.map +1 -1
  175. package/dist/src/nile-wysiwyg-editor/ui/math-panel.js +0 -3
  176. package/dist/src/nile-wysiwyg-editor/ui/math-panel.js.map +1 -1
  177. package/dist/src/nile-wysiwyg-editor/ui/preview.d.ts +20 -0
  178. package/dist/src/nile-wysiwyg-editor/ui/preview.js +44 -0
  179. package/dist/src/nile-wysiwyg-editor/ui/preview.js.map +1 -0
  180. package/dist/src/nile-wysiwyg-editor/ui/source-view.d.ts +35 -3
  181. package/dist/src/nile-wysiwyg-editor/ui/source-view.js +337 -24
  182. package/dist/src/nile-wysiwyg-editor/ui/source-view.js.map +1 -1
  183. package/dist/src/nile-wysiwyg-editor/ui/suggest-list.d.ts +1 -0
  184. package/dist/src/nile-wysiwyg-editor/ui/suggest-list.js +9 -0
  185. package/dist/src/nile-wysiwyg-editor/ui/suggest-list.js.map +1 -1
  186. package/dist/src/nile-wysiwyg-editor/ui/table-grid-picker.d.ts +13 -2
  187. package/dist/src/nile-wysiwyg-editor/ui/table-grid-picker.js +166 -10
  188. package/dist/src/nile-wysiwyg-editor/ui/table-grid-picker.js.map +1 -1
  189. package/dist/src/nile-wysiwyg-editor/ui/table-menu.d.ts +1 -0
  190. package/dist/src/nile-wysiwyg-editor/ui/table-menu.js +10 -8
  191. package/dist/src/nile-wysiwyg-editor/ui/table-menu.js.map +1 -1
  192. package/dist/src/version.js +1 -1
  193. package/dist/src/version.js.map +1 -1
  194. package/dist/tsconfig.tsbuildinfo +1 -1
  195. package/dist/xml-25c4108a.esm.js +1 -0
  196. package/dist/xml-90fd974e.cjs.js +2 -0
  197. package/dist/xml-90fd974e.cjs.js.map +1 -0
  198. package/package.json +2 -1
  199. package/rollup.config.js +1 -0
  200. package/src/index.ts +8 -0
  201. package/src/nile-color-picker/nile-color-picker.ts +1 -0
  202. package/src/nile-wysiwyg-editor/engine/index.ts +1 -0
  203. package/src/nile-wysiwyg-editor/engine/plugins/embed-view.ts +3 -0
  204. package/src/nile-wysiwyg-editor/engine/plugins/embeds.ts +26 -20
  205. package/src/nile-wysiwyg-editor/engine/plugins/math-view.ts +45 -4
  206. package/src/nile-wysiwyg-editor/engine/plugins/style-block-view.ts +178 -0
  207. package/src/nile-wysiwyg-editor/engine/plugins/tables.ts +34 -2
  208. package/src/nile-wysiwyg-editor/engine/pm.test.ts +58 -8
  209. package/src/nile-wysiwyg-editor/engine/sanitize.ts +71 -8
  210. package/src/nile-wysiwyg-editor/engine/schema.ts +158 -7
  211. package/src/nile-wysiwyg-editor/index.ts +5 -0
  212. package/src/nile-wysiwyg-editor/math/katex-renderer.ts +59 -0
  213. package/src/nile-wysiwyg-editor/math/katex.d.ts +17 -0
  214. package/src/nile-wysiwyg-editor/nile-wysiwyg-editor.css.ts +22 -3
  215. package/src/nile-wysiwyg-editor/nile-wysiwyg-editor.test.ts +5 -2
  216. package/src/nile-wysiwyg-editor/nile-wysiwyg-editor.ts +142 -7
  217. package/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.ts +2 -0
  218. package/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar-item.ts +68 -0
  219. package/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.ts +33 -54
  220. package/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.ts +202 -108
  221. package/src/nile-wysiwyg-editor/types.ts +6 -4
  222. package/src/nile-wysiwyg-editor/ui/bubble-item.ts +60 -0
  223. package/src/nile-wysiwyg-editor/ui/bubble-menu.ts +147 -40
  224. package/src/nile-wysiwyg-editor/ui/embed-panel.ts +4 -3
  225. package/src/nile-wysiwyg-editor/ui/math-panel.ts +0 -3
  226. package/src/nile-wysiwyg-editor/ui/preview.ts +42 -0
  227. package/src/nile-wysiwyg-editor/ui/source-view.ts +339 -31
  228. package/src/nile-wysiwyg-editor/ui/suggest-list.ts +10 -0
  229. package/src/nile-wysiwyg-editor/ui/table-grid-picker.ts +160 -10
  230. package/src/nile-wysiwyg-editor/ui/table-menu.ts +10 -8
  231. package/vscode-html-custom-data.json +75 -6
  232. package/dist/index-df196058.cjs.js.map +0 -1
@@ -1,7 +1,8 @@
1
- import { Schema, NodeSpec, MarkSpec } from 'prosemirror-model';
1
+ import { Schema, NodeSpec, MarkSpec, Attrs, DOMOutputSpec, ParseRule } from 'prosemirror-model';
2
2
  import { addListNodes } from 'prosemirror-schema-list';
3
3
  import { tableNodes } from 'prosemirror-tables';
4
4
  import OrderedMap from 'orderedmap';
5
+ import { EMBED_SANDBOX, EMBED_REFERRER_POLICY } from './plugins/embeds';
5
6
 
6
7
  export type Alignment = 'left' | 'center' | 'right' | 'justify';
7
8
 
@@ -54,6 +55,100 @@ function blockStyle(attrs: {
54
55
  return parts.length ? parts.join('; ') : null;
55
56
  }
56
57
 
58
+ /* -------------------------------------------------------------------------
59
+ * Author class / id passthrough
60
+ *
61
+ * Content elements keep the `class` and `id` an author gave them, so class/id
62
+ * CSS (e.g. a scoped author <style> block) can target the document. Values that
63
+ * collide with the editor's own internal names are dropped so content can never
64
+ * impersonate or restyle the chrome; the editor's own classes (emitted in
65
+ * toDOM) are always preserved and merged with the author's.
66
+ * ------------------------------------------------------------------------- */
67
+
68
+ const RESERVED_NAME = /^(?:nile-wysiwyg|ProseMirror)/;
69
+
70
+ export function cleanClass(value: string | null): string | null {
71
+ if (!value) return null;
72
+ const kept = value.split(/\s+/).filter(token => token && !RESERVED_NAME.test(token));
73
+ return kept.length ? kept.join(' ') : null;
74
+ }
75
+
76
+ export function cleanId(value: string | null): string | null {
77
+ return value && !RESERVED_NAME.test(value) ? value : null;
78
+ }
79
+
80
+ const HTML_ATTR_SPEC = { htmlClass: { default: null }, htmlId: { default: null } };
81
+
82
+ function readHtmlAttrs(dom: HTMLElement): { htmlClass: string | null; htmlId: string | null } {
83
+ return {
84
+ htmlClass: cleanClass(dom.getAttribute('class')),
85
+ htmlId: cleanId(dom.getAttribute('id')),
86
+ };
87
+ }
88
+
89
+ /** Wraps a tag-based parse rule so it also captures author class/id. Rules
90
+ * keyed by `style` (whose getAttrs receives a value string, not an element)
91
+ * pass through untouched. */
92
+ function augmentParseRule(rule: ParseRule): ParseRule {
93
+ if (!('tag' in rule) || 'style' in rule) return rule;
94
+ const inner = rule.getAttrs;
95
+ return {
96
+ ...rule,
97
+ getAttrs(dom: HTMLElement) {
98
+ const base = inner ? inner(dom) : null;
99
+ if (base === false) return false;
100
+ return { ...(base ?? {}), ...readHtmlAttrs(dom) };
101
+ },
102
+ } as ParseRule;
103
+ }
104
+
105
+ /** Wraps a toDOM so stored class/id are written back onto the element, merged
106
+ * with any class the spec emits itself. Handles the `[tag]`, `[tag, attrs]`,
107
+ * `[tag, 0]`, and `[tag, attrs, …children]` output shapes. */
108
+ function augmentToDOM(
109
+ toDOM?: (node: { attrs: Attrs }, ...rest: unknown[]) => DOMOutputSpec
110
+ ): ((node: { attrs: Attrs }, ...rest: unknown[]) => DOMOutputSpec) | undefined {
111
+ if (!toDOM) return toDOM;
112
+ return (node, ...rest) => {
113
+ const out = toDOM(node, ...rest);
114
+ const cls = node.attrs.htmlClass as string | null;
115
+ const id = node.attrs.htmlId as string | null;
116
+ if (!Array.isArray(out) || (!cls && !id)) return out;
117
+ const [tag, ...tail] = out as unknown[];
118
+ let attrs: Record<string, unknown> = {};
119
+ let children = tail;
120
+ if (tail.length && tail[0] && typeof tail[0] === 'object' && !Array.isArray(tail[0])) {
121
+ attrs = { ...(tail[0] as Record<string, unknown>) };
122
+ children = tail.slice(1);
123
+ }
124
+ if (cls) attrs.class = attrs.class ? `${attrs.class as string} ${cls}` : cls;
125
+ if (id) attrs.id = id;
126
+ return [tag, attrs, ...children] as DOMOutputSpec;
127
+ };
128
+ }
129
+
130
+ /** Adds `htmlClass`/`htmlId` attrs plus parse/serialize support to a spec. */
131
+ function withHtmlAttrs<S extends NodeSpec | MarkSpec>(spec: S): S {
132
+ return {
133
+ ...spec,
134
+ attrs: { ...(spec.attrs ?? {}), ...HTML_ATTR_SPEC },
135
+ parseDOM: spec.parseDOM?.map(augmentParseRule),
136
+ toDOM: augmentToDOM(spec.toDOM as never) as never,
137
+ };
138
+ }
139
+
140
+ /** Content nodes/marks that carry author class/id. Excludes structural leaves
141
+ * (doc, text), atoms with bespoke DOM (image, embeds, mentions, math, …) and
142
+ * style-only marks (their spans are covered by `html_span`). */
143
+ const HTML_ATTR_NODES = [
144
+ 'paragraph', 'heading', 'blockquote', 'code_block',
145
+ 'bullet_list', 'ordered_list', 'list_item',
146
+ 'table', 'table_row', 'table_cell', 'table_header',
147
+ ];
148
+ const HTML_ATTR_MARKS = [
149
+ 'link', 'bold', 'italic', 'underline', 'strike', 'code', 'subscript', 'superscript',
150
+ ];
151
+
57
152
  /**
58
153
  * Block nodes. `align` is modeled as an attribute on textblocks rather than a
59
154
  * wrapper node so alignment survives block-type changes (heading <-> paragraph).
@@ -368,6 +463,8 @@ const nodes: Record<string, NodeSpec> = {
368
463
  frameborder: '0',
369
464
  allowfullscreen: 'true',
370
465
  allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
466
+ sandbox: EMBED_SANDBOX,
467
+ referrerpolicy: EMBED_REFERRER_POLICY,
371
468
  };
372
469
  if (title) iframeAttrs.title = title;
373
470
  const wrapperAttrs: Record<string, string> = {
@@ -389,6 +486,29 @@ const nodes: Record<string, NodeSpec> = {
389
486
  },
390
487
  },
391
488
 
489
+ /**
490
+ * Author-supplied `<style>` block, entered via the HTML source view. The CSS
491
+ * is sanitized on the way in (see sanitizeCss) — `@import`, `expression()`
492
+ * and external `url()` are neutralized — then stored verbatim and serialized
493
+ * straight back to a `<style>` element. A node view renders it live in the
494
+ * editor with a small chip so the otherwise-invisible block stays selectable.
495
+ */
496
+ styleBlock: {
497
+ group: 'block',
498
+ atom: true,
499
+ selectable: true,
500
+ attrs: { css: { default: '' } },
501
+ parseDOM: [
502
+ {
503
+ tag: 'style',
504
+ getAttrs: (dom: HTMLElement) => ({ css: dom.textContent ?? '' }),
505
+ },
506
+ ],
507
+ toDOM(node) {
508
+ return ['style', node.attrs.css];
509
+ },
510
+ },
511
+
392
512
  /**
393
513
  * Bookmark: a named in-text anchor (`<a id="…">`) for intra-document
394
514
  * navigation. Invisible in serialized output; the editor draws a flag
@@ -705,6 +825,30 @@ const marks: Record<string, MarkSpec> = {
705
825
  },
706
826
  },
707
827
 
828
+ /** Carries an author `class`/`id` on an inline `<span>` that has no other
829
+ * meaning, so class/id CSS can target runs of text. Low priority so styled
830
+ * spans (color/font marks) and atom spans (mention/merge/math) win. */
831
+ html_span: {
832
+ attrs: { htmlClass: { default: null }, htmlId: { default: null } },
833
+ parseDOM: [
834
+ {
835
+ tag: 'span',
836
+ priority: 5,
837
+ getAttrs: (dom: HTMLElement) => {
838
+ const htmlClass = cleanClass(dom.getAttribute('class'));
839
+ const htmlId = cleanId(dom.getAttribute('id'));
840
+ return htmlClass || htmlId ? { htmlClass, htmlId } : false;
841
+ },
842
+ },
843
+ ],
844
+ toDOM(mark) {
845
+ const attrs: Record<string, string> = {};
846
+ if (mark.attrs.htmlClass) attrs.class = mark.attrs.htmlClass;
847
+ if (mark.attrs.htmlId) attrs.id = mark.attrs.htmlId;
848
+ return ['span', attrs, 0];
849
+ },
850
+ },
851
+
708
852
  text_color: {
709
853
  attrs: { color: {} },
710
854
  parseDOM: [
@@ -843,16 +987,23 @@ const orderedListSpec: NodeSpec = {
843
987
 
844
988
  /** Builds the editor schema. List nodes are added via prosemirror-schema-list. */
845
989
  export function createSchema(): Schema {
846
- const nodeMap = addListNodes(
847
- OrderedMap.from(nodes),
848
- 'paragraph block*',
849
- 'block'
850
- )
990
+ let nodeMap = addListNodes(OrderedMap.from(nodes), 'paragraph block*', 'block')
851
991
  .update('bullet_list', bulletListSpec)
852
992
  .update('ordered_list', orderedListSpec)
853
993
  .append(tableNodeSpecs)
854
994
  .update('table', tableSpec);
855
- return new Schema({ nodes: nodeMap, marks: OrderedMap.from(marks) });
995
+ // Thread author class/id through the content nodes and marks (applied last so
996
+ // it also wraps the list/table specs pulled in from external packages).
997
+ for (const name of HTML_ATTR_NODES) {
998
+ const spec = nodeMap.get(name);
999
+ if (spec) nodeMap = nodeMap.update(name, withHtmlAttrs(spec));
1000
+ }
1001
+ let markMap = OrderedMap.from<MarkSpec>(marks);
1002
+ for (const name of HTML_ATTR_MARKS) {
1003
+ const spec = markMap.get(name);
1004
+ if (spec) markMap = markMap.update(name, withHtmlAttrs(spec));
1005
+ }
1006
+ return new Schema({ nodes: nodeMap, marks: markMap });
856
1007
  }
857
1008
 
858
1009
  /** A shared default schema instance. */
@@ -1,3 +1,8 @@
1
1
  export { NileWysiwygEditor, default } from './nile-wysiwyg-editor';
2
+ export { NileWysiwygToolbarItem } from './nile-wysiwyg-toolbar/nile-wysiwyg-toolbar-item';
3
+ export { NileWysiwygBubbleItem } from './ui/bubble-item';
4
+ export type { AuthoredToolbarItem } from './nile-wysiwyg-toolbar/nile-wysiwyg-toolbar';
2
5
  export type { WysiwygChangeDetail, WysiwygCommandDetail, ToolbarConfig } from './types';
3
6
  export { DEFAULT_TOOLBAR } from './types';
7
+ export { createKatexRenderer } from './math/katex-renderer';
8
+ export type { KatexLike, KatexRendererOptions } from './math/katex-renderer';
@@ -0,0 +1,59 @@
1
+ import type { MathRenderer } from '../engine/plugins/math-view';
2
+
3
+ /**
4
+ * The slice of the KaTeX module this helper needs. Declared locally so the
5
+ * design system never imports `katex` itself — you bundle KaTeX from your own
6
+ * app's install and pass the instance in.
7
+ */
8
+ export interface KatexLike {
9
+ renderToString(latex: string, options?: Record<string, unknown>): string;
10
+ }
11
+
12
+ export interface KatexRendererOptions {
13
+ /**
14
+ * Extra options merged into every `katex.renderToString` call (after the
15
+ * `displayMode` / `throwOnError` defaults). See the KaTeX options reference.
16
+ */
17
+ katexOptions?: Record<string, unknown>;
18
+ }
19
+
20
+ /**
21
+ * Builds a {@link MathRenderer} backed by KaTeX for the editor's `mathRenderer`
22
+ * property. KaTeX is **not** bundled by the design system — install it in your
23
+ * app and hand the instance to this helper so your bundler ships it (no CDN):
24
+ *
25
+ * ```ts
26
+ * import katex from 'katex';
27
+ * import 'katex/dist/katex.min.css';
28
+ * import { createKatexRenderer } from '@aquera/nile-elements';
29
+ *
30
+ * editor.mathRenderer = createKatexRenderer(katex);
31
+ * // or with overrides:
32
+ * editor.mathRenderer = createKatexRenderer(katex, {
33
+ * katexOptions: { macros: { '\\RR': '\\mathbb{R}' } },
34
+ * });
35
+ * ```
36
+ *
37
+ * Called with no instance it falls back to a global `katex` (e.g. a CDN
38
+ * `<script>`), which is handy for quick demos. If no engine is available at
39
+ * render time the raw LaTeX is returned, so content stays visible.
40
+ */
41
+ export function createKatexRenderer(
42
+ katex?: KatexLike,
43
+ options: KatexRendererOptions = {}
44
+ ): MathRenderer {
45
+ return (latex, display) => {
46
+ const engine = katex ?? (globalThis as { katex?: KatexLike }).katex;
47
+ if (!engine) return latex;
48
+ try {
49
+ return engine.renderToString(latex, {
50
+ displayMode: display === 'block',
51
+ throwOnError: false,
52
+ ...options.katexOptions,
53
+ });
54
+ } catch {
55
+ // Malformed LaTeX (with throwOnError overridden true, say) → show source.
56
+ return latex;
57
+ }
58
+ };
59
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Minimal ambient types for KaTeX — the package ships no declarations and we
3
+ * only use `renderToString`. Keeps the built-in MathML renderer in
4
+ * `engine/plugins/math-view.ts` type-safe without pulling in @types/katex.
5
+ */
6
+ declare module 'katex' {
7
+ interface KatexOptions {
8
+ displayMode?: boolean;
9
+ output?: 'html' | 'mathml' | 'htmlAndMathml';
10
+ throwOnError?: boolean;
11
+ [option: string]: unknown;
12
+ }
13
+ const katex: {
14
+ renderToString(tex: string, options?: KatexOptions): string;
15
+ };
16
+ export default katex;
17
+ }
@@ -292,8 +292,10 @@ export const styles = css`
292
292
  text-align: left;
293
293
  }
294
294
 
295
- .ProseMirror td > :last-child,
296
- .ProseMirror th > :last-child {
295
+
296
+ .ProseMirror td > *,
297
+ .ProseMirror th > * {
298
+ margin-top: 0;
297
299
  margin-bottom: 0;
298
300
  }
299
301
 
@@ -605,11 +607,28 @@ export const styles = css`
605
607
  font-weight: 600;
606
608
  }
607
609
 
608
- /* --- Sticky toolbar --- */
610
+ /* --- Author <style> block (from HTML source) ---
611
+ Applied live by StyleBlockView (a hidden <style> host, scoped to the
612
+ content); the block renders nothing in the editor. */
613
+ .nile-wysiwyg__style-block {
614
+ display: none;
615
+ }
616
+
617
+ /* --- Sticky toolbar ---
618
+ Pins to the top of the scroll container. Note: sticky anchors to the
619
+ container's PADDING box, so if the scroll container has top padding, a
620
+ band of content shows above the toolbar — set --nile-wysiwyg-sticky-top
621
+ (or remove the container's top padding) to compensate. The opaque
622
+ background makes the pinned bar self-backing so content can never bleed
623
+ through it. */
609
624
  :host([sticky-toolbar]) nile-wysiwyg-toolbar {
610
625
  position: sticky;
611
626
  top: var(--nile-wysiwyg-sticky-top, 0);
612
627
  z-index: 12;
628
+ background: var(
629
+ --nile-wysiwyg-bg,
630
+ var(--nile-colors-white-base, var(--ng-colors-bg-primary, #ffffff))
631
+ );
613
632
  }
614
633
 
615
634
  /* --- Mentions & suggestions --- */
@@ -381,9 +381,12 @@ describe('nile-wysiwyg-editor', () => {
381
381
  const source = el.shadowRoot!.querySelector('nile-wysiwyg-source-view')!;
382
382
  expect(source).to.exist;
383
383
  await source.updateComplete;
384
- expect(source.textarea!.value).to.equal('<p>original</p>');
384
+ expect(source.currentValue).to.equal('<p>original</p>');
385
385
 
386
- source.textarea!.value = '<p>edited</p><script>alert(1)</script>';
386
+ // Simulate editing the textarea (its input event drives the draft).
387
+ const textarea = source.shadowRoot!.querySelector('textarea')!;
388
+ textarea.value = '<p>edited</p><script>alert(1)</script>';
389
+ textarea.dispatchEvent(new Event('input'));
387
390
  el.toggleSourceView();
388
391
  await el.updateComplete;
389
392
  expect(el.shadowRoot!.querySelector('nile-wysiwyg-source-view')).to.not.exist;
@@ -30,6 +30,7 @@ import {
30
30
  ImageView,
31
31
  FigureView,
32
32
  EmbedView,
33
+ StyleBlockView,
33
34
  MathView,
34
35
  isOfficeHtml,
35
36
  cleanOfficeHtml,
@@ -64,8 +65,11 @@ import { DEFAULT_TOOLBAR } from './types';
64
65
  import { EMOJI } from './emoji';
65
66
 
66
67
  import './nile-wysiwyg-toolbar/nile-wysiwyg-toolbar';
68
+ import './nile-wysiwyg-toolbar/nile-wysiwyg-toolbar-item';
69
+ import type { AuthoredToolbarItem } from './nile-wysiwyg-toolbar/nile-wysiwyg-toolbar';
67
70
  import './ui/link-editor';
68
71
  import './ui/bubble-menu';
72
+ import './ui/bubble-item';
69
73
  import './ui/image-panel';
70
74
  import './ui/table-menu';
71
75
  import './ui/suggest-list';
@@ -177,6 +181,22 @@ export class NileWysiwygEditor extends NileElement {
177
181
  /** Toolbar config string; `"none"` hides the toolbar. */
178
182
  @property({ type: String }) toolbar = DEFAULT_TOOLBAR;
179
183
 
184
+ /**
185
+ * Toolbar built from authored `<nile-wysiwyg-toolbar-item>` light-DOM
186
+ * children. When present it takes precedence over the `toolbar` string.
187
+ */
188
+ @state() private authoredToolbar?: AuthoredToolbarItem[];
189
+
190
+ /**
191
+ * Bubble menu built from authored `<nile-wysiwyg-bubble-item>` light-DOM
192
+ * children. When present it replaces the built-in bold/italic/underline/
193
+ * strike/link set.
194
+ */
195
+ @state() private authoredBubble?: AuthoredToolbarItem[];
196
+
197
+ /** Watches authored toolbar/bubble children so edits re-render them. */
198
+ private toolbarObserver?: MutationObserver;
199
+
180
200
  /** Shows a balloon toolbar over text selections. */
181
201
  @property({ type: Boolean, attribute: 'bubble-menu' }) bubbleMenu = false;
182
202
 
@@ -459,11 +479,34 @@ export class NileWysiwygEditor extends NileElement {
459
479
  }
460
480
  // Close floating panels when the user clicks anywhere outside them.
461
481
  document.addEventListener('pointerdown', this.onOutsidePointerDown, true);
482
+ // Format painter applies on pointer release so a drag-selection is painted
483
+ // whole, not on the first mid-drag selection change.
484
+ document.addEventListener('mouseup', this.onPainterPointerUp);
485
+
486
+ // Discover an author-supplied toolbar / bubble menu (light-DOM
487
+ // <nile-wysiwyg-toolbar-item> / <nile-wysiwyg-bubble-item> children) and
488
+ // keep them in sync as they change.
489
+ this.syncAuthoredToolbar();
490
+ this.syncAuthoredBubble();
491
+ if (!this.toolbarObserver) {
492
+ this.toolbarObserver = new MutationObserver(() => {
493
+ this.syncAuthoredToolbar();
494
+ this.syncAuthoredBubble();
495
+ });
496
+ }
497
+ this.toolbarObserver.observe(this, {
498
+ childList: true,
499
+ subtree: true,
500
+ attributes: true,
501
+ attributeFilter: ['name', 'label', 'icon', 'divider', 'overflow'],
502
+ });
462
503
  }
463
504
 
464
505
  disconnectedCallback(): void {
465
506
  super.disconnectedCallback();
466
507
  document.removeEventListener('pointerdown', this.onOutsidePointerDown, true);
508
+ document.removeEventListener('mouseup', this.onPainterPointerUp);
509
+ this.toolbarObserver?.disconnect();
467
510
  if (this.changeTimer) clearTimeout(this.changeTimer);
468
511
  if (this.autosaveTimer) clearInterval(this.autosaveTimer);
469
512
  if (this.previousBodyOverflow !== null) {
@@ -476,9 +519,57 @@ export class NileWysiwygEditor extends NileElement {
476
519
 
477
520
  protected firstUpdated(): void {
478
521
  this.defaultValue = this.value;
522
+ // Re-read now that light-DOM children are guaranteed parsed (the initial
523
+ // connectedCallback read can run mid-parse before children exist).
524
+ this.syncAuthoredToolbar();
525
+ this.syncAuthoredBubble();
479
526
  this.mountView();
480
527
  }
481
528
 
529
+ /**
530
+ * Reads `<nile-wysiwyg-toolbar-item>` children into a structured toolbar and
531
+ * stores it on {@link authoredToolbar}. Absent items clear the override
532
+ * (falling back to the `toolbar` string). No-ops when nothing changed to
533
+ * avoid needless re-renders.
534
+ */
535
+ private syncAuthoredToolbar(): void {
536
+ const nodes = this.querySelectorAll('nile-wysiwyg-toolbar-item');
537
+ const next: AuthoredToolbarItem[] | undefined = nodes.length
538
+ ? Array.from(nodes)
539
+ .map(el => ({
540
+ name: el.hasAttribute('divider') ? '|' : el.getAttribute('name') || '',
541
+ label: el.getAttribute('label') || undefined,
542
+ icon: el.getAttribute('icon') || undefined,
543
+ overflow: el.hasAttribute('overflow') || undefined,
544
+ }))
545
+ .filter(item => item.name)
546
+ : undefined;
547
+ if (JSON.stringify(next) !== JSON.stringify(this.authoredToolbar)) {
548
+ this.authoredToolbar = next;
549
+ }
550
+ }
551
+
552
+ /**
553
+ * Reads `<nile-wysiwyg-bubble-item>` children into a structured bubble-menu
554
+ * item list stored on {@link authoredBubble}. Absent items clear the override
555
+ * (falling back to the built-in default). No-ops when nothing changed.
556
+ */
557
+ private syncAuthoredBubble(): void {
558
+ const nodes = this.querySelectorAll('nile-wysiwyg-bubble-item');
559
+ const next: AuthoredToolbarItem[] | undefined = nodes.length
560
+ ? Array.from(nodes)
561
+ .map(el => ({
562
+ name: el.hasAttribute('divider') ? '|' : el.getAttribute('name') || '',
563
+ label: el.getAttribute('label') || undefined,
564
+ icon: el.getAttribute('icon') || undefined,
565
+ }))
566
+ .filter(item => item.name)
567
+ : undefined;
568
+ if (JSON.stringify(next) !== JSON.stringify(this.authoredBubble)) {
569
+ this.authoredBubble = next;
570
+ }
571
+ }
572
+
482
573
  protected updated(changed: PropertyValues): void {
483
574
  if (changed.has('value') && this.view && !this.syncingValue) {
484
575
  const current = docToHtml(this.view.state.doc);
@@ -654,23 +745,60 @@ export class NileWysiwygEditor extends NileElement {
654
745
  new FigureView(node, view, getPos as () => number | undefined),
655
746
  embed: (node, view, getPos) =>
656
747
  new EmbedView(node, view, getPos as () => number | undefined),
748
+ styleBlock: node => new StyleBlockView(node),
657
749
  math: node => new MathView(node, this.mathRenderer),
658
750
  },
659
751
  });
660
752
 
753
+ this.view.dom.addEventListener('mousedown', this.onPainterPointerDown);
754
+
661
755
  this.toolbarState = this.snapshotToolbar(this.view.state);
662
756
  this.counter = countWords(this.view.state.doc);
663
757
  this.syncFormValue(this.view.state.doc);
664
758
  }
665
759
 
760
+ /**
761
+ * True while a drag-selection is in progress with the format painter armed.
762
+ * Suppresses the mid-drag auto-apply in {@link handleTransaction} so the
763
+ * copied formatting lands on the final, complete selection (on mouse-up).
764
+ */
765
+ private painterDragActive = false;
766
+
767
+ private onPainterPointerDown = (): void => {
768
+ if (this.view && isFormatPainterArmed(this.view.state)) {
769
+ this.painterDragActive = true;
770
+ }
771
+ };
772
+
773
+ private onPainterPointerUp = (): void => {
774
+ if (!this.painterDragActive) return;
775
+ this.painterDragActive = false;
776
+ // Defer a frame so ProseMirror has synced the final DOM selection.
777
+ requestAnimationFrame(() => {
778
+ if (!this.view) return;
779
+ if (this.view.state.selection.empty || !isFormatPainterArmed(this.view.state)) return;
780
+ const applyTr = applyFormatPainter(this.view.state);
781
+ if (applyTr) this.view.dispatch(applyTr);
782
+ });
783
+ };
784
+
666
785
  private handleTransaction(tr: Transaction): void {
667
786
  if (!this.view) return;
668
787
  const newState = this.view.state.apply(tr);
669
788
  this.view.updateState(newState);
670
789
 
671
790
  // Format painter: once armed, the next non-empty selection the user makes
672
- // receives the copied formatting, then the painter disarms.
673
- if (tr.selectionSet && !newState.selection.empty && isFormatPainterArmed(newState)) {
791
+ // receives the copied formatting, then the painter disarms. While a pointer
792
+ // drag is building the selection we defer to the mouse-up handler
793
+ // (onPainterPointerUp) so the whole range is painted, not just the first
794
+ // mid-drag character. Single-shot selections (double-click, Shift+End,
795
+ // programmatic) still apply here.
796
+ if (
797
+ tr.selectionSet &&
798
+ !newState.selection.empty &&
799
+ isFormatPainterArmed(newState) &&
800
+ !this.painterDragActive
801
+ ) {
674
802
  const applyTr = applyFormatPainter(newState);
675
803
  if (applyTr) {
676
804
  this.view.dispatch(applyTr);
@@ -1389,7 +1517,7 @@ export class NileWysiwygEditor extends NileElement {
1389
1517
  /** Toggles raw-HTML source editing. Content re-sanitizes on the way back. */
1390
1518
  public toggleSourceView(): void {
1391
1519
  if (this.sourceMode) {
1392
- this.applySourceView(this.sourceViewEl?.textarea?.value ?? null);
1520
+ this.applySourceView(this.sourceViewEl?.currentValue ?? null);
1393
1521
  } else {
1394
1522
  this.sourceMode = true;
1395
1523
  this.updateComplete.then(() => {
@@ -1924,7 +2052,13 @@ export class NileWysiwygEditor extends NileElement {
1924
2052
  };
1925
2053
 
1926
2054
  public render(): TemplateResult {
1927
- const showToolbar = this.toolbar !== 'none' && !this.readonly && !this.sourceMode;
2055
+ const hasAuthoredToolbar = (this.authoredToolbar?.length ?? 0) > 0;
2056
+ // Keep the toolbar mounted in source mode too, so the main menu stays put
2057
+ // above the split source/preview view instead of being replaced by it. It
2058
+ // is disabled there — formatting commands would target the hidden rich-text
2059
+ // view — and the source view's own Cancel/Done bar handles exiting.
2060
+ const showToolbar =
2061
+ (this.toolbar !== 'none' || hasAuthoredToolbar) && !this.readonly;
1928
2062
  return html`
1929
2063
  <div
1930
2064
  class="nile-wysiwyg ${this.showBlocks ? 'nile-wysiwyg--show-blocks' : ''}"
@@ -1937,9 +2071,10 @@ export class NileWysiwygEditor extends NileElement {
1937
2071
  ? html`
1938
2072
  <nile-wysiwyg-toolbar
1939
2073
  part="toolbar"
1940
- exportparts="button, select, color"
2074
+ exportparts="button, select, color, overflow, overflow-panel"
1941
2075
  config=${this.toolbar}
1942
- ?disabled=${this.disabled}
2076
+ .items=${this.authoredToolbar}
2077
+ ?disabled=${this.disabled || this.sourceMode}
1943
2078
  .toolbarState=${this.toolbarState}
1944
2079
  ></nile-wysiwyg-toolbar>
1945
2080
  `
@@ -1984,7 +2119,7 @@ export class NileWysiwygEditor extends NileElement {
1984
2119
  this.closeShortcutHelp();
1985
2120
  }}
1986
2121
  ></nile-wysiwyg-shortcut-help>
1987
- <nile-wysiwyg-bubble-menu></nile-wysiwyg-bubble-menu>
2122
+ <nile-wysiwyg-bubble-menu .items=${this.authoredBubble}></nile-wysiwyg-bubble-menu>
1988
2123
  <nile-wysiwyg-table-menu></nile-wysiwyg-table-menu>
1989
2124
  <nile-wysiwyg-code-menu></nile-wysiwyg-code-menu>
1990
2125
  <nile-wysiwyg-image-menu></nile-wysiwyg-image-menu>
@@ -1 +1,3 @@
1
1
  export { NileWysiwygToolbar } from './nile-wysiwyg-toolbar';
2
+ export type { AuthoredToolbarItem } from './nile-wysiwyg-toolbar';
3
+ export { NileWysiwygToolbarItem } from './nile-wysiwyg-toolbar-item';
@@ -0,0 +1,68 @@
1
+ import { LitElement } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+
4
+ /**
5
+ * Declarative, light-DOM marker for a single toolbar control.
6
+ *
7
+ * Author these inside a `<nile-wysiwyg-toolbar>` container within a
8
+ * `<nile-wysiwyg-editor>` to build the toolbar by hand instead of (or in
9
+ * addition to) the `toolbar` config string:
10
+ *
11
+ * ```html
12
+ * <nile-wysiwyg-editor>
13
+ * <nile-wysiwyg-toolbar>
14
+ * <nile-wysiwyg-toolbar-item name="bold" label="Bold"></nile-wysiwyg-toolbar-item>
15
+ * <nile-wysiwyg-toolbar-item name="italic"></nile-wysiwyg-toolbar-item>
16
+ * <nile-wysiwyg-toolbar-item divider></nile-wysiwyg-toolbar-item>
17
+ * <nile-wysiwyg-toolbar-item name="heading"></nile-wysiwyg-toolbar-item>
18
+ * <!-- goes into the 3-dot "more" menu, wherever it sits in the list -->
19
+ * <nile-wysiwyg-toolbar-item name="sourceView" overflow></nile-wysiwyg-toolbar-item>
20
+ * </nile-wysiwyg-toolbar>
21
+ * </nile-wysiwyg-editor>
22
+ * ```
23
+ *
24
+ * The container is optional — items may also be direct children of the editor.
25
+ *
26
+ * `name` maps to a built-in control (the same keys the `toolbar` string
27
+ * accepts, e.g. `bold`, `italic`, `heading`, `textColor`, `table`). `label`
28
+ * and `icon` override that control's built-in tooltip/glyph; set `divider` to
29
+ * insert a group separator, or `overflow` to tuck this control into the
30
+ * toolbar's collapsible 3-dot "more" menu.
31
+ *
32
+ * The element renders nothing itself — the editor discovers it and renders the
33
+ * real control inside its own shadow DOM.
34
+ *
35
+ * @tag nile-wysiwyg-toolbar-item
36
+ */
37
+ @customElement('nile-wysiwyg-toolbar-item')
38
+ export class NileWysiwygToolbarItem extends LitElement {
39
+ protected createRenderRoot(): this {
40
+ return this;
41
+ }
42
+ protected shouldUpdate(): boolean {
43
+ return false;
44
+ }
45
+
46
+ /** Built-in control name — same keys accepted by the `toolbar` string. */
47
+ @property({ type: String, reflect: true }) name = '';
48
+
49
+ /** Overrides the control's tooltip / accessible label. */
50
+ @property({ type: String, reflect: true }) label = '';
51
+
52
+ /** Overrides the control's glyph (a `nile-glyph` name). */
53
+ @property({ type: String, reflect: true }) icon = '';
54
+
55
+ /** Renders a group divider instead of a control. */
56
+ @property({ type: Boolean, reflect: true }) divider = false;
57
+
58
+ /** When set, this control is tucked into the toolbar's collapsible 3-dot
59
+ * "more" menu instead of the main bar — regardless of where it sits among
60
+ * the items. */
61
+ @property({ type: Boolean, reflect: true }) overflow = false;
62
+ }
63
+
64
+ declare global {
65
+ interface HTMLElementTagNameMap {
66
+ 'nile-wysiwyg-toolbar-item': NileWysiwygToolbarItem;
67
+ }
68
+ }