tungsten 0.3.3 → 0.3.5

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 (153) hide show
  1. package/dist/.build-complete +1 -1
  2. package/dist/Components.d.ts +6 -1
  3. package/dist/Components.d.ts.map +1 -1
  4. package/dist/Components.js +103 -40
  5. package/dist/DomPointer.d.ts +11 -0
  6. package/dist/DomPointer.d.ts.map +1 -1
  7. package/dist/DomPointer.js +15 -0
  8. package/dist/DomRegion.d.ts +2 -0
  9. package/dist/DomRegion.d.ts.map +1 -1
  10. package/dist/DomRegion.js +23 -3
  11. package/dist/Framework.d.ts.map +1 -1
  12. package/dist/Framework.js +28 -6
  13. package/dist/FrameworkConfig.d.ts +2 -0
  14. package/dist/FrameworkConfig.d.ts.map +1 -1
  15. package/dist/Instance/Instance.d.ts +1 -1
  16. package/dist/Instance/Instance.d.ts.map +1 -1
  17. package/dist/Servers/DevServer.d.ts.map +1 -1
  18. package/dist/Servers/DevServer.js +3 -3
  19. package/dist/Servers/ProdServer.d.ts +1 -1
  20. package/dist/Servers/ProdServer.d.ts.map +1 -1
  21. package/dist/Servers/ProdServer.js +13 -3
  22. package/dist/Servers/loadEnvsDev.d.ts.map +1 -1
  23. package/dist/Servers/loadEnvsDev.js +17 -6
  24. package/dist/babelPlugins/BabelExtractComponentStylesheet.d.ts +13 -0
  25. package/dist/babelPlugins/BabelExtractComponentStylesheet.d.ts.map +1 -0
  26. package/dist/babelPlugins/BabelExtractComponentStylesheet.js +24 -0
  27. package/dist/babelPlugins/BabelTransformComponentStyles.d.ts +2 -0
  28. package/dist/babelPlugins/BabelTransformComponentStyles.d.ts.map +1 -1
  29. package/dist/babelPlugins/BabelTransformComponentStyles.js +21 -5
  30. package/dist/babelPlugins/BabelTransformPackageImports.d.ts +1 -0
  31. package/dist/babelPlugins/BabelTransformPackageImports.d.ts.map +1 -1
  32. package/dist/babelPlugins/BabelTransformPackageImports.js +21 -9
  33. package/dist/bin/commands/build.d.ts.map +1 -1
  34. package/dist/bin/commands/build.js +9 -2
  35. package/dist/bin/commands/compile.d.ts.map +1 -1
  36. package/dist/bin/commands/compile.js +7 -2
  37. package/dist/build/CompilationInfo.d.ts +1 -0
  38. package/dist/build/CompilationInfo.d.ts.map +1 -1
  39. package/dist/build/combineComponentCss.d.ts +1 -1
  40. package/dist/build/combineComponentCss.d.ts.map +1 -1
  41. package/dist/build/combineComponentCss.js +3 -3
  42. package/dist/build/compileJavascript.d.ts.map +1 -1
  43. package/dist/build/compileJavascript.js +2 -2
  44. package/dist/build/compileUserApp.d.ts.map +1 -1
  45. package/dist/build/compileUserApp.js +14 -5
  46. package/dist/build/frameworkStylesheets.d.ts +5 -0
  47. package/dist/build/frameworkStylesheets.d.ts.map +1 -0
  48. package/dist/build/frameworkStylesheets.js +49 -0
  49. package/dist/build/getPackageDirForFile.d.ts +2 -0
  50. package/dist/build/getPackageDirForFile.d.ts.map +1 -0
  51. package/dist/build/getPackageDirForFile.js +11 -0
  52. package/dist/build/getSxyClientPackageAliases.d.ts.map +1 -1
  53. package/dist/build/getSxyClientPackageAliases.js +3 -1
  54. package/dist/build/makeBabelUserDevConfig.d.ts +1 -1
  55. package/dist/build/makeBabelUserDevConfig.d.ts.map +1 -1
  56. package/dist/build/makeBabelUserDevConfig.js +3 -2
  57. package/dist/build/makeBabelUserProdConfig.d.ts +1 -1
  58. package/dist/build/makeBabelUserProdConfig.d.ts.map +1 -1
  59. package/dist/build/makeBabelUserProdConfig.js +4 -2
  60. package/dist/build/rollup.user.prod.config.d.ts.map +1 -1
  61. package/dist/build/rollup.user.prod.config.js +4 -1
  62. package/dist/build/rollupExtractComponentStylesheets.d.ts +12 -0
  63. package/dist/build/rollupExtractComponentStylesheets.d.ts.map +1 -0
  64. package/dist/build/rollupExtractComponentStylesheets.js +35 -0
  65. package/dist/compat/context.d.ts +1 -1
  66. package/dist/compat/context.d.ts.map +1 -1
  67. package/dist/componentCss/i/T-components-library-RichTextEditor-RichTextEditor.css +115 -0
  68. package/dist/componentCss/i/T-components-library-__StatefulMessage-StatefulMessage.css +25 -0
  69. package/dist/componentCss/tungsten.css +2 -0
  70. package/dist/componentProxyHandlers.d.ts +2 -1
  71. package/dist/componentProxyHandlers.d.ts.map +1 -1
  72. package/dist/componentProxyHandlers.js +45 -9
  73. package/dist/components/GoToComponent.d.ts +1 -1
  74. package/dist/components/GoToComponent.d.ts.map +1 -1
  75. package/dist/components/_GoToComponent.d.ts +10 -0
  76. package/dist/components/_GoToComponent.d.ts.map +1 -0
  77. package/dist/components/_GoToComponent.js +13 -0
  78. package/dist/components/library/RichTextEditor/RichTextEditor.d.ts +50 -0
  79. package/dist/components/library/RichTextEditor/RichTextEditor.d.ts.map +1 -0
  80. package/dist/components/library/RichTextEditor/RichTextEditor.js +458 -0
  81. package/dist/components/library/RichTextEditor/StoredContentNode.d.ts +5 -0
  82. package/dist/components/library/RichTextEditor/StoredContentNode.d.ts.map +1 -0
  83. package/dist/components/library/RichTextEditor/StoredContentNode.js +1 -0
  84. package/dist/components/library/RichTextEditor/activeFormats.d.ts +20 -0
  85. package/dist/components/library/RichTextEditor/activeFormats.d.ts.map +1 -0
  86. package/dist/components/library/RichTextEditor/activeFormats.js +48 -0
  87. package/dist/components/library/RichTextEditor/contentTree.d.ts +30 -0
  88. package/dist/components/library/RichTextEditor/contentTree.d.ts.map +1 -0
  89. package/dist/components/library/RichTextEditor/contentTree.js +81 -0
  90. package/dist/components/library/RichTextEditor/sanitizeAuthoredHtml.d.ts +30 -0
  91. package/dist/components/library/RichTextEditor/sanitizeAuthoredHtml.d.ts.map +1 -0
  92. package/dist/components/library/RichTextEditor/sanitizeAuthoredHtml.js +97 -0
  93. package/dist/components/library/__StatefulMessage/MessageData.d.ts +7 -0
  94. package/dist/components/library/__StatefulMessage/MessageData.d.ts.map +1 -0
  95. package/dist/components/library/__StatefulMessage/MessageData.js +1 -0
  96. package/dist/components/library/__StatefulMessage/StatefulMessage.d.ts +22 -0
  97. package/dist/components/library/__StatefulMessage/StatefulMessage.d.ts.map +1 -0
  98. package/dist/components/library/__StatefulMessage/StatefulMessage.js +88 -0
  99. package/dist/components/library/index.d.ts +5 -0
  100. package/dist/components/library/index.d.ts.map +1 -0
  101. package/dist/components/library/index.js +7 -0
  102. package/dist/defaultConfig.d.ts +1 -0
  103. package/dist/defaultConfig.d.ts.map +1 -1
  104. package/dist/defaultConfig.js +1 -0
  105. package/dist/env/addToEnv.d.ts +2 -0
  106. package/dist/env/addToEnv.d.ts.map +1 -0
  107. package/dist/env/addToEnv.js +19 -0
  108. package/dist/findUserConfig.d.ts +1 -1
  109. package/dist/indexInner.d.ts +5 -5
  110. package/dist/indexInner.d.ts.map +1 -1
  111. package/dist/indexInner.js +2 -1
  112. package/dist/postcssPlugins/TransformCssNames.d.ts.map +1 -1
  113. package/dist/postcssPlugins/TransformCssNames.js +10 -2
  114. package/dist/propsAreEqual.d.ts +8 -0
  115. package/dist/propsAreEqual.d.ts.map +1 -0
  116. package/dist/propsAreEqual.js +97 -0
  117. package/dist/render/HeadData.d.ts +1 -0
  118. package/dist/render/HeadData.d.ts.map +1 -1
  119. package/dist/render/contentToHtml.d.ts.map +1 -1
  120. package/dist/render/contentToHtml.js +1 -0
  121. package/dist/render/instantiateClassComponent.d.ts.map +1 -1
  122. package/dist/render/instantiateClassComponent.js +4 -0
  123. package/dist/render/processHead.d.ts.map +1 -1
  124. package/dist/render/processHead.js +14 -1
  125. package/dist/settings.d.ts +2 -0
  126. package/dist/settings.d.ts.map +1 -1
  127. package/dist/settings.js +3 -0
  128. package/dist/stateTracking.d.ts +47 -0
  129. package/dist/stateTracking.d.ts.map +1 -0
  130. package/dist/stateTracking.js +83 -0
  131. package/dist/staticGen/staticGen.d.ts.map +1 -1
  132. package/dist/staticGen/staticGen.js +117 -107
  133. package/dist/testing/installTestStyleProcessors.d.ts +2 -0
  134. package/dist/testing/installTestStyleProcessors.d.ts.map +1 -0
  135. package/dist/testing/installTestStyleProcessors.js +21 -0
  136. package/dist/testing/temp/componentPackageMock.d.ts +18 -0
  137. package/dist/testing/temp/componentPackageMock.d.ts.map +1 -0
  138. package/dist/testing/temp/componentPackageMock.js +37 -0
  139. package/dist/testing/temp/contentHtmlElements.d.ts +3 -0
  140. package/dist/testing/temp/contentHtmlElements.d.ts.map +1 -0
  141. package/dist/testing/temp/contentHtmlElements.js +12 -0
  142. package/dist/update/integrateUpdateVdom.d.ts.map +1 -1
  143. package/dist/update/integrateUpdateVdom.js +160 -51
  144. package/dist/update/pendingMounts.d.ts +25 -0
  145. package/dist/update/pendingMounts.d.ts.map +1 -0
  146. package/dist/update/pendingMounts.js +55 -0
  147. package/package.json +4 -2
  148. package/dist/env/injectEnv.d.ts +0 -2
  149. package/dist/env/injectEnv.d.ts.map +0 -1
  150. package/dist/env/injectEnv.js +0 -9
  151. package/dist/env/parseEnv.d.ts +0 -2
  152. package/dist/env/parseEnv.d.ts.map +0 -1
  153. package/dist/env/parseEnv.js +0 -14
@@ -0,0 +1,458 @@
1
+ function _defineProperty(e, r, t) {
2
+ return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
3
+ value: t,
4
+ enumerable: !0,
5
+ configurable: !0,
6
+ writable: !0
7
+ }) : e[r] = t, e;
8
+ }
9
+ function _toPropertyKey(t) {
10
+ var i = _toPrimitive(t, "string");
11
+ return "symbol" == typeof i ? i : i + "";
12
+ }
13
+ function _toPrimitive(t, r) {
14
+ if ("object" != typeof t || !t) return t;
15
+ var e = t[Symbol.toPrimitive];
16
+ if (void 0 !== e) {
17
+ var i = e.call(t, r || "default");
18
+ if ("object" != typeof i) return i;
19
+ throw new TypeError("@@toPrimitive must return a primitive value.");
20
+ }
21
+ return ("string" === r ? String : Number)(t);
22
+ }
23
+ import { TungstenStatefulComponent } from "../../../Components.js";
24
+ import { createElementRef } from "../../../createRef.js";
25
+ import { combineClassNames } from "../../../helpers/combineClassNames.js";
26
+ import { parseHtml } from "../../../rawHtml/parseHtml.js";
27
+ import { safeHtmlAllowList } from "../../../rawHtml/safeHtmlAllowList.js";
28
+ import { activeFormatsAt } from "./activeFormats.js";
29
+ import { serializeContentTree } from "./contentTree.js";
30
+ import { sanitizeAuthoredHtml } from "./sanitizeAuthoredHtml.js";
31
+ import { jsx as _jsx, jsxs as _jsxs } from "tungsten/jsxRuntime.js";
32
+ const formatBlockOptions = [
33
+ // Applying this produces a <p> which the sanitizer then flattens back to
34
+ // inline content — the way out of a heading or quote, and the state plain
35
+ // text reports as.
36
+ {
37
+ value: 'p',
38
+ text: 'Normal text'
39
+ }, {
40
+ value: 'h2',
41
+ text: 'Heading 2'
42
+ }, {
43
+ value: 'h3',
44
+ text: 'Heading 3'
45
+ }, {
46
+ value: 'h4',
47
+ text: 'Heading 4'
48
+ }, {
49
+ value: 'blockquote',
50
+ text: 'Quote'
51
+ }];
52
+ // The blocks the menu can actually display. A caret inside anything else —
53
+ // contentEditable's own bare <div> wrappers, say — shows the placeholder
54
+ // rather than silently misreporting the format.
55
+ const knownBlocks = new Set(formatBlockOptions.map(option => option.value));
56
+ // Commands whose active state at the cursor is reflected on the toolbar.
57
+ const trackedCommands = ['bold', 'italic', 'underline', 'strikeThrough', 'subscript', 'superscript', 'insertUnorderedList', 'insertOrderedList'];
58
+ // `mark` names the styling each label carries, so the icon demonstrates the
59
+ // formatting it applies: a bold B, an underlined U, a struck-through S.
60
+ const formattingButtons = [{
61
+ command: 'bold',
62
+ label: 'B',
63
+ title: 'Bold',
64
+ mark: 'bold'
65
+ }, {
66
+ command: 'italic',
67
+ label: 'I',
68
+ title: 'Italic',
69
+ mark: 'italic'
70
+ }, {
71
+ command: 'underline',
72
+ label: 'U',
73
+ title: 'Underline',
74
+ mark: 'underline'
75
+ }, {
76
+ command: 'strikeThrough',
77
+ label: 'S',
78
+ title: 'Strikethrough',
79
+ mark: 'strike'
80
+ }, {
81
+ command: 'superscript',
82
+ label: 'x²',
83
+ title: 'Superscript',
84
+ mark: 'plain'
85
+ }, {
86
+ command: 'subscript',
87
+ label: 'x₂',
88
+ title: 'Subscript',
89
+ mark: 'plain'
90
+ }];
91
+ const listButtons = [{
92
+ command: 'insertUnorderedList',
93
+ label: '•≡',
94
+ title: 'Bulleted list'
95
+ }, {
96
+ command: 'insertOrderedList',
97
+ label: '1≡',
98
+ title: 'Numbered list'
99
+ }];
100
+ const escapeHtml = text => text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>');
101
+ const readHtmlValue = bind => {
102
+ const value = bind[0][bind[1]];
103
+ return typeof value === 'string' ? value : '';
104
+ };
105
+ const writeHtmlValue = (bind, html) => {
106
+ // TypedBindVariable doesn't constrain O[K] to string, so this assumes the
107
+ // caller binds htmlBind to a string-valued field.
108
+ // eslint-disable-next-line no-type-assertion/no-type-assertion
109
+ bind[0][bind[1]] = html;
110
+ };
111
+ const writeTreeValue = (bind, tree) => {
112
+ // As writeHtmlValue above: assumes treeBind targets a
113
+ // StoredContentNode-typed field.
114
+ // eslint-disable-next-line no-type-assertion/no-type-assertion
115
+ bind[0][bind[1]] = tree;
116
+ };
117
+ export class RichTextEditor extends TungstenStatefulComponent {
118
+ constructor(...args) {
119
+ super(...args);
120
+ _defineProperty(this, "state", {
121
+ initialHtml: readHtmlValue(this.props.htmlBind),
122
+ sourceMode: false,
123
+ sourceDraft: '',
124
+ active: new Set(),
125
+ blockFormat: 'p',
126
+ loadedRecordKey: this.props.recordKey
127
+ });
128
+ _defineProperty(this, "surfaceRef", createElementRef());
129
+ _defineProperty(this, "sourceRef", createElementRef());
130
+ // Re-seeds the surface when the page points this editor at a different
131
+ // record. `initialHtml` is otherwise captured once and left alone (see
132
+ // State), which is what keeps the reconciler away from the live DOM — but
133
+ // that same stability means a record swap would leave the previous
134
+ // record's content on screen, and the next keystroke would commit it over
135
+ // the newly selected record. Writing innerHTML directly as well as
136
+ // updating initialHtml covers the case where the two records happen to
137
+ // share the stored string but the DOM has since been edited.
138
+ _defineProperty(this, "componentDidUpdate", () => {
139
+ const {
140
+ recordKey
141
+ } = this.props;
142
+ if (recordKey === this.state.loadedRecordKey) return;
143
+ const html = readHtmlValue(this.props.htmlBind);
144
+ this.state.loadedRecordKey = recordKey;
145
+ this.state.initialHtml = html;
146
+ this.state.sourceMode = false;
147
+ this.state.sourceDraft = '';
148
+ this.state.active = new Set();
149
+ if (this.surfaceRef.current !== null) this.surfaceRef.current.innerHTML = html;
150
+ });
151
+ // Derives the owning document from whichever host element is currently
152
+ // mounted, rather than the ambient global `document` — the global is
153
+ // only ever correct by accident of a real browser having exactly one,
154
+ // and a test harness (or a future iframe/multi-window use) may not
155
+ // define it at all.
156
+ _defineProperty(this, "ownerDocument", () => this.surfaceRef.current?.ownerDocument ?? this.sourceRef.current?.ownerDocument ?? null);
157
+ // Reads the surface as a string snapshot — the live editing DOM is never
158
+ // mutated here — then derives the tree from the *sanitized* string rather
159
+ // than from the raw one, so the two stored representations always describe
160
+ // the same content.
161
+ //
162
+ // Deriving them independently looked appealing (each through its own
163
+ // hardened pipeline) but is wrong: removing an element splits the text
164
+ // around it, so raw `<p>New <script>x</script>text</p>` parses to adjacent
165
+ // text nodes `['New ', 'text']`, while the sanitized string is the merged
166
+ // `<p>New text</p>`. Those render alike, but re-parsing the stored HTML
167
+ // would then disagree with the stored tree — and worse, a tree with
168
+ // adjacent text nodes renders to markup the browser re-parses as one text
169
+ // node, which is exactly the server/client shape mismatch that breaks
170
+ // hydration. Parsing the sanitized string keeps both allow-list passes
171
+ // (defence in depth) and yields one normalized text node.
172
+ _defineProperty(this, "commitSurface", () => {
173
+ const surface = this.surfaceRef.current;
174
+ if (surface === null) return;
175
+ const cleaned = sanitizeAuthoredHtml(surface.innerHTML, surface.ownerDocument);
176
+ writeHtmlValue(this.props.htmlBind, cleaned);
177
+ writeTreeValue(this.props.treeBind, serializeContentTree(parseHtml(cleaned, {
178
+ allowList: safeHtmlAllowList
179
+ })));
180
+ });
181
+ // The caret/selection last seen inside the surface. Toolbar controls that
182
+ // must take focus to work at all — the format <select> — necessarily blur
183
+ // the editable area first, and execCommand acts on whatever is selected
184
+ // when it runs, so the range is remembered here and put back before the
185
+ // command. Refocusing alone is not dependable enough to rely on.
186
+ _defineProperty(this, "savedRange", null);
187
+ _defineProperty(this, "captureSelection", () => {
188
+ const surface = this.surfaceRef.current;
189
+ const selection = this.ownerDocument()?.defaultView?.getSelection();
190
+ if (surface === null || selection === undefined || selection === null) return;
191
+ if (selection.rangeCount === 0) return;
192
+ const range = selection.getRangeAt(0);
193
+ // Ignore selections outside this editor, so an unrelated click
194
+ // elsewhere on the page cannot redirect a later command.
195
+ if (!surface.contains(range.commonAncestorContainer)) return;
196
+ this.savedRange = range.cloneRange();
197
+ });
198
+ _defineProperty(this, "restoreSelection", () => {
199
+ const selection = this.ownerDocument()?.defaultView?.getSelection();
200
+ if (this.savedRange === null || selection === undefined || selection === null) return;
201
+ selection.removeAllRanges();
202
+ selection.addRange(this.savedRange);
203
+ });
204
+ // Wired to the surface's keyup/mouseup/focus: both jobs want the same
205
+ // moments, whenever the caret may have moved.
206
+ _defineProperty(this, "selectionChanged", () => {
207
+ this.captureSelection();
208
+ this.refreshFormatState();
209
+ });
210
+ // Reflects the formatting at the caret onto the toolbar: which marks are
211
+ // lit, and which block the format menu shows.
212
+ //
213
+ // Read from the element ancestry (activeFormatsAt), not
214
+ // document.queryCommandState, which answers from computed style and so
215
+ // reports bold inside any heading.
216
+ _defineProperty(this, "refreshFormatState", () => {
217
+ const surface = this.surfaceRef.current;
218
+ const selection = this.ownerDocument()?.defaultView?.getSelection();
219
+ if (surface === null) return;
220
+ const at = selection === undefined || selection === null ? null : selection.focusNode;
221
+ const {
222
+ block,
223
+ marks
224
+ } = activeFormatsAt(at, surface);
225
+ this.state.active = new Set(trackedCommands.filter(command => marks.has(command)));
226
+ // Anything the menu has no option for — bare text, or a <div> the
227
+ // browser made — is Normal text, which is what it would become on save.
228
+ this.state.blockFormat = block !== null && knownBlocks.has(block) ? block : 'p';
229
+ });
230
+ _defineProperty(this, "exec", (command, value) => {
231
+ const doc = this.ownerDocument();
232
+ if (doc === null) return;
233
+ this.surfaceRef.current?.focus();
234
+ this.restoreSelection();
235
+ doc.execCommand(command, false, value);
236
+ this.captureSelection();
237
+ this.commitSurface();
238
+ this.refreshFormatState();
239
+ });
240
+ // Buttons take focus on click, which would drop the surface's selection
241
+ // before the click handler runs; preventing the mousedown keeps it.
242
+ //
243
+ // This must NOT go on the format <select>: opening the dropdown *is* that
244
+ // element's default mousedown action, so preventing it makes the control
245
+ // completely dead to the mouse. The select instead relies on
246
+ // saveRange/restoreSelection around exec().
247
+ _defineProperty(this, "preventFocusLoss", event => {
248
+ event.preventDefault();
249
+ });
250
+ _defineProperty(this, "changeFormatBlock", event => {
251
+ const tag = event.currentTarget.value;
252
+ if (tag === '') return;
253
+ // No reset afterwards: exec() ends in refreshFormatState(), which sets
254
+ // the menu from the caret's actual block, so it now shows the applied
255
+ // format the way a format menu is expected to.
256
+ this.exec('formatBlock', `<${tag}>`);
257
+ });
258
+ _defineProperty(this, "insertLink", () => {
259
+ // Via the surface's own window rather than the ambient global, for the
260
+ // same reason ownerDocument() exists.
261
+ const url = this.ownerDocument()?.defaultView?.prompt('Link URL');
262
+ if (url === undefined || url === null || url.trim() === '') return;
263
+ this.exec('createLink', url.trim());
264
+ });
265
+ _defineProperty(this, "removeLink", () => this.exec('unlink'));
266
+ _defineProperty(this, "clearFormatting", () => this.exec('removeFormat'));
267
+ _defineProperty(this, "undo", () => this.exec('undo'));
268
+ _defineProperty(this, "redo", () => this.exec('redo'));
269
+ // Enter inserts a line break instead of letting the browser open a new
270
+ // block. Browsers disagree over whether that block is a <p> or a <div>,
271
+ // and leave empty ones behind as content is edited — the markup this
272
+ // editor produces stays inline, with deliberate blocks (headings, quotes,
273
+ // lists) applied explicitly from the toolbar. Shift+Enter already means
274
+ // "line break", so it is left to the browser.
275
+ _defineProperty(this, "handleKeyDown", event => {
276
+ if (event.key !== 'Enter' || event.shiftKey) return;
277
+ const doc = this.ownerDocument();
278
+ if (doc === null) return;
279
+ event.preventDefault();
280
+ doc.execCommand('insertLineBreak');
281
+ this.commitSurface();
282
+ this.selectionChanged();
283
+ });
284
+ _defineProperty(this, "handlePaste", event => {
285
+ event.preventDefault();
286
+ const doc = event.currentTarget.ownerDocument;
287
+ const rawHtml = event.clipboardData.getData('text/html');
288
+ const cleaned = rawHtml !== '' ? sanitizeAuthoredHtml(rawHtml, doc) : escapeHtml(event.clipboardData.getData('text/plain'));
289
+ doc.execCommand('insertHTML', false, cleaned);
290
+ this.commitSurface();
291
+ });
292
+ _defineProperty(this, "toggleSource", () => {
293
+ if (this.state.sourceMode) {
294
+ const cleaned = sanitizeAuthoredHtml(this.state.sourceDraft, this.ownerDocument() ?? undefined);
295
+ this.state.initialHtml = cleaned;
296
+ this.state.sourceMode = false;
297
+ writeHtmlValue(this.props.htmlBind, cleaned);
298
+ const cleanedTree = parseHtml(cleaned, {
299
+ allowList: safeHtmlAllowList
300
+ });
301
+ writeTreeValue(this.props.treeBind, serializeContentTree(cleanedTree));
302
+ } else {
303
+ const current = this.surfaceRef.current?.innerHTML ?? readHtmlValue(this.props.htmlBind);
304
+ this.state.sourceDraft = current;
305
+ this.state.sourceMode = true;
306
+ }
307
+ });
308
+ _defineProperty(this, "changeSourceDraft", event => {
309
+ this.state.sourceDraft = event.currentTarget.value;
310
+ });
311
+ _defineProperty(this, "content", ({
312
+ className,
313
+ name,
314
+ placeholder
315
+ }, state) => _jsxs("div", {
316
+ className: combineClassNames(className, 'editor'),
317
+ children: [_jsxs("div", {
318
+ className: "toolbar",
319
+ role: "toolbar",
320
+ children: [_jsx("select", {
321
+ className: "format-block",
322
+ value: state.blockFormat,
323
+ onChange: this.changeFormatBlock,
324
+ disabled: state.sourceMode,
325
+ "aria-label": "Paragraph style",
326
+ children: formatBlockOptions.map(option => _jsx("option", {
327
+ value: option.value,
328
+ children: option.text
329
+ }, option.value))
330
+ }), _jsx("span", {
331
+ className: "group",
332
+ children: formattingButtons.map(item => _jsx("button", {
333
+ type: "button",
334
+ title: item.title,
335
+ className: combineClassNames('tool', item.mark, state.active.has(item.command) && 'active'),
336
+ disabled: state.sourceMode,
337
+ onMouseDown: this.preventFocusLoss,
338
+ onClick: () => this.exec(item.command),
339
+ children: item.label
340
+ }, item.command))
341
+ }), _jsx("span", {
342
+ className: "group",
343
+ children: listButtons.map(item => _jsx("button", {
344
+ type: "button",
345
+ title: item.title,
346
+ className: combineClassNames('tool', state.active.has(item.command) && 'active'),
347
+ disabled: state.sourceMode,
348
+ onMouseDown: this.preventFocusLoss,
349
+ onClick: () => this.exec(item.command),
350
+ children: item.label
351
+ }, item.command))
352
+ }), _jsxs("span", {
353
+ className: "group",
354
+ children: [_jsx("button", {
355
+ type: "button",
356
+ title: "Insert link",
357
+ className: "tool",
358
+ disabled: state.sourceMode,
359
+ onMouseDown: this.preventFocusLoss,
360
+ onClick: this.insertLink,
361
+ children: "Link"
362
+ }), _jsx("button", {
363
+ type: "button",
364
+ title: "Remove link",
365
+ className: "tool",
366
+ disabled: state.sourceMode,
367
+ onMouseDown: this.preventFocusLoss,
368
+ onClick: this.removeLink,
369
+ children: "Unlink"
370
+ })]
371
+ }), _jsxs("span", {
372
+ className: "group",
373
+ children: [_jsx("button", {
374
+ type: "button",
375
+ title: "Clear formatting",
376
+ className: "tool",
377
+ disabled: state.sourceMode,
378
+ onMouseDown: this.preventFocusLoss,
379
+ onClick: this.clearFormatting,
380
+ children: "Clear"
381
+ }), _jsx("button", {
382
+ type: "button",
383
+ title: "Undo",
384
+ className: "tool",
385
+ disabled: state.sourceMode,
386
+ onMouseDown: this.preventFocusLoss,
387
+ onClick: this.undo,
388
+ children: "\u21B6"
389
+ }), _jsx("button", {
390
+ type: "button",
391
+ title: "Redo",
392
+ className: "tool",
393
+ disabled: state.sourceMode,
394
+ onMouseDown: this.preventFocusLoss,
395
+ onClick: this.redo,
396
+ children: "\u21B7"
397
+ })]
398
+ }), _jsx("button", {
399
+ type: "button",
400
+ title: "View HTML source",
401
+ className: combineClassNames('tool', 'source-toggle', state.sourceMode && 'active'),
402
+ onMouseDown: this.preventFocusLoss,
403
+ onClick: this.toggleSource,
404
+ children: "HTML"
405
+ })]
406
+ }), state.sourceMode ? _jsx("textarea", {
407
+ className: "source",
408
+ name: name,
409
+ ref: this.sourceRef,
410
+ value: state.sourceDraft,
411
+ onInput: this.changeSourceDraft
412
+ })
413
+ // The explicit string rather than a bare/boolean prop. Tungsten
414
+ // now renders either as `contenteditable="true"`, so this is no
415
+ // longer working around anything — it just states the value of
416
+ // a three-state enumerated attribute (true/false/inherit)
417
+ // rather than leaving it to a boolean-prop convention.
418
+ : _jsx("div", {
419
+ className: "surface",
420
+ id: name,
421
+ ref: this.surfaceRef,
422
+ contentEditable: "true",
423
+ "data-placeholder": placeholder ?? '',
424
+ dangerouslySetInnerHTML: {
425
+ __html: state.initialHtml
426
+ },
427
+ onInput: this.commitSurface,
428
+ onPaste: this.handlePaste,
429
+ onKeyDown: this.handleKeyDown,
430
+ onKeyUp: this.selectionChanged,
431
+ onMouseUp: this.selectionChanged,
432
+ onFocus: this.selectionChanged
433
+ })]
434
+ }));
435
+ _defineProperty(this, "style", {
436
+ "moduleName": "T#-components-library-RichTextEditor-RichTextEditor",
437
+ "tags": {
438
+ "select": "T#-components-library-RichTextEditor-RichTextEditor_#_select"
439
+ },
440
+ "classes": {
441
+ "editor": "T#-components-library-RichTextEditor-RichTextEditor_editor",
442
+ "toolbar": "T#-components-library-RichTextEditor-RichTextEditor_toolbar",
443
+ "group": "T#-components-library-RichTextEditor-RichTextEditor_group",
444
+ "format-block": "T#-components-library-RichTextEditor-RichTextEditor_format-block",
445
+ "tool": "T#-components-library-RichTextEditor-RichTextEditor_tool",
446
+ "active": "T#-components-library-RichTextEditor-RichTextEditor_active",
447
+ "bold": "T#-components-library-RichTextEditor-RichTextEditor_bold",
448
+ "italic": "T#-components-library-RichTextEditor-RichTextEditor_italic",
449
+ "underline": "T#-components-library-RichTextEditor-RichTextEditor_underline",
450
+ "strike": "T#-components-library-RichTextEditor-RichTextEditor_strike",
451
+ "source-toggle": "T#-components-library-RichTextEditor-RichTextEditor_source-toggle",
452
+ "surface": "T#-components-library-RichTextEditor-RichTextEditor_surface",
453
+ "source": "T#-components-library-RichTextEditor-RichTextEditor_source"
454
+ }
455
+ });
456
+ }
457
+ static $stylesheet = "i/T-components-library-RichTextEditor-RichTextEditor.css";
458
+ }
@@ -0,0 +1,5 @@
1
+ export type StoredContentNode = string | number | null | StoredContentNode[] | {
2
+ tag: string;
3
+ props: Record<string, unknown>;
4
+ };
5
+ //# sourceMappingURL=StoredContentNode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StoredContentNode.d.ts","sourceRoot":"","sources":["../../../../src/components/library/RichTextEditor/StoredContentNode.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,iBAAiB,GACvB,MAAM,GACN,MAAM,GACN,IAAI,GACJ,iBAAiB,EAAE,GACnB;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAA"}
@@ -0,0 +1,20 @@
1
+ export type ActiveFormats = {
2
+ /** Tag of the nearest block element inside the surface, or null. */
3
+ block: string | null;
4
+ /** execCommand names for the inline marks in force at this point. */
5
+ marks: ReadonlySet<string>;
6
+ };
7
+ /**
8
+ * The formatting in force at a point in the editable surface, read from the
9
+ * element ancestry rather than `document.queryCommandState`.
10
+ *
11
+ * queryCommandState answers from computed style, so it reports `bold` as true
12
+ * anywhere inside a heading — headings render bold — which lights up the Bold
13
+ * button misleadingly and invites a redundant `<strong>` inside an `<h2>`.
14
+ * Walking the ancestors instead means only real markup counts.
15
+ *
16
+ * The walk stops at `root` so formatting *around* the editor cannot leak into
17
+ * its toolbar state.
18
+ */
19
+ export declare function activeFormatsAt(node: Node | null, root: Element): ActiveFormats;
20
+ //# sourceMappingURL=activeFormats.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activeFormats.d.ts","sourceRoot":"","sources":["../../../../src/components/library/RichTextEditor/activeFormats.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,aAAa,GAAG;IACxB,oEAAoE;IACpE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,qEAAqE;IACrE,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CAC7B,CAAA;AAmBD;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,EAAE,IAAI,EAAE,OAAO,GAAG,aAAa,CAe/E"}
@@ -0,0 +1,48 @@
1
+ // Tag to the execCommand that produces it. Several tags map to one command
2
+ // because browsers differ over which they emit (`b`/`strong`, `s`/`strike`/`del`).
3
+ const markCommands = {
4
+ b: 'bold',
5
+ strong: 'bold',
6
+ i: 'italic',
7
+ em: 'italic',
8
+ u: 'underline',
9
+ s: 'strikeThrough',
10
+ strike: 'strikeThrough',
11
+ del: 'strikeThrough',
12
+ sub: 'subscript',
13
+ sup: 'superscript',
14
+ ul: 'insertUnorderedList',
15
+ ol: 'insertOrderedList'
16
+ };
17
+ const blockTags = new Set(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'div']);
18
+ function NodeIsElement(node) {
19
+ return node.nodeType === 1;
20
+ }
21
+ /**
22
+ * The formatting in force at a point in the editable surface, read from the
23
+ * element ancestry rather than `document.queryCommandState`.
24
+ *
25
+ * queryCommandState answers from computed style, so it reports `bold` as true
26
+ * anywhere inside a heading — headings render bold — which lights up the Bold
27
+ * button misleadingly and invites a redundant `<strong>` inside an `<h2>`.
28
+ * Walking the ancestors instead means only real markup counts.
29
+ *
30
+ * The walk stops at `root` so formatting *around* the editor cannot leak into
31
+ * its toolbar state.
32
+ */
33
+ export function activeFormatsAt(node, root) {
34
+ const marks = new Set();
35
+ let block = null;
36
+ for (let current = node; current !== null && current !== root; current = current.parentNode) {
37
+ if (!NodeIsElement(current)) continue;
38
+ const tag = current.tagName.toLowerCase();
39
+ const command = markCommands[tag];
40
+ if (command !== undefined) marks.add(command);
41
+ // nearest block wins, so only take the first one met on the way up
42
+ if (block === null && blockTags.has(tag)) block = tag;
43
+ }
44
+ return {
45
+ block,
46
+ marks
47
+ };
48
+ }
@@ -0,0 +1,30 @@
1
+ import type { ContentNode } from '../../../ContentNode.js';
2
+ import type { HtmlAllowList } from '../../../rawHtml/HtmlAllowList.js';
3
+ import type { StoredContentNode } from './StoredContentNode.js';
4
+ /**
5
+ * Converts a Tungsten content tree into the JSON-safe StoredContentNode shape
6
+ * for persistence/transport. Only host HTML nodes are supported — the shape
7
+ * `parseHtml` produces — never component elements, which have no meaningful
8
+ * serialized form.
9
+ */
10
+ export declare function serializeContentTree(node: ContentNode): StoredContentNode;
11
+ /**
12
+ * Rebuilds a Tungsten content tree from stored JSON, filtered against the
13
+ * allow list.
14
+ *
15
+ * Filtering on *read* is deliberate and not redundant with the write side.
16
+ * Storing a pre-parsed tree is what makes rendering cheap — no re-parse per
17
+ * render — but it also means the stored value is handed to the renderer
18
+ * without passing back through `parseHtml`, which is where `<RawHtml safe>`
19
+ * would otherwise re-apply the allow list every time. Anything that ever put
20
+ * an unfiltered tree in the column (an endpoint bug, a hand-edited row) would
21
+ * then render unchecked, including `on*` props that Tungsten binds as event
22
+ * listeners.
23
+ *
24
+ * The rules themselves are Tungsten's `filterContentNode`, which makes the same
25
+ * decisions `parseHtml` makes while parsing — so the stored-tree path, the
26
+ * editor's write path and `<RawHtml safe>` all agree by construction rather
27
+ * than by three implementations being kept in step.
28
+ */
29
+ export declare function deserializeContentTree(stored: StoredContentNode, allowList?: HtmlAllowList): ContentNode;
30
+ //# sourceMappingURL=contentTree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contentTree.d.ts","sourceRoot":"","sources":["../../../../src/components/library/RichTextEditor/contentTree.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAA8B,MAAM,yBAAyB,CAAA;AAGtF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAA;AACtE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAE/D;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,WAAW,GAAG,iBAAiB,CAezE;AA6BD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,sBAAsB,CAClC,MAAM,EAAE,iBAAiB,EAAE,SAAS,GAAE,aAAiC,GACxE,WAAW,CAEb"}
@@ -0,0 +1,81 @@
1
+ import { $props, $type } from "../../../contentNodeSymbols.js";
2
+ import { ContentNodeIsArray, ContentNodeIsHtml, ContentNodeIsLiteral, ContentNodeIsNonRendered } from "../../../ContentNode.js";
3
+ import { filterContentNode } from "../../../rawHtml/filterContentNode.js";
4
+ import { safeHtmlAllowList } from "../../../rawHtml/safeHtmlAllowList.js";
5
+ /**
6
+ * Converts a Tungsten content tree into the JSON-safe StoredContentNode shape
7
+ * for persistence/transport. Only host HTML nodes are supported — the shape
8
+ * `parseHtml` produces — never component elements, which have no meaningful
9
+ * serialized form.
10
+ */
11
+ export function serializeContentTree(node) {
12
+ if (ContentNodeIsNonRendered(node)) return null;
13
+ if (ContentNodeIsArray(node)) return node.map(serializeContentTree);
14
+ if (ContentNodeIsLiteral(node)) return node;
15
+ if (!ContentNodeIsHtml(node)) {
16
+ throw new Error('serializeContentTree only supports host HTML nodes (e.g. from parseHtml), not component elements.');
17
+ }
18
+ const {
19
+ children,
20
+ ...attributes
21
+ } = node[$props];
22
+ return {
23
+ tag: node[$type],
24
+ props: children === undefined ? attributes : {
25
+ ...attributes,
26
+ children: serializeContentTree(children)
27
+ }
28
+ };
29
+ }
30
+ /**
31
+ * Rebuilds the tree's shape from stored JSON, and nothing else. Whatever is in
32
+ * the column is reproduced faithfully, including markup that must not survive
33
+ * to a render — filtering is `deserializeContentTree`'s job, in one pass over
34
+ * the finished tree, so that this stays a plain structural inverse of
35
+ * `serializeContentTree`.
36
+ */
37
+ function rebuildContentTree(stored) {
38
+ if (stored === null) return null;
39
+ if (Array.isArray(stored)) return stored.map(rebuildContentTree);
40
+ if (typeof stored !== 'object') return stored;
41
+ const {
42
+ children,
43
+ ...attributes
44
+ } = stored.props;
45
+ const props = {
46
+ ...attributes
47
+ };
48
+ if (children !== undefined) {
49
+ // Storage round-trip boundary: `children` holds whatever
50
+ // serializeContentTree put there.
51
+ // eslint-disable-next-line no-type-assertion/no-type-assertion
52
+ props.children = rebuildContentTree(children);
53
+ }
54
+ // Lowercased to match what parseHtml produces, so a hand-edited row cannot
55
+ // present a tag the allow list would then fail to recognise.
56
+ return {
57
+ [$type]: stored.tag.toLowerCase(),
58
+ [$props]: props
59
+ };
60
+ }
61
+ /**
62
+ * Rebuilds a Tungsten content tree from stored JSON, filtered against the
63
+ * allow list.
64
+ *
65
+ * Filtering on *read* is deliberate and not redundant with the write side.
66
+ * Storing a pre-parsed tree is what makes rendering cheap — no re-parse per
67
+ * render — but it also means the stored value is handed to the renderer
68
+ * without passing back through `parseHtml`, which is where `<RawHtml safe>`
69
+ * would otherwise re-apply the allow list every time. Anything that ever put
70
+ * an unfiltered tree in the column (an endpoint bug, a hand-edited row) would
71
+ * then render unchecked, including `on*` props that Tungsten binds as event
72
+ * listeners.
73
+ *
74
+ * The rules themselves are Tungsten's `filterContentNode`, which makes the same
75
+ * decisions `parseHtml` makes while parsing — so the stored-tree path, the
76
+ * editor's write path and `<RawHtml safe>` all agree by construction rather
77
+ * than by three implementations being kept in step.
78
+ */
79
+ export function deserializeContentTree(stored, allowList = safeHtmlAllowList) {
80
+ return filterContentNode(rebuildContentTree(stored), allowList);
81
+ }