@37signals/lexxy 0.7.4-beta → 0.7.6-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lexxy.esm.js CHANGED
@@ -10,20 +10,20 @@ import 'prismjs/components/prism-json';
10
10
  import 'prismjs/components/prism-diff';
11
11
  import DOMPurify from 'dompurify';
12
12
  import { getStyleObjectFromCSS, getCSSFromStyleObject, $getSelectionStyleValueForProperty, $patchStyleText } from '@lexical/selection';
13
- import { HISTORY_MERGE_TAG, SKIP_DOM_SELECTION_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $isTextNode, TextNode, $isRangeSelection, $getSelection, DecoratorNode, $getEditor, FORMAT_TEXT_COMMAND, $createTextNode, $isRootOrShadowRoot, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, KEY_TAB_COMMAND, COMMAND_PRIORITY_NORMAL, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $isNodeSelection, $getRoot, $isLineBreakNode, $isElementNode, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_DELETE_COMMAND, KEY_BACKSPACE_COMMAND, SELECTION_CHANGE_COMMAND, $getNodeByKey, $createNodeSelection, $setSelection, $createParagraphNode, KEY_ENTER_COMMAND, COMMAND_PRIORITY_HIGH, $isParagraphNode, $insertNodes, $createLineBreakNode, PASTE_TAG, createCommand, createState, defineExtension, $setState, $getState, $hasUpdateTag, CLEAR_HISTORY_COMMAND, $addUpdateTag, KEY_SPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
14
- import { $isListItemNode, $isListNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, ListNode, $getListDepth, $createListNode, ListItemNode, registerList } from '@lexical/list';
15
- import { $isQuoteNode, $isHeadingNode, $createQuoteNode, $createHeadingNode, RichTextExtension, QuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
16
- import { $isCodeNode, CodeNode, normalizeCodeLang, CodeHighlightNode, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
17
- import { $isLinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, LinkNode, AutoLinkNode } from '@lexical/link';
18
- import { $getTableCellNodeFromLexicalNode, INSERT_TABLE_COMMAND, TableCellNode, TableNode, TableRowNode, registerTablePlugin, registerTableSelectionObserver, setScrollableTablesActive, TableCellHeaderStates, $insertTableRowAtSelection, $insertTableColumnAtSelection, $deleteTableRowAtSelection, $deleteTableColumnAtSelection, $findTableNode, $getTableRowIndexFromTableCellNode, $getTableColumnIndexFromTableCellNode, $findCellNode, $getElementForTableNode } from '@lexical/table';
19
- import { createElement, createAttachmentFigure, isPreviewableImage, dispatchCustomEvent, parseHtml, dispatch, generateDomId } from './lexxy_helpers.esm.js';
20
- export { highlightCode as highlightAll, highlightCode } from './lexxy_helpers.esm.js';
13
+ import { SKIP_DOM_SELECTION_TAG, $getSelection, $isRangeSelection, DecoratorNode, $getEditor, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $createNodeSelection, $isTextNode, TextNode, createCommand, createState, defineExtension, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, $createTextNode, $isRootOrShadowRoot, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $isNodeSelection, $getRoot, $isLineBreakNode, $isElementNode, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, DELETE_CHARACTER_COMMAND, SELECTION_CHANGE_COMMAND, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, $isDecoratorNode, $createParagraphNode, $setSelection, KEY_ENTER_COMMAND, COMMAND_PRIORITY_HIGH, $isParagraphNode, $insertNodes, $getNodeByKey, $createLineBreakNode, ParagraphNode, RootNode, CLEAR_HISTORY_COMMAND, $addUpdateTag, KEY_SPACE_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
21
14
  import { buildEditorFromExtensions } from '@lexical/extension';
15
+ import { ListNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, ListItemNode, $getListDepth, $isListItemNode, $isListNode, $createListNode, registerList } from '@lexical/list';
16
+ import { $createAutoLinkNode, $toggleLink, LinkNode, $createLinkNode, AutoLinkNode, $isLinkNode } from '@lexical/link';
22
17
  import { registerPlainText } from '@lexical/plain-text';
18
+ import { RichTextExtension, $isQuoteNode, $createQuoteNode, $createHeadingNode, $isHeadingNode, QuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
23
19
  import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
20
+ import { $isCodeNode, CodeNode, normalizeCodeLang, CodeHighlightNode, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
24
21
  import { registerMarkdownShortcuts, TRANSFORMERS } from '@lexical/markdown';
25
22
  import { createEmptyHistoryState, registerHistory } from '@lexical/history';
26
- import { $getNearestNodeOfType } from '@lexical/utils';
23
+ import { createElement, createAttachmentFigure, isPreviewableImage, parseHtml, dispatch, generateDomId } from './lexxy_helpers.esm.js';
24
+ export { highlightCode as highlightAll, highlightCode } from './lexxy_helpers.esm.js';
25
+ import { $getNearestNodeOfType, mergeRegister, $insertFirst, $firstToLastIterator, $descendantsMatching } from '@lexical/utils';
26
+ import { INSERT_TABLE_COMMAND, $getTableCellNodeFromLexicalNode, TableCellNode, TableNode, TableRowNode, registerTablePlugin, registerTableSelectionObserver, setScrollableTablesActive, TableCellHeaderStates, $insertTableRowAtSelection, $insertTableColumnAtSelection, $deleteTableRowAtSelection, $deleteTableColumnAtSelection, $findTableNode, $getTableRowIndexFromTableCellNode, $getTableColumnIndexFromTableCellNode, $findCellNode, $getElementForTableNode } from '@lexical/table';
27
27
  import { marked } from 'marked';
28
28
  import { $insertDataTransferForRichText } from '@lexical/clipboard';
29
29
 
@@ -156,137 +156,6 @@ function getNonce() {
156
156
  return element?.content
157
157
  }
158
158
 
159
- const SILENT_UPDATE_TAGS = [ HISTORY_MERGE_TAG, SKIP_DOM_SELECTION_TAG, SKIP_SCROLL_INTO_VIEW_TAG ];
160
-
161
- function getNearestListItemNode(node) {
162
- let current = node;
163
- while (current !== null) {
164
- if ($isListItemNode(current)) return current
165
- current = current.getParent();
166
- }
167
- return null
168
- }
169
-
170
- function getListType(node) {
171
- let current = node;
172
- while (current) {
173
- if ($isListNode(current)) {
174
- return current.getListType()
175
- }
176
- current = current.getParent();
177
- }
178
- return null
179
- }
180
-
181
- function isPrintableCharacter(event) {
182
- // Ignore if modifier keys are pressed (except Shift for uppercase)
183
- if (event.ctrlKey || event.metaKey || event.altKey) return false
184
-
185
- // Ignore special keys
186
- if (event.key.length > 1 && event.key !== "Enter" && event.key !== "Space") return false
187
-
188
- // Accept single character keys (letters, numbers, punctuation)
189
- return event.key.length === 1
190
- }
191
-
192
- function extendTextNodeConversion(conversionName, ...callbacks) {
193
- return extendConversion(TextNode, conversionName, (conversionOutput, element) => ({
194
- ...conversionOutput,
195
- forChild: (lexicalNode, parentNode) => {
196
- const originalForChild = conversionOutput?.forChild ?? (x => x);
197
- let childNode = originalForChild(lexicalNode, parentNode);
198
-
199
-
200
- if ($isTextNode(childNode)) {
201
- childNode = callbacks.reduce(
202
- (childNode, callback) => callback(childNode, element) ?? childNode,
203
- childNode
204
- );
205
- return childNode
206
- }
207
- }
208
- }))
209
- }
210
-
211
- function extendConversion(nodeKlass, conversionName, callback = (output => output)) {
212
- return (element) => {
213
- const converter = nodeKlass.importDOM()?.[conversionName]?.(element);
214
- if (!converter) return null
215
-
216
- const conversionOutput = converter.conversion(element);
217
- if (!conversionOutput) return conversionOutput
218
-
219
- return callback(conversionOutput, element) ?? conversionOutput
220
- }
221
- }
222
-
223
- function isSelectionHighlighted(selection) {
224
- if (!$isRangeSelection(selection)) return false
225
-
226
- if (selection.isCollapsed()) {
227
- return hasHighlightStyles(selection.style)
228
- } else {
229
- return selection.hasFormat("highlight")
230
- }
231
- }
232
-
233
- function hasHighlightStyles(cssOrStyles) {
234
- const styles = typeof cssOrStyles === "string" ? getStyleObjectFromCSS(cssOrStyles) : cssOrStyles;
235
- return !!(styles.color || styles["background-color"])
236
- }
237
-
238
- class StyleCanonicalizer {
239
- constructor(property, allowedValues= []) {
240
- this._property = property;
241
- this._allowedValues = allowedValues;
242
- this._canonicalValues = this.#allowedValuesIdentityObject;
243
- }
244
-
245
- applyCanonicalization(css) {
246
- const styles = { ...getStyleObjectFromCSS(css) };
247
-
248
- styles[this._property] = this.getCanonicalAllowedValue(styles[this._property]);
249
- if (!styles[this._property]) {
250
- delete styles[this._property];
251
- }
252
-
253
- return getCSSFromStyleObject(styles)
254
- }
255
-
256
- getCanonicalAllowedValue(value) {
257
- return this._canonicalValues[value] ||= this.#resolveCannonicalValue(value)
258
- }
259
-
260
- // Private
261
-
262
- get #allowedValuesIdentityObject() {
263
- return this._allowedValues.reduce((object, value) => ({ ...object, [value]: value }), {})
264
- }
265
-
266
- #resolveCannonicalValue(value) {
267
- let index = this.#computedAllowedValues.indexOf(value);
268
- index ||= this.#computedAllowedValues.indexOf(getComputedStyleForProperty(this._property, value));
269
- return index === -1 ? null : this._allowedValues[index]
270
- }
271
-
272
- get #computedAllowedValues() {
273
- return this._computedAllowedValues ||= this._allowedValues.map(
274
- value => getComputedStyleForProperty(this._property, value)
275
- )
276
- }
277
- }
278
-
279
- function getComputedStyleForProperty(property, value) {
280
- const style = `${property}: ${value};`;
281
-
282
- // the element has to be attached to the DOM have computed styles
283
- const element = document.body.appendChild(createElement("span", { style: "display: none;" + style }));
284
- const computedStyle = window.getComputedStyle(element).getPropertyValue(property);
285
- element.remove();
286
-
287
- return computedStyle
288
- }
289
-
290
159
  function handleRollingTabIndex(elements, event) {
291
160
  const previousActiveElement = document.activeElement;
292
161
 
@@ -406,6 +275,7 @@ class LexicalToolbarElement extends HTMLElement {
406
275
  setEditor(editorElement) {
407
276
  this.editorElement = editorElement;
408
277
  this.editor = editorElement.editor;
278
+ this.selection = editorElement.selection;
409
279
  this.#bindButtons();
410
280
  this.#bindHotkeys();
411
281
  this.#resetTabIndexValues();
@@ -565,19 +435,8 @@ class LexicalToolbarElement extends HTMLElement {
565
435
  const anchorNode = selection.anchor.getNode();
566
436
  if (!anchorNode.getParent()) { return }
567
437
 
568
- const topLevelElement = anchorNode.getTopLevelElementOrThrow();
569
-
570
- const isBold = selection.hasFormat("bold");
571
- const isItalic = selection.hasFormat("italic");
572
- const isStrikethrough = selection.hasFormat("strikethrough");
573
- const isHighlight = isSelectionHighlighted(selection);
574
- const isInLink = this.#isInLink(anchorNode);
575
- const isInQuote = $isQuoteNode(topLevelElement);
576
- const isInHeading = $isHeadingNode(topLevelElement);
577
- const isInCode = $isCodeNode(topLevelElement) || selection.hasFormat("code");
578
- const isInList = this.#isInList(anchorNode);
579
- const listType = getListType(anchorNode);
580
- const isInTable = $getTableCellNodeFromLexicalNode(anchorNode) !== null;
438
+ const { isBold, isItalic, isStrikethrough, isHighlight, isInLink, isInQuote, isInHeading,
439
+ isInCode, isInList, listType, isInTable } = this.selection.getFormat();
581
440
 
582
441
  this.#setButtonPressed("bold", isBold);
583
442
  this.#setButtonPressed("italic", isItalic);
@@ -594,24 +453,6 @@ class LexicalToolbarElement extends HTMLElement {
594
453
  this.#updateUndoRedoButtonStates();
595
454
  }
596
455
 
597
- #isInList(node) {
598
- let current = node;
599
- while (current) {
600
- if ($isListNode(current) || $isListItemNode(current)) return true
601
- current = current.getParent();
602
- }
603
- return false
604
- }
605
-
606
- #isInLink(node) {
607
- let current = node;
608
- while (current) {
609
- if ($isLinkNode(current)) return true
610
- current = current.getParent();
611
- }
612
- return false
613
- }
614
-
615
456
  #setButtonPressed(name, isPressed) {
616
457
  const button = this.querySelector(`[name="${name}"]`);
617
458
  if (button) {
@@ -975,10 +816,6 @@ class ActionTextAttachmentNode extends DecoratorNode {
975
816
  createDOM() {
976
817
  const figure = this.createAttachmentFigure();
977
818
 
978
- figure.addEventListener("click", () => {
979
- this.#select(figure);
980
- });
981
-
982
819
  if (this.isPreviewableAttachment) {
983
820
  figure.appendChild(this.#createDOMForImage());
984
821
  figure.appendChild(this.#createEditableCaption());
@@ -1091,10 +928,6 @@ class ActionTextAttachmentNode extends DecoratorNode {
1091
928
  return figcaption
1092
929
  }
1093
930
 
1094
- #select(figure) {
1095
- dispatchCustomEvent(figure, "lexxy:internal:select-node", { key: this.getKey() });
1096
- }
1097
-
1098
931
  #createEditableCaption() {
1099
932
  const caption = createElement("figcaption", { className: "attachment__caption" });
1100
933
  const input = createElement("textarea", {
@@ -1125,14 +958,62 @@ class ActionTextAttachmentNode extends DecoratorNode {
1125
958
 
1126
959
  #handleCaptionInputKeydown(event) {
1127
960
  if (event.key === "Enter") {
1128
- this.#updateCaptionValueFromInput(event.target);
1129
961
  event.preventDefault();
962
+ event.stopPropagation();
963
+ event.target.blur();
1130
964
 
1131
965
  this.editor.update(() => {
1132
- this.selectNext();
1133
- }, { tag: HISTORY_MERGE_TAG });
966
+ // Place the cursor after the current image
967
+ this.selectNext(0, 0);
968
+ }, {
969
+ tag: HISTORY_MERGE_TAG
970
+ });
1134
971
  }
1135
- event.stopPropagation();
972
+
973
+ }
974
+ }
975
+
976
+ const SILENT_UPDATE_TAGS = [ HISTORY_MERGE_TAG, SKIP_DOM_SELECTION_TAG, SKIP_SCROLL_INTO_VIEW_TAG ];
977
+
978
+ function $createNodeSelectionWith(...nodes) {
979
+ const selection = $createNodeSelection();
980
+ nodes.forEach(node => selection.add(node.getKey()));
981
+ return selection
982
+ }
983
+
984
+ function getListType(node) {
985
+ const list = $getNearestNodeOfType(node, ListNode);
986
+ return list?.getListType() ?? null
987
+ }
988
+
989
+ function extendTextNodeConversion(conversionName, ...callbacks) {
990
+ return extendConversion(TextNode, conversionName, (conversionOutput, element) => ({
991
+ ...conversionOutput,
992
+ forChild: (lexicalNode, parentNode) => {
993
+ const originalForChild = conversionOutput?.forChild ?? (x => x);
994
+ let childNode = originalForChild(lexicalNode, parentNode);
995
+
996
+
997
+ if ($isTextNode(childNode)) {
998
+ childNode = callbacks.reduce(
999
+ (childNode, callback) => callback(childNode, element) ?? childNode,
1000
+ childNode
1001
+ );
1002
+ return childNode
1003
+ }
1004
+ }
1005
+ }))
1006
+ }
1007
+
1008
+ function extendConversion(nodeKlass, conversionName, callback = (output => output)) {
1009
+ return (element) => {
1010
+ const converter = nodeKlass.importDOM()?.[conversionName]?.(element);
1011
+ if (!converter) return null
1012
+
1013
+ const conversionOutput = converter.conversion(element);
1014
+ if (!conversionOutput) return conversionOutput
1015
+
1016
+ return callback(conversionOutput, element) ?? conversionOutput
1136
1017
  }
1137
1018
  }
1138
1019
 
@@ -1219,8 +1100,7 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
1219
1100
  }
1220
1101
 
1221
1102
  exportDOM() {
1222
- const img = document.createElement("img");
1223
- return { element: img }
1103
+ return { element: null }
1224
1104
  }
1225
1105
 
1226
1106
  exportJSON() {
@@ -1429,10 +1309,6 @@ class HorizontalDividerNode extends DecoratorNode {
1429
1309
  const figure = createElement("figure", { className: "horizontal-divider" });
1430
1310
  const hr = createElement("hr");
1431
1311
 
1432
- figure.addEventListener("click", (event) => {
1433
- dispatchCustomEvent(figure, "lexxy:internal:select-node", { key: this.getKey() });
1434
- });
1435
-
1436
1312
  figure.appendChild(hr);
1437
1313
 
1438
1314
  return figure
@@ -1467,6 +1343,225 @@ class HorizontalDividerNode extends DecoratorNode {
1467
1343
  }
1468
1344
  }
1469
1345
 
1346
+ function isSelectionHighlighted(selection) {
1347
+ if (!$isRangeSelection(selection)) return false
1348
+
1349
+ if (selection.isCollapsed()) {
1350
+ return hasHighlightStyles(selection.style)
1351
+ } else {
1352
+ return selection.hasFormat("highlight")
1353
+ }
1354
+ }
1355
+
1356
+ function hasHighlightStyles(cssOrStyles) {
1357
+ const styles = typeof cssOrStyles === "string" ? getStyleObjectFromCSS(cssOrStyles) : cssOrStyles;
1358
+ return !!(styles.color || styles["background-color"])
1359
+ }
1360
+
1361
+ function applyCanonicalizers(styles, canonicalizers = []) {
1362
+ return canonicalizers.reduce((css, canonicalizer) => {
1363
+ return canonicalizer.applyCanonicalization(css)
1364
+ }, styles)
1365
+ }
1366
+
1367
+ class StyleCanonicalizer {
1368
+ constructor(property, allowedValues= []) {
1369
+ this._property = property;
1370
+ this._allowedValues = allowedValues;
1371
+ this._canonicalValues = this.#allowedValuesIdentityObject;
1372
+ }
1373
+
1374
+ applyCanonicalization(css) {
1375
+ const styles = { ...getStyleObjectFromCSS(css) };
1376
+
1377
+ styles[this._property] = this.getCanonicalAllowedValue(styles[this._property]);
1378
+ if (!styles[this._property]) {
1379
+ delete styles[this._property];
1380
+ }
1381
+
1382
+ return getCSSFromStyleObject(styles)
1383
+ }
1384
+
1385
+ getCanonicalAllowedValue(value) {
1386
+ return this._canonicalValues[value] ||= this.#resolveCannonicalValue(value)
1387
+ }
1388
+
1389
+ // Private
1390
+
1391
+ get #allowedValuesIdentityObject() {
1392
+ return this._allowedValues.reduce((object, value) => ({ ...object, [value]: value }), {})
1393
+ }
1394
+
1395
+ #resolveCannonicalValue(value) {
1396
+ let index = this.#computedAllowedValues.indexOf(value);
1397
+ index ||= this.#computedAllowedValues.indexOf(getComputedStyleForProperty(this._property, value));
1398
+ return index === -1 ? null : this._allowedValues[index]
1399
+ }
1400
+
1401
+ get #computedAllowedValues() {
1402
+ return this._computedAllowedValues ||= this._allowedValues.map(
1403
+ value => getComputedStyleForProperty(this._property, value)
1404
+ )
1405
+ }
1406
+ }
1407
+
1408
+ function getComputedStyleForProperty(property, value) {
1409
+ const style = `${property}: ${value};`;
1410
+
1411
+ // the element has to be attached to the DOM have computed styles
1412
+ const element = document.body.appendChild(createElement("span", { style: "display: none;" + style }));
1413
+ const computedStyle = window.getComputedStyle(element).getPropertyValue(property);
1414
+ element.remove();
1415
+
1416
+ return computedStyle
1417
+ }
1418
+
1419
+ class LexxyExtension {
1420
+ #editorElement
1421
+
1422
+ constructor(editorElement) {
1423
+ this.#editorElement = editorElement;
1424
+ }
1425
+
1426
+ get editorElement() {
1427
+ return this.#editorElement
1428
+ }
1429
+
1430
+ get editorConfig() {
1431
+ return this.#editorElement.config
1432
+ }
1433
+
1434
+ // optional: defaults to true
1435
+ get enabled() {
1436
+ return true
1437
+ }
1438
+
1439
+ get lexicalExtension() {
1440
+ return null
1441
+ }
1442
+
1443
+ initializeToolbar(_lexxyToolbar) {
1444
+
1445
+ }
1446
+ }
1447
+
1448
+ const TOGGLE_HIGHLIGHT_COMMAND = createCommand();
1449
+ const REMOVE_HIGHLIGHT_COMMAND = createCommand();
1450
+ const BLANK_STYLES = { "color": null, "background-color": null };
1451
+
1452
+ const hasPastedStylesState = createState("hasPastedStyles", {
1453
+ parse: (value) => value || false
1454
+ });
1455
+
1456
+ class HighlightExtension extends LexxyExtension {
1457
+ get enabled() {
1458
+ return this.editorElement.supportsRichText
1459
+ }
1460
+
1461
+ get lexicalExtension() {
1462
+ const extension = defineExtension({
1463
+ dependencies: [ RichTextExtension ],
1464
+ name: "lexxy/highlight",
1465
+ config: {
1466
+ color: { buttons: [], permit: [] },
1467
+ "background-color": { buttons: [], permit: [] }
1468
+ },
1469
+ html: {
1470
+ import: {
1471
+ mark: $markConversion
1472
+ }
1473
+ },
1474
+ register(editor, config) {
1475
+ // keep the ref to the canonicalizers for optimized css conversion
1476
+ const canonicalizers = buildCanonicalizers(config);
1477
+
1478
+ return mergeRegister(
1479
+ editor.registerCommand(TOGGLE_HIGHLIGHT_COMMAND, $toggleSelectionStyles, COMMAND_PRIORITY_NORMAL),
1480
+ editor.registerCommand(REMOVE_HIGHLIGHT_COMMAND, () => $toggleSelectionStyles(BLANK_STYLES), COMMAND_PRIORITY_NORMAL),
1481
+ editor.registerNodeTransform(TextNode, $syncHighlightWithStyle),
1482
+ editor.registerNodeTransform(TextNode, (textNode) => $canonicalizePastedStyles(textNode, canonicalizers))
1483
+ )
1484
+ }
1485
+ });
1486
+
1487
+ return [ extension, this.editorConfig.get("highlight") ]
1488
+ }
1489
+ }
1490
+
1491
+ function $applyHighlightStyle(textNode, element) {
1492
+ const elementStyles = {
1493
+ color: element.style?.color,
1494
+ "background-color": element.style?.backgroundColor
1495
+ };
1496
+
1497
+ if ($hasUpdateTag(PASTE_TAG)) { $setPastedStyles(textNode); }
1498
+ const highlightStyle = getCSSFromStyleObject(elementStyles);
1499
+
1500
+ if (highlightStyle.length) {
1501
+ return textNode.setStyle(textNode.getStyle() + highlightStyle)
1502
+ }
1503
+ }
1504
+
1505
+ function $markConversion() {
1506
+ return {
1507
+ conversion: extendTextNodeConversion("mark", $applyHighlightStyle),
1508
+ priority: 1
1509
+ }
1510
+ }
1511
+
1512
+ function buildCanonicalizers(config) {
1513
+ return [
1514
+ new StyleCanonicalizer("color", [ ...config.buttons.color, ...config.permit.color ]),
1515
+ new StyleCanonicalizer("background-color", [ ...config.buttons["background-color"], ...config.permit["background-color"] ])
1516
+ ]
1517
+ }
1518
+
1519
+ function $toggleSelectionStyles(styles) {
1520
+ const selection = $getSelection();
1521
+ if (!$isRangeSelection(selection)) return
1522
+
1523
+ const patch = {};
1524
+ for (const property in styles) {
1525
+ const oldValue = $getSelectionStyleValueForProperty(selection, property);
1526
+ patch[property] = toggleOrReplace(oldValue, styles[property]);
1527
+ }
1528
+
1529
+ $patchStyleText(selection, patch);
1530
+ }
1531
+
1532
+ function toggleOrReplace(oldValue, newValue) {
1533
+ return oldValue === newValue ? null : newValue
1534
+ }
1535
+
1536
+ function $syncHighlightWithStyle(textNode) {
1537
+ if (hasHighlightStyles(textNode.getStyle()) !== textNode.hasFormat("highlight")) {
1538
+ textNode.toggleFormat("highlight");
1539
+ }
1540
+ }
1541
+
1542
+ function $canonicalizePastedStyles(textNode, canonicalizers = []) {
1543
+ if ($hasPastedStyles(textNode)) {
1544
+ $setPastedStyles(textNode, false);
1545
+
1546
+ const canonicalizedCSS = applyCanonicalizers(textNode.getStyle(), canonicalizers);
1547
+ textNode.setStyle(canonicalizedCSS);
1548
+
1549
+ const selection = $getSelection();
1550
+ if (textNode.isSelected(selection)) {
1551
+ selection.setStyle(textNode.getStyle());
1552
+ selection.setFormat(textNode.getFormat());
1553
+ }
1554
+ }
1555
+ }
1556
+
1557
+ function $setPastedStyles(textNode, value = true) {
1558
+ $setState(textNode, hasPastedStylesState, value);
1559
+ }
1560
+
1561
+ function $hasPastedStyles(textNode) {
1562
+ return $getState(textNode, hasPastedStylesState)
1563
+ }
1564
+
1470
1565
  const COMMANDS = [
1471
1566
  "bold",
1472
1567
  "italic",
@@ -1500,7 +1595,6 @@ class CommandDispatcher {
1500
1595
  this.selection = editorElement.selection;
1501
1596
  this.contents = editorElement.contents;
1502
1597
  this.clipboard = editorElement.clipboard;
1503
- this.highlighter = editorElement.highlighter;
1504
1598
 
1505
1599
  this.#registerCommands();
1506
1600
  this.#registerKeyboardCommands();
@@ -1524,11 +1618,11 @@ class CommandDispatcher {
1524
1618
  }
1525
1619
 
1526
1620
  dispatchToggleHighlight(styles) {
1527
- this.highlighter.toggle(styles);
1621
+ this.editor.dispatchCommand(TOGGLE_HIGHLIGHT_COMMAND, styles);
1528
1622
  }
1529
1623
 
1530
1624
  dispatchRemoveHighlight() {
1531
- this.highlighter.remove();
1625
+ this.editor.dispatchCommand(REMOVE_HIGHLIGHT_COMMAND);
1532
1626
  }
1533
1627
 
1534
1628
  dispatchLink(url) {
@@ -1593,41 +1687,38 @@ class CommandDispatcher {
1593
1687
 
1594
1688
  dispatchInsertHorizontalDivider() {
1595
1689
  this.contents.insertAtCursorEnsuringLineBelow(new HorizontalDividerNode());
1596
-
1597
1690
  this.editor.focus();
1598
1691
  }
1599
1692
 
1600
1693
  dispatchRotateHeadingFormat() {
1601
- this.editor.update(() => {
1602
- const selection = $getSelection();
1603
- if (!$isRangeSelection(selection)) return
1604
-
1605
- if ($isRootOrShadowRoot(selection.anchor.getNode())) {
1606
- selection.insertNodes([ $createHeadingNode("h2") ]);
1607
- return
1608
- }
1694
+ const selection = $getSelection();
1695
+ if (!$isRangeSelection(selection)) return
1609
1696
 
1610
- const topLevelElement = selection.anchor.getNode().getTopLevelElementOrThrow();
1611
- let nextTag = "h2";
1612
- if ($isHeadingNode(topLevelElement)) {
1613
- const currentTag = topLevelElement.getTag();
1614
- if (currentTag === "h2") {
1615
- nextTag = "h3";
1616
- } else if (currentTag === "h3") {
1617
- nextTag = "h4";
1618
- } else if (currentTag === "h4") {
1619
- nextTag = null;
1620
- } else {
1621
- nextTag = "h2";
1622
- }
1623
- }
1697
+ if ($isRootOrShadowRoot(selection.anchor.getNode())) {
1698
+ selection.insertNodes([ $createHeadingNode("h2") ]);
1699
+ return
1700
+ }
1624
1701
 
1625
- if (nextTag) {
1626
- this.contents.insertNodeWrappingEachSelectedLine(() => $createHeadingNode(nextTag));
1702
+ const topLevelElement = selection.anchor.getNode().getTopLevelElementOrThrow();
1703
+ let nextTag = "h2";
1704
+ if ($isHeadingNode(topLevelElement)) {
1705
+ const currentTag = topLevelElement.getTag();
1706
+ if (currentTag === "h2") {
1707
+ nextTag = "h3";
1708
+ } else if (currentTag === "h3") {
1709
+ nextTag = "h4";
1710
+ } else if (currentTag === "h4") {
1711
+ nextTag = null;
1627
1712
  } else {
1628
- this.contents.removeFormattingFromSelectedLines();
1713
+ nextTag = "h2";
1629
1714
  }
1630
- });
1715
+ }
1716
+
1717
+ if (nextTag) {
1718
+ this.contents.insertNodeWrappingEachSelectedLine(() => $createHeadingNode(nextTag));
1719
+ } else {
1720
+ this.contents.removeFormattingFromSelectedLines();
1721
+ }
1631
1722
  }
1632
1723
 
1633
1724
  dispatchUploadAttachments() {
@@ -1796,7 +1887,6 @@ class Selection {
1796
1887
 
1797
1888
  this.#listenForNodeSelections();
1798
1889
  this.#processSelectionChangeCommands();
1799
- this.#handleInputWhenDecoratorNodesSelected();
1800
1890
  this.#containEditorFocus();
1801
1891
  }
1802
1892
 
@@ -1807,12 +1897,10 @@ class Selection {
1807
1897
  }
1808
1898
 
1809
1899
  get hasNodeSelection() {
1810
- let result = false;
1811
- this.editor.getEditorState().read(() => {
1900
+ return this.editor.getEditorState().read(() => {
1812
1901
  const selection = $getSelection();
1813
- result = selection !== null && $isNodeSelection(selection);
1814
- });
1815
- return result
1902
+ return selection !== null && $isNodeSelection(selection)
1903
+ })
1816
1904
  }
1817
1905
 
1818
1906
  get cursorPosition() {
@@ -1890,6 +1978,36 @@ class Selection {
1890
1978
  }
1891
1979
  }
1892
1980
 
1981
+ getFormat() {
1982
+ const selection = $getSelection();
1983
+ if (!$isRangeSelection(selection)) return {}
1984
+
1985
+ const anchorNode = selection.anchor.getNode();
1986
+ if (!anchorNode.getParent()) return {}
1987
+
1988
+ const topLevelElement = anchorNode.getTopLevelElementOrThrow();
1989
+ const listType = getListType(anchorNode);
1990
+
1991
+ return {
1992
+ isBold: selection.hasFormat("bold"),
1993
+ isItalic: selection.hasFormat("italic"),
1994
+ isStrikethrough: selection.hasFormat("strikethrough"),
1995
+ isHighlight: isSelectionHighlighted(selection),
1996
+ isInLink: $getNearestNodeOfType(anchorNode, LinkNode) !== null,
1997
+ isInQuote: $isQuoteNode(topLevelElement),
1998
+ isInHeading: $isHeadingNode(topLevelElement),
1999
+ isInCode: selection.hasFormat("code") || $getNearestNodeOfType(anchorNode, CodeNode) !== null,
2000
+ isInList: listType !== null,
2001
+ listType,
2002
+ isInTable: $getTableCellNodeFromLexicalNode(anchorNode) !== null
2003
+ }
2004
+ }
2005
+
2006
+ nearestNodeOfType(nodeType) {
2007
+ const anchorNode = $getSelection()?.anchor?.getNode();
2008
+ return $getNearestNodeOfType(anchorNode, nodeType)
2009
+ }
2010
+
1893
2011
  get hasSelectedWordsInSingleLine() {
1894
2012
  const selection = $getSelection();
1895
2013
  if (!$isRangeSelection(selection)) return false
@@ -1917,42 +2035,20 @@ class Selection {
1917
2035
  }
1918
2036
 
1919
2037
  get isInsideList() {
1920
- const selection = $getSelection();
1921
- if (!$isRangeSelection(selection)) return false
1922
-
1923
- const anchorNode = selection.anchor.getNode();
1924
- return getNearestListItemNode(anchorNode) !== null
2038
+ return this.nearestNodeOfType(ListItemNode)
1925
2039
  }
1926
2040
 
1927
2041
  get isIndentedList() {
1928
- const selection = $getSelection();
1929
- if (!$isRangeSelection(selection)) return false
1930
-
1931
- const nodes = selection.getNodes();
1932
- for (const node of nodes) {
1933
- const closestListNode = $getNearestNodeOfType(node, ListNode);
1934
- if (closestListNode && $getListDepth(closestListNode) > 1) {
1935
- return true
1936
- }
1937
- }
1938
-
1939
- return false
2042
+ const closestListNode = this.nearestNodeOfType(ListNode);
2043
+ return closestListNode && ($getListDepth(closestListNode) > 1)
1940
2044
  }
1941
2045
 
1942
2046
  get isInsideCodeBlock() {
1943
- const selection = $getSelection();
1944
- if (!$isRangeSelection(selection)) return false
1945
-
1946
- const anchorNode = selection.anchor.getNode();
1947
- return $getNearestNodeOfType(anchorNode, CodeNode) !== null
2047
+ return this.nearestNodeOfType(CodeNode) !== null
1948
2048
  }
1949
2049
 
1950
2050
  get isTableCellSelected() {
1951
- const selection = $getSelection();
1952
- if (!$isRangeSelection(selection)) return false
1953
-
1954
- const anchorNode = selection.anchor.getNode();
1955
- return $getNearestNodeOfType(anchorNode, TableCellNode) !== null
2051
+ return this.nearestNodeOfType(TableCellNode) !== null
1956
2052
  }
1957
2053
 
1958
2054
  get nodeAfterCursor() {
@@ -2015,10 +2111,6 @@ class Selection {
2015
2111
  return this.#findPreviousSiblingUp(anchorNode)
2016
2112
  }
2017
2113
 
2018
- get #contents() {
2019
- return this.editorElement.contents
2020
- }
2021
-
2022
2114
  get #currentlySelectedKeys() {
2023
2115
  if (this.currentlySelectedKeys) { return this.currentlySelectedKeys }
2024
2116
 
@@ -2040,84 +2132,24 @@ class Selection {
2040
2132
  this.editor.registerCommand(KEY_ARROW_UP_COMMAND, this.#selectPreviousTopLevelNode.bind(this), COMMAND_PRIORITY_LOW);
2041
2133
  this.editor.registerCommand(KEY_ARROW_DOWN_COMMAND, this.#selectNextTopLevelNode.bind(this), COMMAND_PRIORITY_LOW);
2042
2134
 
2043
- this.editor.registerCommand(KEY_DELETE_COMMAND, this.#deleteSelectedOrNext.bind(this), COMMAND_PRIORITY_LOW);
2044
- this.editor.registerCommand(KEY_BACKSPACE_COMMAND, this.#deletePreviousOrNext.bind(this), COMMAND_PRIORITY_LOW);
2135
+ this.editor.registerCommand(DELETE_CHARACTER_COMMAND, this.#selectDecoratorNodeBeforeDeletion.bind(this), COMMAND_PRIORITY_LOW);
2045
2136
 
2046
2137
  this.editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
2047
2138
  this.current = $getSelection();
2048
- }, COMMAND_PRIORITY_LOW);
2049
- }
2050
-
2051
- #listenForNodeSelections() {
2052
- this.editor.getRootElement().addEventListener("lexxy:internal:select-node", async (event) => {
2053
- await nextFrame();
2054
-
2055
- const { key } = event.detail;
2056
- this.editor.update(() => {
2057
- const node = $getNodeByKey(key);
2058
- if (node) {
2059
- const selection = $createNodeSelection();
2060
- selection.add(node.getKey());
2061
- $setSelection(selection);
2062
- }
2063
- this.editor.focus();
2064
- });
2065
- });
2066
-
2067
- this.editor.getRootElement().addEventListener("lexxy:internal:move-to-next-line", (event) => {
2068
- this.#selectOrAppendNextLine();
2069
- });
2070
- }
2071
-
2072
- // In Safari, when the only node in the document is an attachment, it won't let you enter text
2073
- // before/below it. There is probably a better fix here, but this workaround solves the problem until
2074
- // we find it.
2075
- #handleInputWhenDecoratorNodesSelected() {
2076
- this.editor.getRootElement().addEventListener("keydown", (event) => {
2077
- if (isPrintableCharacter(event)) {
2078
- this.editor.update(() => {
2079
- const selection = $getSelection();
2080
-
2081
- if ($isRangeSelection(selection) && selection.isCollapsed()) {
2082
- const anchorNode = selection.anchor.getNode();
2083
- const offset = selection.anchor.offset;
2084
-
2085
- const nodeBefore = this.#getNodeBeforePosition(anchorNode, offset);
2086
- const nodeAfter = this.#getNodeAfterPosition(anchorNode, offset);
2087
-
2088
- if (nodeBefore instanceof DecoratorNode && !nodeBefore.isInline()) {
2089
- event.preventDefault();
2090
- this.#contents.createParagraphAfterNode(nodeBefore, event.key);
2091
- return
2092
- } else if (nodeAfter instanceof DecoratorNode && !nodeAfter.isInline()) {
2093
- event.preventDefault();
2094
- this.#contents.createParagraphBeforeNode(nodeAfter, event.key);
2095
- return
2096
- }
2097
- }
2098
- });
2099
- }
2100
- }, true);
2101
- }
2102
-
2103
- #getNodeBeforePosition(node, offset) {
2104
- if ($isTextNode(node) && offset === 0) {
2105
- return node.getPreviousSibling()
2106
- }
2107
- if ($isElementNode(node) && offset > 0) {
2108
- return node.getChildAtIndex(offset - 1)
2109
- }
2110
- return null
2139
+ }, COMMAND_PRIORITY_LOW);
2111
2140
  }
2112
2141
 
2113
- #getNodeAfterPosition(node, offset) {
2114
- if ($isTextNode(node) && offset === node.getTextContentSize()) {
2115
- return node.getNextSibling()
2116
- }
2117
- if ($isElementNode(node)) {
2118
- return node.getChildAtIndex(offset)
2119
- }
2120
- return null
2142
+ #listenForNodeSelections() {
2143
+ this.editor.registerCommand(CLICK_COMMAND, ({ target }) => {
2144
+ if (!isDOMNode(target)) return false
2145
+
2146
+ const targetNode = $getNearestNodeFromDOMNode(target);
2147
+ return $isDecoratorNode(targetNode) && this.#selectInLexical(targetNode)
2148
+ }, COMMAND_PRIORITY_LOW);
2149
+
2150
+ this.editor.getRootElement().addEventListener("lexxy:internal:move-to-next-line", (event) => {
2151
+ this.#selectOrAppendNextLine();
2152
+ });
2121
2153
  }
2122
2154
 
2123
2155
  #containEditorFocus() {
@@ -2184,33 +2216,33 @@ class Selection {
2184
2216
 
2185
2217
  async #selectPreviousNode() {
2186
2218
  if (this.hasNodeSelection) {
2187
- await this.#withCurrentNode((currentNode) => currentNode.selectPrevious());
2219
+ return await this.#withCurrentNode((currentNode) => currentNode.selectPrevious())
2188
2220
  } else {
2189
- this.#selectInLexical(this.nodeBeforeCursor);
2221
+ return this.#selectInLexical(this.nodeBeforeCursor)
2190
2222
  }
2191
2223
  }
2192
2224
 
2193
2225
  async #selectNextNode() {
2194
2226
  if (this.hasNodeSelection) {
2195
- await this.#withCurrentNode((currentNode) => currentNode.selectNext(0, 0));
2227
+ return await this.#withCurrentNode((currentNode) => currentNode.selectNext(0, 0))
2196
2228
  } else {
2197
- this.#selectInLexical(this.nodeAfterCursor);
2229
+ return this.#selectInLexical(this.nodeAfterCursor)
2198
2230
  }
2199
2231
  }
2200
2232
 
2201
2233
  async #selectPreviousTopLevelNode() {
2202
2234
  if (this.hasNodeSelection) {
2203
- await this.#withCurrentNode((currentNode) => currentNode.selectPrevious());
2235
+ return await this.#withCurrentNode((currentNode) => currentNode.getTopLevelElement().selectPrevious())
2204
2236
  } else {
2205
- this.#selectInLexical(this.topLevelNodeBeforeCursor);
2237
+ return this.#selectInLexical(this.topLevelNodeBeforeCursor)
2206
2238
  }
2207
2239
  }
2208
2240
 
2209
2241
  async #selectNextTopLevelNode() {
2210
2242
  if (this.hasNodeSelection) {
2211
- await this.#withCurrentNode((currentNode) => currentNode.selectNext(0, 0));
2243
+ return await this.#withCurrentNode((currentNode) => currentNode.getTopLevelElement().selectNext(0, 0))
2212
2244
  } else {
2213
- this.#selectInLexical(this.topLevelNodeAfterCursor);
2245
+ return this.#selectInLexical(this.topLevelNodeAfterCursor)
2214
2246
  }
2215
2247
  }
2216
2248
 
@@ -2276,37 +2308,24 @@ class Selection {
2276
2308
  }
2277
2309
 
2278
2310
  #selectInLexical(node) {
2279
- if (!node || !(node instanceof DecoratorNode)) return
2280
-
2281
- this.editor.update(() => {
2282
- const selection = $createNodeSelection();
2283
- selection.add(node.getKey());
2311
+ if ($isDecoratorNode(node)) {
2312
+ const selection = $createNodeSelectionWith(node);
2284
2313
  $setSelection(selection);
2285
- });
2286
- }
2287
-
2288
- #deleteSelectedOrNext() {
2289
- const node = this.nodeAfterCursor;
2290
- if (node instanceof DecoratorNode) {
2291
- this.#selectInLexical(node);
2292
- return true
2314
+ return selection
2293
2315
  } else {
2294
- this.#contents.deleteSelectedNodes();
2316
+ return false
2295
2317
  }
2296
-
2297
- return false
2298
2318
  }
2299
2319
 
2300
- #deletePreviousOrNext() {
2301
- const node = this.nodeBeforeCursor;
2320
+ #selectDecoratorNodeBeforeDeletion(backwards) {
2321
+ const node = backwards ? this.nodeBeforeCursor : this.nodeAfterCursor;
2302
2322
  if (node instanceof DecoratorNode) {
2303
2323
  this.#selectInLexical(node);
2324
+
2304
2325
  return true
2305
2326
  } else {
2306
- this.#contents.deleteSelectedNodes();
2327
+ return false
2307
2328
  }
2308
-
2309
- return false
2310
2329
  }
2311
2330
 
2312
2331
  #getValidSelectionRange() {
@@ -2600,17 +2619,13 @@ class CustomActionTextAttachmentNode extends DecoratorNode {
2600
2619
  createDOM() {
2601
2620
  const figure = createElement(this.tagName, { "content-type": this.contentType, "data-lexxy-decorator": true });
2602
2621
 
2603
- figure.addEventListener("click", (event) => {
2604
- dispatchCustomEvent(figure, "lexxy:internal:select-node", { key: this.getKey() });
2605
- });
2606
-
2607
2622
  figure.insertAdjacentHTML("beforeend", this.innerHtml);
2608
2623
 
2609
2624
  return figure
2610
2625
  }
2611
2626
 
2612
2627
  updateDOM() {
2613
- return true
2628
+ return false
2614
2629
  }
2615
2630
 
2616
2631
  getTextContent() {
@@ -2946,20 +2961,18 @@ class Contents {
2946
2961
  }
2947
2962
 
2948
2963
  insertAtCursor(node) {
2949
- this.editor.update(() => {
2950
- const selection = $getSelection();
2951
- const selectedNodes = selection?.getNodes();
2964
+ const selection = $getSelection();
2965
+ const selectedNodes = selection?.getNodes();
2952
2966
 
2953
- if ($isRangeSelection(selection)) {
2954
- $insertNodes([ node ]);
2955
- } else if ($isNodeSelection(selection) && selectedNodes && selectedNodes.length > 0) {
2956
- const lastNode = selectedNodes[selectedNodes.length - 1];
2957
- lastNode.insertAfter(node);
2958
- } else {
2959
- const root = $getRoot();
2960
- root.append(node);
2961
- }
2962
- });
2967
+ if ($isRangeSelection(selection)) {
2968
+ $insertNodes([ node ]);
2969
+ } else if ($isNodeSelection(selection) && selectedNodes && selectedNodes.length > 0) {
2970
+ const lastNode = selectedNodes.at(-1);
2971
+ lastNode.insertAfter(node);
2972
+ } else {
2973
+ const root = $getRoot();
2974
+ root.append(node);
2975
+ }
2963
2976
  }
2964
2977
 
2965
2978
  insertAtCursorEnsuringLineBelow(node) {
@@ -3187,27 +3200,6 @@ class Contents {
3187
3200
  }, { tag: HISTORY_MERGE_TAG });
3188
3201
  }
3189
3202
 
3190
- async deleteSelectedNodes() {
3191
- let focusNode = null;
3192
-
3193
- this.editor.update(() => {
3194
- if (this.#selection.hasNodeSelection) {
3195
- const nodesToRemove = $getSelection().getNodes();
3196
- if (nodesToRemove.length === 0) return
3197
-
3198
- focusNode = this.#findAdjacentNodeTo(nodesToRemove);
3199
- this.#deleteNodes(nodesToRemove);
3200
- }
3201
- });
3202
-
3203
- await nextFrame();
3204
-
3205
- this.editor.update(() => {
3206
- this.#selectAfterDeletion(focusNode);
3207
- this.editor.focus();
3208
- });
3209
- }
3210
-
3211
3203
  replaceNodeWithHTML(nodeKey, html, options = {}) {
3212
3204
  this.editor.update(() => {
3213
3205
  const node = $getNodeByKey(nodeKey);
@@ -3246,10 +3238,6 @@ class Contents {
3246
3238
  });
3247
3239
  }
3248
3240
 
3249
- get #selection() {
3250
- return this.editorElement.selection
3251
- }
3252
-
3253
3241
  #insertLineBelowIfLastNode(node) {
3254
3242
  this.editor.update(() => {
3255
3243
  const nextSibling = node.getNextSibling();
@@ -3446,52 +3434,13 @@ class Contents {
3446
3434
  nodesToDelete.forEach((node) => node.remove());
3447
3435
  }
3448
3436
 
3449
- #deleteNodes(nodes) {
3450
- // Use splice() instead of node.remove() for proper removal and
3451
- // reconciliation. Would have issues with removing unintended decorator nodes
3452
- // with node.remove()
3453
- nodes.forEach((node) => {
3454
- const parent = node.getParent();
3455
- if (!$isElementNode(parent)) return
3456
-
3457
- const children = parent.getChildren();
3458
- const index = children.indexOf(node);
3459
-
3460
- if (index >= 0) {
3461
- parent.splice(index, 1, []);
3462
- }
3463
- });
3464
- }
3465
-
3466
- #findAdjacentNodeTo(nodes) {
3467
- const firstNode = nodes[0];
3468
- const lastNode = nodes[nodes.length - 1];
3469
-
3470
- return firstNode?.getPreviousSibling() || lastNode?.getNextSibling()
3471
- }
3472
-
3473
- #selectAfterDeletion(focusNode) {
3474
- const root = $getRoot();
3475
- if (root.getChildrenSize() === 0) {
3476
- const newParagraph = $createParagraphNode();
3477
- root.append(newParagraph);
3478
- newParagraph.selectStart();
3479
- } else if (focusNode) {
3480
- if ($isTextNode(focusNode) || $isParagraphNode(focusNode)) {
3481
- focusNode.selectEnd();
3482
- } else {
3483
- focusNode.selectNext(0, 0);
3484
- }
3485
- }
3486
- }
3487
-
3488
3437
  #collectSelectedListItems(selection) {
3489
3438
  const nodes = selection.getNodes();
3490
3439
  const listItems = new Set();
3491
3440
  const parentLists = new Set();
3492
3441
 
3493
3442
  for (const node of nodes) {
3494
- const listItem = getNearestListItemNode(node);
3443
+ const listItem = $getNearestNodeOfType(node, ListItemNode);
3495
3444
  if (listItem) {
3496
3445
  listItems.add(listItem);
3497
3446
  const parentList = listItem.getParent();
@@ -3807,8 +3756,16 @@ class Extensions {
3807
3756
  return this.lexxyElement.toolbar
3808
3757
  }
3809
3758
 
3759
+ get #baseExtensions() {
3760
+ return this.lexxyElement.baseExtensions
3761
+ }
3762
+
3763
+ get #configuredExtensions() {
3764
+ return Lexxy.global.get("extensions")
3765
+ }
3766
+
3810
3767
  #initializeExtensions() {
3811
- const extensionDefinitions = Lexxy.global.get("extensions");
3768
+ const extensionDefinitions = this.#baseExtensions.concat(this.#configuredExtensions);
3812
3769
 
3813
3770
  return extensionDefinitions.map(
3814
3771
  extension => new extension(this.lexxyElement)
@@ -3816,156 +3773,175 @@ class Extensions {
3816
3773
  }
3817
3774
  }
3818
3775
 
3819
- const TOGGLE_HIGHLIGHT_COMMAND = createCommand();
3820
-
3821
- const hasPastedStylesState = createState("hasPastedStyles", {
3822
- parse: (value) => value || false
3823
- });
3824
-
3825
- const HighlightExtension = defineExtension({
3826
- dependencies: [ RichTextExtension ],
3827
- name: "lexxy/highlight",
3828
- config: {
3829
- color: { buttons: [], permit: [] },
3830
- "background-color": { buttons: [], permit: [] }
3831
- },
3832
- html: {
3833
- import: {
3834
- mark: $markConversion
3835
- }
3836
- },
3837
- register(editor, config) {
3838
- const canonicalizers = buildCanonicalizers(config);
3839
-
3840
- editor.registerCommand(TOGGLE_HIGHLIGHT_COMMAND, $toggleSelectionStyles, COMMAND_PRIORITY_NORMAL);
3841
- editor.registerNodeTransform(TextNode, $syncHighlightWithStyle);
3842
- editor.registerNodeTransform(TextNode, (textNode) => $canonicalizePastedStyles(textNode, canonicalizers));
3776
+ class ProvisionalParagraphNode extends ParagraphNode {
3777
+ $config() {
3778
+ return this.config("provisonal_paragraph", {
3779
+ extends: ParagraphNode,
3780
+ importDOM: () => null,
3781
+ $transform: (node) => {
3782
+ node.concretizeIfEdited(node);
3783
+ node.removeUnlessRequired(node);
3784
+ }
3785
+ })
3843
3786
  }
3844
- });
3845
3787
 
3846
- function $applyHighlightStyle(textNode, element) {
3847
- const elementStyles = {
3848
- color: element.style?.color,
3849
- "background-color": element.style?.backgroundColor
3850
- };
3788
+ static neededBetween(nodeBefore, nodeAfter) {
3789
+ return !$isSelectableElement(nodeBefore, "next")
3790
+ && !$isSelectableElement(nodeAfter, "previous")
3791
+ }
3851
3792
 
3852
- if ($hasUpdateTag(PASTE_TAG)) { $setPastedStyles(textNode); }
3853
- const highlightStyle = getCSSFromStyleObject(elementStyles);
3793
+ createDOM(editor) {
3794
+ const p = super.createDOM(editor);
3795
+ const selected = this.isSelected($getSelection());
3796
+ p.classList.add("provisional-paragraph");
3797
+ p.classList.toggle("hidden", !selected);
3798
+ return p
3799
+ }
3854
3800
 
3855
- if (highlightStyle.length) {
3856
- return textNode.setStyle(textNode.getStyle() + highlightStyle)
3801
+ updateDOM(_prevNode, dom) {
3802
+ const selected = this.isSelected($getSelection());
3803
+ dom.classList.toggle("hidden", !selected);
3804
+ return false
3857
3805
  }
3858
- }
3859
3806
 
3860
- function $markConversion() {
3861
- return {
3862
- conversion: extendTextNodeConversion("mark", $applyHighlightStyle),
3863
- priority: 1
3807
+ getTextContent() {
3808
+ return ""
3864
3809
  }
3865
- }
3866
3810
 
3867
- function buildCanonicalizers(config) {
3868
- return [
3869
- new StyleCanonicalizer("color", [ ...config.buttons.color, ...config.permit.color ]),
3870
- new StyleCanonicalizer("background-color", [ ...config.buttons["background-color"], ...config.permit["background-color"] ])
3871
- ]
3872
- }
3811
+ exportDOM() {
3812
+ return {
3813
+ element: null
3814
+ }
3815
+ }
3873
3816
 
3874
- function $toggleSelectionStyles(styles) {
3875
- const selection = $getSelection();
3876
- if (!$isRangeSelection(selection)) return
3817
+ // override as Lexical has an interesting view of collapsed selection in ElementNodes
3818
+ // https://github.com/facebook/lexical/blob/f1e4f66014377b1f2595aec2b0ee17f5b7ef4dfc/packages/lexical/src/LexicalNode.ts#L646
3819
+ isSelected(selection = null) {
3820
+ const targetSelection = selection || $getSelection();
3821
+ return targetSelection?.getNodes().some(node => node.is(this) || this.isParentOf(node))
3822
+ }
3877
3823
 
3878
- const patch = {};
3879
- for (const property in styles) {
3880
- const oldValue = $getSelectionStyleValueForProperty(selection, property);
3881
- patch[property] = toggleOrReplace(oldValue, styles[property]);
3824
+ removeUnlessRequired(self = this.getLatest()) {
3825
+ if (!self.required) self.remove();
3882
3826
  }
3883
3827
 
3884
- $patchStyleText(selection, patch);
3885
- }
3828
+ concretizeIfEdited(self = this.getLatest()) {
3829
+ if (self.getTextContentSize() > 0) {
3830
+ self.replace($createParagraphNode(), true);
3831
+ }
3832
+ }
3886
3833
 
3887
- function toggleOrReplace(oldValue, newValue) {
3888
- return oldValue === newValue ? null : newValue
3889
- }
3890
3834
 
3891
- function $syncHighlightWithStyle(textNode) {
3892
- if (hasHighlightStyles(textNode.getStyle()) !== textNode.hasFormat("highlight")) {
3893
- textNode.toggleFormat("highlight");
3835
+ get required() {
3836
+ return this.isDirectRootChild && ProvisionalParagraphNode.neededBetween(...this.immediateSiblings)
3894
3837
  }
3895
- }
3896
-
3897
- function $canonicalizePastedStyles(textNode, canonicalizers = []) {
3898
- if ($hasPastedStyles(textNode)) {
3899
- $setPastedStyles(textNode, false);
3900
3838
 
3901
- const canonicalizedCSS = canonicalizers.reduce((css, canonicalizer) => {
3902
- return canonicalizer.applyCanonicalization(css)
3903
- }, textNode.getStyle());
3839
+ get isDirectRootChild() {
3840
+ const parent = this.getParent();
3841
+ return $isRootOrShadowRoot(parent)
3842
+ }
3904
3843
 
3905
- textNode.setStyle(canonicalizedCSS);
3844
+ get immediateSiblings() {
3845
+ return [ this.getPreviousSibling(), this.getNextSibling() ]
3906
3846
  }
3907
3847
  }
3908
3848
 
3909
- function $setPastedStyles(textNode, value = true) {
3910
- $setState(textNode, hasPastedStylesState, value);
3849
+ function $isProvisionalParagraphNode(node) {
3850
+ return node instanceof ProvisionalParagraphNode
3911
3851
  }
3912
3852
 
3913
- function $hasPastedStyles(textNode) {
3914
- return $getState(textNode, hasPastedStylesState)
3853
+ function $isSelectableElement(node, direction) {
3854
+ return $isElementNode(node) && (direction === "next" ? node.canInsertTextBefore() : node.canInsertTextAfter())
3915
3855
  }
3916
3856
 
3917
- class Highlighter {
3918
-
3919
- constructor(editorElement) {
3920
- this.editorElement = editorElement;
3857
+ class ProvisionalParagraphExtension extends LexxyExtension {
3858
+ get lexicalExtension() {
3859
+ return defineExtension({
3860
+ name: "lexxy/provisional-paragraph",
3861
+ nodes: [
3862
+ ProvisionalParagraphNode
3863
+ ],
3864
+ register(editor) {
3865
+ return mergeRegister(
3866
+ // Process Provisional Paragraph Nodes on RootNode changes as sibling status influences whether
3867
+ // they are required and their visible/hidden status
3868
+ editor.registerNodeTransform(RootNode, $insertRequiredProvisionalParagraphs),
3869
+ editor.registerNodeTransform(RootNode, $removeUnneededProvisionalParagraphs),
3870
+ editor.registerCommand(SELECTION_CHANGE_COMMAND, $markAllProvisionalParagraphsDirty, COMMAND_PRIORITY_HIGH)
3871
+ )
3872
+ }
3873
+ })
3921
3874
  }
3875
+ }
3922
3876
 
3923
- get editor() {
3924
- return this.editorElement.editor
3877
+ function $insertRequiredProvisionalParagraphs(rootNode) {
3878
+ const firstNode = rootNode.getFirstChild();
3879
+ if (ProvisionalParagraphNode.neededBetween(null, firstNode)) {
3880
+ $insertFirst(rootNode, new ProvisionalParagraphNode);
3925
3881
  }
3926
3882
 
3927
- get lexicalExtension() {
3928
- return [ HighlightExtension, this.editorElement.config.get("highlight") ]
3883
+ for (const node of $firstToLastIterator(rootNode)) {
3884
+ const nextNode = node.getNextSibling();
3885
+ if (ProvisionalParagraphNode.neededBetween(node, nextNode)) {
3886
+ node.insertAfter(new ProvisionalParagraphNode);
3887
+ }
3929
3888
  }
3889
+ }
3930
3890
 
3931
- toggle(styles) {
3932
- this.editor.dispatchCommand(TOGGLE_HIGHLIGHT_COMMAND, styles);
3891
+ function $removeUnneededProvisionalParagraphs(rootNode) {
3892
+ for (const provisionalParagraph of $getAllProvisionalParagraphs(rootNode)) {
3893
+ provisionalParagraph.removeUnlessRequired();
3933
3894
  }
3895
+ }
3934
3896
 
3935
- remove() {
3936
- this.toggle({ "color": null, "background-color": null });
3897
+ function $markAllProvisionalParagraphsDirty() {
3898
+ for (const provisionalParagraph of $getAllProvisionalParagraphs()) {
3899
+ provisionalParagraph.markDirty();
3937
3900
  }
3938
3901
  }
3939
3902
 
3903
+ function $getAllProvisionalParagraphs(rootNode = $getRoot()) {
3904
+ return $descendantsMatching(rootNode.getChildren(), $isProvisionalParagraphNode)
3905
+ }
3906
+
3940
3907
  const TRIX_LANGUAGE_ATTR = "language";
3941
3908
 
3942
- const TrixContentExtension = defineExtension({
3943
- name: "lexxy/trix-content",
3944
- html: {
3945
- import: {
3946
- em: (element) => onlyStyledElements(element, {
3947
- conversion: extendTextNodeConversion("i", $applyHighlightStyle),
3948
- priority: 1
3949
- }),
3950
- span: (element) => onlyStyledElements(element, {
3951
- conversion: extendTextNodeConversion("mark", $applyHighlightStyle),
3952
- priority: 1
3953
- }),
3954
- strong: (element) => onlyStyledElements(element, {
3955
- conversion: extendTextNodeConversion("b", $applyHighlightStyle),
3956
- priority: 1
3957
- }),
3958
- del: () => ({
3959
- conversion: extendTextNodeConversion("s", $applyStrikethrough, $applyHighlightStyle),
3960
- priority: 1
3961
- }),
3962
- pre: (element) => onlyPreLanguageElements(element, {
3963
- conversion: extendConversion(CodeNode, "pre", $applyLanguage),
3964
- priority: 1
3965
- })
3966
- }
3909
+ class TrixContentExtension extends LexxyExtension {
3910
+
3911
+ get enabled() {
3912
+ return this.editorElement.supportsRichText
3967
3913
  }
3968
- });
3914
+
3915
+ get lexicalExtension() {
3916
+ return defineExtension({
3917
+ name: "lexxy/trix-content",
3918
+ html: {
3919
+ import: {
3920
+ em: (element) => onlyStyledElements(element, {
3921
+ conversion: extendTextNodeConversion("i", $applyHighlightStyle),
3922
+ priority: 1
3923
+ }),
3924
+ span: (element) => onlyStyledElements(element, {
3925
+ conversion: extendTextNodeConversion("mark", $applyHighlightStyle),
3926
+ priority: 1
3927
+ }),
3928
+ strong: (element) => onlyStyledElements(element, {
3929
+ conversion: extendTextNodeConversion("b", $applyHighlightStyle),
3930
+ priority: 1
3931
+ }),
3932
+ del: () => ({
3933
+ conversion: extendTextNodeConversion("s", $applyStrikethrough, $applyHighlightStyle),
3934
+ priority: 1
3935
+ }),
3936
+ pre: (element) => onlyPreLanguageElements(element, {
3937
+ conversion: extendConversion(CodeNode, "pre", $applyLanguage),
3938
+ priority: 1
3939
+ })
3940
+ }
3941
+ }
3942
+ })
3943
+ }
3944
+ }
3969
3945
 
3970
3946
  function onlyStyledElements(element, conversion) {
3971
3947
  const elementHighlighted = element.style.color !== "" || element.style.backgroundColor !== "";
@@ -3987,8 +3963,12 @@ function $applyLanguage(conversionOutput, element) {
3987
3963
  }
3988
3964
 
3989
3965
  class WrappedTableNode extends TableNode {
3990
- static clone(node) {
3991
- return new WrappedTableNode(node.__key)
3966
+ $config() {
3967
+ return this.config("wrapped_table_node", { extends: TableNode })
3968
+ }
3969
+
3970
+ static importDOM() {
3971
+ return super.importDOM()
3992
3972
  }
3993
3973
 
3994
3974
  exportDOM(editor) {
@@ -4010,99 +3990,106 @@ class WrappedTableNode extends TableNode {
4010
3990
  }
4011
3991
  }
4012
3992
 
4013
- const TablesLexicalExtension = defineExtension({
4014
- name: "lexxy/tables",
4015
- nodes: [
4016
- WrappedTableNode,
4017
- {
4018
- replace: TableNode,
4019
- with: () => new WrappedTableNode()
4020
- },
4021
- TableCellNode,
4022
- TableRowNode
4023
- ],
4024
- register(editor) {
4025
- // Register Lexical table plugins
4026
- registerTablePlugin(editor);
4027
- registerTableSelectionObserver(editor, true);
4028
- setScrollableTablesActive(editor, true);
4029
-
4030
- // Bug fix: Prevent hardcoded background color (Lexical #8089)
4031
- editor.registerNodeTransform(TableCellNode, (node) => {
4032
- if (node.getBackgroundColor() === null) {
4033
- node.setBackgroundColor("");
4034
- }
4035
- });
3993
+ class TablesExtension extends LexxyExtension {
4036
3994
 
4037
- // Bug fix: Fix column header states (Lexical #8090)
4038
- editor.registerNodeTransform(TableCellNode, (node) => {
4039
- const headerState = node.getHeaderStyles();
3995
+ get enabled() {
3996
+ return this.editorElement.supportsRichText
3997
+ }
4040
3998
 
4041
- if (headerState !== TableCellHeaderStates.ROW) return
3999
+ get lexicalExtension() {
4000
+ return defineExtension({
4001
+ name: "lexxy/tables",
4002
+ nodes: [
4003
+ WrappedTableNode,
4004
+ {
4005
+ replace: TableNode,
4006
+ with: () => new WrappedTableNode(),
4007
+ withKlass: WrappedTableNode
4008
+ },
4009
+ TableCellNode,
4010
+ TableRowNode
4011
+ ],
4012
+ register(editor) {
4013
+ return mergeRegister(
4014
+ // Register Lexical table plugins
4015
+ registerTablePlugin(editor),
4016
+ registerTableSelectionObserver(editor, true),
4017
+ setScrollableTablesActive(editor, true),
4018
+
4019
+ // Bug fix: Prevent hardcoded background color (Lexical #8089)
4020
+ editor.registerNodeTransform(TableCellNode, (node) => {
4021
+ if (node.getBackgroundColor() === null) {
4022
+ node.setBackgroundColor("");
4023
+ }
4024
+ }),
4042
4025
 
4043
- const rowParent = node.getParent();
4044
- const tableNode = rowParent?.getParent();
4045
- if (!tableNode) return
4026
+ // Bug fix: Fix column header states (Lexical #8090)
4027
+ editor.registerNodeTransform(TableCellNode, (node) => {
4028
+ const headerState = node.getHeaderStyles();
4046
4029
 
4047
- const rows = tableNode.getChildren();
4048
- const cellIndex = rowParent.getChildren().indexOf(node);
4030
+ if (headerState !== TableCellHeaderStates.ROW) return
4049
4031
 
4050
- const cellsInRow = rowParent.getChildren();
4051
- const isHeaderRow = cellsInRow.every(cell =>
4052
- cell.getHeaderStyles() !== TableCellHeaderStates.NO_STATUS
4053
- );
4032
+ const rowParent = node.getParent();
4033
+ const tableNode = rowParent?.getParent();
4034
+ if (!tableNode) return
4054
4035
 
4055
- const isHeaderColumn = rows.every(row => {
4056
- const cell = row.getChildren()[cellIndex];
4057
- return cell && cell.getHeaderStyles() !== TableCellHeaderStates.NO_STATUS
4058
- });
4036
+ const rows = tableNode.getChildren();
4037
+ const cellIndex = rowParent.getChildren().indexOf(node);
4059
4038
 
4060
- let newHeaderState = TableCellHeaderStates.NO_STATUS;
4039
+ const cellsInRow = rowParent.getChildren();
4040
+ const isHeaderRow = cellsInRow.every(cell =>
4041
+ cell.getHeaderStyles() !== TableCellHeaderStates.NO_STATUS
4042
+ );
4061
4043
 
4062
- if (isHeaderRow) {
4063
- newHeaderState |= TableCellHeaderStates.ROW;
4064
- }
4044
+ const isHeaderColumn = rows.every(row => {
4045
+ const cell = row.getChildren()[cellIndex];
4046
+ return cell && cell.getHeaderStyles() !== TableCellHeaderStates.NO_STATUS
4047
+ });
4065
4048
 
4066
- if (isHeaderColumn) {
4067
- newHeaderState |= TableCellHeaderStates.COLUMN;
4068
- }
4049
+ let newHeaderState = TableCellHeaderStates.NO_STATUS;
4069
4050
 
4070
- if (newHeaderState !== headerState) {
4071
- node.setHeaderStyles(newHeaderState, TableCellHeaderStates.BOTH);
4072
- }
4073
- });
4051
+ if (isHeaderRow) newHeaderState |= TableCellHeaderStates.ROW;
4052
+ if (isHeaderColumn) newHeaderState |= TableCellHeaderStates.COLUMN;
4074
4053
 
4075
- editor.registerCommand("insertTableRowAfter", () => {
4076
- $insertTableRowAtSelection(true);
4077
- }, COMMAND_PRIORITY_NORMAL);
4054
+ if (newHeaderState !== headerState) {
4055
+ node.setHeaderStyles(newHeaderState, TableCellHeaderStates.BOTH);
4056
+ }
4057
+ }),
4078
4058
 
4079
- editor.registerCommand("insertTableRowBefore", () => {
4080
- $insertTableRowAtSelection(false);
4081
- }, COMMAND_PRIORITY_NORMAL);
4059
+ editor.registerCommand("insertTableRowAfter", () => {
4060
+ $insertTableRowAtSelection(true);
4061
+ }, COMMAND_PRIORITY_NORMAL),
4082
4062
 
4083
- editor.registerCommand("insertTableColumnAfter", () => {
4084
- $insertTableColumnAtSelection(true);
4085
- }, COMMAND_PRIORITY_NORMAL);
4063
+ editor.registerCommand("insertTableRowBefore", () => {
4064
+ $insertTableRowAtSelection(false);
4065
+ }, COMMAND_PRIORITY_NORMAL),
4086
4066
 
4087
- editor.registerCommand("insertTableColumnBefore", () => {
4088
- $insertTableColumnAtSelection(false);
4089
- }, COMMAND_PRIORITY_NORMAL);
4067
+ editor.registerCommand("insertTableColumnAfter", () => {
4068
+ $insertTableColumnAtSelection(true);
4069
+ }, COMMAND_PRIORITY_NORMAL),
4090
4070
 
4091
- editor.registerCommand("deleteTableRow", () => {
4092
- $deleteTableRowAtSelection();
4093
- }, COMMAND_PRIORITY_NORMAL);
4071
+ editor.registerCommand("insertTableColumnBefore", () => {
4072
+ $insertTableColumnAtSelection(false);
4073
+ }, COMMAND_PRIORITY_NORMAL),
4094
4074
 
4095
- editor.registerCommand("deleteTableColumn", () => {
4096
- $deleteTableColumnAtSelection();
4097
- }, COMMAND_PRIORITY_NORMAL);
4075
+ editor.registerCommand("deleteTableRow", () => {
4076
+ $deleteTableRowAtSelection();
4077
+ }, COMMAND_PRIORITY_NORMAL),
4098
4078
 
4099
- editor.registerCommand("deleteTable", () => {
4100
- const selection = $getSelection();
4101
- if (!$isRangeSelection(selection)) return false
4102
- $findTableNode(selection.anchor.getNode())?.remove();
4103
- }, COMMAND_PRIORITY_NORMAL);
4079
+ editor.registerCommand("deleteTableColumn", () => {
4080
+ $deleteTableColumnAtSelection();
4081
+ }, COMMAND_PRIORITY_NORMAL),
4082
+
4083
+ editor.registerCommand("deleteTable", () => {
4084
+ const selection = $getSelection();
4085
+ if (!$isRangeSelection(selection)) return false
4086
+ $findTableNode(selection.anchor.getNode())?.remove();
4087
+ }, COMMAND_PRIORITY_NORMAL)
4088
+ )
4089
+ }
4090
+ })
4104
4091
  }
4105
- });
4092
+ }
4106
4093
 
4107
4094
  class LexicalEditorElement extends HTMLElement {
4108
4095
  static formAssociated = true
@@ -4124,7 +4111,6 @@ class LexicalEditorElement extends HTMLElement {
4124
4111
  this.id ??= generateDomId("lexxy-editor");
4125
4112
  this.config = new EditorConfiguration(this);
4126
4113
  this.extensions = new Extensions(this);
4127
- this.highlighter = new Highlighter(this);
4128
4114
 
4129
4115
  this.editor = this.#createEditor();
4130
4116
 
@@ -4189,6 +4175,15 @@ class LexicalEditorElement extends HTMLElement {
4189
4175
  return this.toolbar
4190
4176
  }
4191
4177
 
4178
+ get baseExtensions() {
4179
+ return [
4180
+ ProvisionalParagraphExtension,
4181
+ HighlightExtension,
4182
+ TrixContentExtension,
4183
+ TablesExtension
4184
+ ]
4185
+ }
4186
+
4192
4187
  get directUploadUrl() {
4193
4188
  return this.dataset.directUploadUrl
4194
4189
  }
@@ -4271,23 +4266,33 @@ class LexicalEditorElement extends HTMLElement {
4271
4266
 
4272
4267
  #parseHtmlIntoLexicalNodes(html) {
4273
4268
  if (!html) html = "<p></p>";
4274
- const nodes = $generateNodesFromDOM(this.editor, parseHtml(`<div>${html}</div>`));
4269
+ const nodes = $generateNodesFromDOM(this.editor, parseHtml(`${html}`));
4275
4270
 
4276
- if (nodes.length === 0) {
4277
- return [ $createParagraphNode() ]
4278
- }
4271
+ return nodes
4272
+ .map(this.#wrapTextNode)
4273
+ .map(this.#unwrapDecoratorNode)
4274
+ }
4279
4275
 
4280
- // Custom decorator block elements such action-text-attachments get wrapped into <p> automatically by Lexical.
4281
- // We flatten those.
4282
- return nodes.map(node => {
4283
- if (node.getType() === "paragraph" && node.getChildrenSize() === 1) {
4284
- const child = node.getFirstChild();
4285
- if (child instanceof DecoratorNode && !child.isInline()) {
4286
- return child
4287
- }
4276
+ // Raw string values produce TextNodes which cannot be appended directly to the RootNode.
4277
+ // We wrap those in <p>
4278
+ #wrapTextNode(node) {
4279
+ if (!$isTextNode(node)) return node
4280
+
4281
+ const paragraph = $createParagraphNode();
4282
+ paragraph.append(node);
4283
+ return paragraph
4284
+ }
4285
+
4286
+ // Custom decorator block elements such as action-text-attachments get wrapped into <p> automatically by Lexical.
4287
+ // We unwrap those.
4288
+ #unwrapDecoratorNode(node) {
4289
+ if ($isParagraphNode(node) && node.getChildrenSize() === 1) {
4290
+ const child = node.getFirstChild();
4291
+ if ($isDecoratorNode(child) && !child.isInline()) {
4292
+ return child
4288
4293
  }
4289
- return node
4290
- })
4294
+ }
4295
+ return node
4291
4296
  }
4292
4297
 
4293
4298
  #initialize() {
@@ -4310,7 +4315,7 @@ class LexicalEditorElement extends HTMLElement {
4310
4315
  theme: theme,
4311
4316
  nodes: this.#lexicalNodes
4312
4317
  },
4313
- ...this.#lexicalExtensions
4318
+ ...this.extensions.lexicalExtensions
4314
4319
  );
4315
4320
 
4316
4321
  editor.setRootElement(this.editorContentElement);
@@ -4318,23 +4323,6 @@ class LexicalEditorElement extends HTMLElement {
4318
4323
  return editor
4319
4324
  }
4320
4325
 
4321
- get #lexicalExtensions() {
4322
- const extensions = [];
4323
- const richTextExtensions = [
4324
- this.highlighter.lexicalExtension,
4325
- TrixContentExtension,
4326
- TablesLexicalExtension
4327
- ];
4328
-
4329
- if (this.supportsRichText) {
4330
- extensions.push(...richTextExtensions);
4331
- }
4332
-
4333
- extensions.push(...this.extensions.lexicalExtensions);
4334
-
4335
- return extensions
4336
- }
4337
-
4338
4326
  get #lexicalNodes() {
4339
4327
  const nodes = [ CustomActionTextAttachmentNode ];
4340
4328
 
@@ -4540,6 +4528,7 @@ class LexicalEditorElement extends HTMLElement {
4540
4528
  #attachToolbar() {
4541
4529
  if (this.#hasToolbar) {
4542
4530
  this.toolbarElement.setEditor(this);
4531
+ this.extensions.initializeToolbars();
4543
4532
  }
4544
4533
  }
4545
4534
 
@@ -6274,35 +6263,6 @@ function defineElements() {
6274
6263
  });
6275
6264
  }
6276
6265
 
6277
- class LexxyExtension {
6278
- #editorElement
6279
-
6280
- constructor(editorElement) {
6281
- this.#editorElement = editorElement;
6282
- }
6283
-
6284
- get editorElement() {
6285
- return this.#editorElement
6286
- }
6287
-
6288
- get editorConfig() {
6289
- return this.#editorElement.config
6290
- }
6291
-
6292
- // optional: defaults to true
6293
- get enabled() {
6294
- return true
6295
- }
6296
-
6297
- get lexicalExtension() {
6298
- return null
6299
- }
6300
-
6301
- initializeToolbar(_lexxyToolbar) {
6302
-
6303
- }
6304
- }
6305
-
6306
6266
  const configure = Lexxy.configure;
6307
6267
 
6308
6268
  // Pushing elements definition to after the current call stack to allow global configuration to take place first