@37signals/lexxy 0.9.9-beta.preview6 → 0.9.11-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
@@ -1,8 +1,7 @@
1
- import { isActiveAndVisible, extractPlainTextFromHtml, createElement, createAttachmentFigure, isPreviewableImage, dispatch, parseHtml, addBlockSpacing, generateDomId } from './lexxy_helpers.esm.js';
2
1
  export { highlightCode } from './lexxy_helpers.esm.js';
3
2
  import DOMPurify from 'dompurify';
4
3
  import { getStyleObjectFromCSS, getCSSFromStyleObject, $ensureForwardRangeSelection, $isAtNodeEnd, $getSelectionStyleValueForProperty, $patchStyleText, $setBlocksType, $forEachSelectedTextNode } from '@lexical/selection';
5
- import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_REDO_COMMAND, $getSelection, $isRangeSelection, DecoratorNode, $createTextNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $createNodeSelection, $isDecoratorNode, $isLineBreakNode, $isTextNode, $isParagraphNode, $splitNode, $getSiblingCaret, LineBreakNode, $createParagraphNode, TextNode, createCommand, defineExtension, COMMAND_PRIORITY_EDITOR, $getEditor, $getNodeByKey, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $cloneWithProperties, $getNearestRootOrShadowRoot, $createRangeSelection, $setSelection, createState, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $isNodeSelection, $getRoot, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, DELETE_CHARACTER_COMMAND, SELECTION_CHANGE_COMMAND, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, $addUpdateTag, ElementNode, $getChildCaretAtIndex, $createLineBreakNode, ParagraphNode, RootNode, COMMAND_PRIORITY_HIGH, DRAGSTART_COMMAND, DROP_COMMAND, INSERT_PARAGRAPH_COMMAND, mergeRegister as mergeRegister$1, $findMatchingParent, CLEAR_HISTORY_COMMAND, $onUpdate, KEY_ENTER_COMMAND, COMMAND_PRIORITY_CRITICAL, KEY_SPACE_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
4
+ import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_REDO_COMMAND, $getSelection, $isRangeSelection, DecoratorNode, $createTextNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $createNodeSelection, $isDecoratorNode, $isLineBreakNode, $isTextNode, $isParagraphNode, $splitNode, $getSiblingCaret, LineBreakNode, $createParagraphNode, $getCommonAncestor, $findMatchingParent, TextNode, createCommand, defineExtension, COMMAND_PRIORITY_EDITOR, $getEditor, $getNodeByKey, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $cloneWithProperties, $getNearestRootOrShadowRoot, $createRangeSelection, $setSelection, createState, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, UNDO_COMMAND, REDO_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $isNodeSelection, $getRoot, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, DELETE_CHARACTER_COMMAND, SELECTION_CHANGE_COMMAND, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, $addUpdateTag, ElementNode, $getChildCaretAtIndex, $createLineBreakNode, PASTE_COMMAND, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, ParagraphNode, RootNode, COMMAND_PRIORITY_HIGH, DRAGSTART_COMMAND, DROP_COMMAND, INSERT_PARAGRAPH_COMMAND, mergeRegister as mergeRegister$1, CLEAR_HISTORY_COMMAND, $onUpdate, KEY_ENTER_COMMAND, COMMAND_PRIORITY_CRITICAL, KEY_SPACE_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
6
5
  import { buildEditorFromExtensions } from '@lexical/extension';
7
6
  import { ListNode, ListItemNode, $getListDepth, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, $isListItemNode, $isListNode, registerList } from '@lexical/list';
8
7
  import { LinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, $isLinkNode, AutoLinkNode } from '@lexical/link';
@@ -126,6 +125,64 @@ class ListenerBin {
126
125
  }
127
126
  }
128
127
 
128
+ function createElement(name, properties, content = "") {
129
+ const element = document.createElement(name);
130
+ for (const [ key, value ] of Object.entries(properties || {})) {
131
+ if (key in element) {
132
+ element[key] = value;
133
+ } else if (value !== null && value !== undefined) {
134
+ element.setAttribute(key, value);
135
+ }
136
+ }
137
+ if (content) {
138
+ element.innerHTML = content;
139
+ }
140
+ return element
141
+ }
142
+
143
+ function parseHtml(html) {
144
+ const parser = new DOMParser();
145
+ return parser.parseFromString(html, "text/html")
146
+ }
147
+
148
+ function createAttachmentFigure(contentType, isPreviewable, fileName) {
149
+ const extension = fileName ? fileName.split(".").pop().toLowerCase() : "unknown";
150
+ return createElement("figure", {
151
+ className: `attachment attachment--${isPreviewable ? "preview" : "file"} attachment--${extension}`,
152
+ "data-content-type": contentType
153
+ })
154
+ }
155
+
156
+ function isPreviewableImage(contentType) {
157
+ return contentType.startsWith("image/") && !contentType.includes("svg")
158
+ }
159
+
160
+ function dispatch(element, eventName, detail = null, cancelable = false) {
161
+ return element.dispatchEvent(new CustomEvent(eventName, { bubbles: true, detail, cancelable }))
162
+ }
163
+
164
+ function addBlockSpacing(doc) {
165
+ const blocks = doc.querySelectorAll("body > :not(h1, h2, h3, h4, h5, h6) + *");
166
+ for (const block of blocks) {
167
+ const spacer = doc.createElement("p");
168
+ spacer.appendChild(doc.createElement("br"));
169
+ block.before(spacer);
170
+ }
171
+ }
172
+
173
+ function generateDomId(prefix) {
174
+ const randomPart = Math.random().toString(36).slice(2, 10);
175
+ return `${prefix}-${randomPart}`
176
+ }
177
+
178
+ function extractPlainTextFromHtml(innerHtml = "") {
179
+ return parseHtml(innerHtml).body.textContent.trim()
180
+ }
181
+
182
+ function isActiveAndVisible(element) {
183
+ return element && !element.disabled && element.checkVisibility()
184
+ }
185
+
129
186
  function handleRollingTabIndex(elements, event) {
130
187
  const previousActiveElement = document.activeElement;
131
188
 
@@ -553,15 +610,23 @@ class LexicalToolbarElement extends HTMLElement {
553
610
  #setButtonPressed(name, isPressed) {
554
611
  const button = this.querySelector(`[name="${name}"]`);
555
612
  if (button) {
556
- button.setAttribute("aria-pressed", isPressed.toString());
613
+ const next = isPressed.toString();
614
+ if (button.getAttribute("aria-pressed") !== next) {
615
+ button.setAttribute("aria-pressed", next);
616
+ }
557
617
  }
558
618
  }
559
619
 
560
620
  #setButtonDisabled(name, isDisabled) {
561
621
  const button = this.querySelector(`[name="${name}"]`);
562
622
  if (button) {
563
- button.disabled = isDisabled;
564
- button.setAttribute("aria-disabled", isDisabled.toString());
623
+ if (button.disabled !== isDisabled) {
624
+ button.disabled = isDisabled;
625
+ }
626
+ const next = isDisabled.toString();
627
+ if (button.getAttribute("aria-disabled") !== next) {
628
+ button.setAttribute("aria-disabled", next);
629
+ }
565
630
  }
566
631
  }
567
632
 
@@ -1078,6 +1143,18 @@ class LexxyExtension {
1078
1143
  initializeToolbar(_lexxyToolbar) {
1079
1144
 
1080
1145
  }
1146
+
1147
+ dispose() {
1148
+ }
1149
+ }
1150
+
1151
+ function $containsRangeSelection(node, selection = $getSelection()) {
1152
+ if ($isRangeSelection(selection)) {
1153
+ const { commonAncestor } = $getCommonAncestor(selection.focus.getNode(), selection.anchor.getNode());
1154
+ return $findMatchingParent(commonAncestor, parent => parent.is(node))
1155
+ } else {
1156
+ return false
1157
+ }
1081
1158
  }
1082
1159
 
1083
1160
  function $createNodeSelectionWith(...nodes) {
@@ -2813,17 +2890,12 @@ class CommandDispatcher {
2813
2890
  this.editor = editorElement.editor;
2814
2891
  this.selection = editorElement.selection;
2815
2892
  this.contents = editorElement.contents;
2816
- this.clipboard = editorElement.clipboard;
2817
2893
 
2818
2894
  this.#registerCommands();
2819
2895
  this.#registerKeyboardCommands();
2820
2896
  this.#registerDragAndDropHandlers();
2821
2897
  }
2822
2898
 
2823
- dispatchPaste(event) {
2824
- return this.clipboard.paste(event)
2825
- }
2826
-
2827
2899
  dispatchBold() {
2828
2900
  this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold");
2829
2901
  }
@@ -3050,8 +3122,6 @@ class CommandDispatcher {
3050
3122
  const methodName = `dispatch${capitalize(command)}`;
3051
3123
  this.#registerCommandHandler(command, 0, this[methodName].bind(this));
3052
3124
  }
3053
-
3054
- this.#registerCommandHandler(PASTE_COMMAND, COMMAND_PRIORITY_LOW, this.dispatchPaste.bind(this));
3055
3125
  }
3056
3126
 
3057
3127
  #registerCommandHandler(command, priority, handler) {
@@ -3412,6 +3482,11 @@ class Selection {
3412
3482
  return $isActionTextAttachmentNode(firstNode) && firstNode.isPreviewableImage
3413
3483
  }
3414
3484
 
3485
+ get isAtNodeStart() {
3486
+ const { anchorNode, offset } = this.#getCollapsedSelectionData();
3487
+ return anchorNode && offset === 0
3488
+ }
3489
+
3415
3490
  get nodeAfterCursor() {
3416
3491
  const { anchorNode, offset } = this.#getCollapsedSelectionData();
3417
3492
  if (!anchorNode) return null
@@ -4586,8 +4661,9 @@ class Uploader {
4586
4661
  return new UploaderKlass(editorElement, files)
4587
4662
  }
4588
4663
 
4589
- constructor(editorElement, files) {
4664
+ constructor(editorElement, files, options = {}) {
4590
4665
  this.#files = files;
4666
+ this.options = options;
4591
4667
 
4592
4668
  this.editorElement = editorElement;
4593
4669
  this.contents = editorElement.contents;
@@ -4629,10 +4705,10 @@ class GalleryUploader extends Uploader {
4629
4705
  #gallery
4630
4706
 
4631
4707
  static handle(editorElement, files) {
4632
- return this.#isMultipleImageUpload(files) || this.#gallerySelection(editorElement.selection)
4708
+ return this.isMultipleImageUpload(files) || this.gallerySelection(editorElement.selection)
4633
4709
  }
4634
4710
 
4635
- static #isMultipleImageUpload(files) {
4711
+ static isMultipleImageUpload(files) {
4636
4712
  let imageFileCount = 0;
4637
4713
  for (const file of files) {
4638
4714
  if (isPreviewableImage(file.type)) imageFileCount++;
@@ -4641,11 +4717,12 @@ class GalleryUploader extends Uploader {
4641
4717
  return false
4642
4718
  }
4643
4719
 
4644
- static #gallerySelection(selection) {
4645
- if (selection.isOnPreviewableImage) return true
4720
+ static gallerySelection(selection) {
4721
+ return selection.isOnPreviewableImage || this.selectionIsAfterGalleryEdge(selection)
4722
+ }
4646
4723
 
4647
- const { node: selectedNode } = selection.selectedNodeWithOffset();
4648
- return $getNearestNodeOfType(selectedNode, ImageGalleryNode) !== null
4724
+ static selectionIsAfterGalleryEdge(selection) {
4725
+ return selection.isAtNodeStart && ImageGalleryNode.canCollapseWith(selection.nodeBeforeCursor)
4649
4726
  }
4650
4727
 
4651
4728
  $insertUploadNodes() {
@@ -4657,18 +4734,26 @@ class GalleryUploader extends Uploader {
4657
4734
  #findOrCreateGallery() {
4658
4735
  if (this.selection.isOnPreviewableImage) {
4659
4736
  this.#gallery = $findOrCreateGalleryForImage(this.#selectedNode);
4737
+ } else if (this.#selectionIsAfterGalleryEdge) {
4738
+ this.#gallery = $findOrCreateGalleryForImage(this.selection.nodeBeforeCursor);
4660
4739
  } else {
4661
4740
  this.#gallery = $createImageGalleryNode();
4662
4741
  this.contents.insertAtCursor(this.#gallery);
4663
4742
  }
4664
4743
  }
4665
4744
 
4745
+ get #selectionIsAfterGalleryEdge() {
4746
+ return this.constructor.selectionIsAfterGalleryEdge(this.selection)
4747
+ }
4748
+
4666
4749
  get #selectedNode() {
4667
4750
  const { node } = this.selection.selectedNodeWithOffset();
4668
4751
  return node
4669
4752
  }
4670
4753
 
4671
4754
  get #galleryInsertPosition() {
4755
+ if (this.#selectionIsAfterGalleryEdge) return this.#gallery.getChildrenSize()
4756
+
4672
4757
  const anchor = $getSelection()?.anchor;
4673
4758
  const galleryHasElementSelection = anchor?.getNode().is(this.#gallery);
4674
4759
  if (galleryHasElementSelection) return anchor.offset
@@ -5396,10 +5481,22 @@ class Contents {
5396
5481
  }
5397
5482
 
5398
5483
  class Clipboard {
5484
+ #listeners = new ListenerBin()
5485
+
5399
5486
  constructor(editorElement) {
5400
5487
  this.editorElement = editorElement;
5401
5488
  this.editor = editorElement.editor;
5402
5489
  this.contents = editorElement.contents;
5490
+
5491
+ this.#registerPasteCommands();
5492
+ }
5493
+
5494
+ dispose() {
5495
+ this.editorElement = null;
5496
+ this.editor = null;
5497
+ this.contents = null;
5498
+
5499
+ this.#listeners.dispose();
5403
5500
  }
5404
5501
 
5405
5502
  paste(event) {
@@ -5424,6 +5521,25 @@ class Clipboard {
5424
5521
  return handled
5425
5522
  }
5426
5523
 
5524
+ #registerPasteCommands() {
5525
+ this.#listeners.track(
5526
+ this.editor.registerCommand(PASTE_COMMAND, this.paste.bind(this), COMMAND_PRIORITY_NORMAL),
5527
+ this.editor.registerCommand(
5528
+ SELECTION_INSERT_CLIPBOARD_NODES_COMMAND,
5529
+ (payload) => this.#handleParsedClipboardNodes(payload),
5530
+ COMMAND_PRIORITY_NORMAL
5531
+ )
5532
+ );
5533
+ }
5534
+
5535
+ #handleParsedClipboardNodes({ nodes, selection }) {
5536
+ const url = $bareUrlFromSingleLink(nodes);
5537
+ if (!url) return false
5538
+
5539
+ this.#insertSingleLinkAt(selection, url);
5540
+ return true
5541
+ }
5542
+
5427
5543
  #isPlainTextOrURLPasted(clipboardData) {
5428
5544
  return this.#isOnlyPlainTextPasted(clipboardData) || this.#isOnlyURLPasted(clipboardData)
5429
5545
  }
@@ -5486,6 +5602,24 @@ class Clipboard {
5486
5602
  });
5487
5603
  }
5488
5604
 
5605
+ #insertSingleLinkAt(selection, url) {
5606
+ if (!$isRangeSelection(selection)) return
5607
+
5608
+ if (!selection.isCollapsed()) {
5609
+ $toggleLink(null);
5610
+ $toggleLink(url);
5611
+ return
5612
+ }
5613
+
5614
+ const linkNode = $createLinkNode(url).append($createTextNode(url));
5615
+ selection.insertNodes([ linkNode ]);
5616
+
5617
+ // Defer the lexxy:insert-link event until after the active update commits;
5618
+ // listeners may run editor mutations of their own.
5619
+ const nodeKey = linkNode.getKey();
5620
+ Promise.resolve().then(() => this.#dispatchLinkInsertEvent(nodeKey, { url }));
5621
+ }
5622
+
5489
5623
  #dispatchLinkInsertEvent(nodeKey, payload) {
5490
5624
  const linkManipulationMethods = {
5491
5625
  replaceLinkWith: (html, options) => this.contents.replaceNodeWithHTML(nodeKey, html, options),
@@ -5577,6 +5711,28 @@ class Clipboard {
5577
5711
  }
5578
5712
  }
5579
5713
 
5714
+ function $bareUrlFromSingleLink(nodes) {
5715
+ if (nodes.length !== 1) return null
5716
+
5717
+ const node = nodes[0];
5718
+ if ($isLinkNode(node)) return $bareUrlFromLink(node)
5719
+
5720
+ if ($isParagraphNode(node)) {
5721
+ const children = node.getChildren();
5722
+ if (children.length === 1 && $isLinkNode(children[0])) {
5723
+ return $bareUrlFromLink(children[0])
5724
+ }
5725
+ }
5726
+
5727
+ return null
5728
+ }
5729
+
5730
+ function $bareUrlFromLink(linkNode) {
5731
+ const url = linkNode.getURL();
5732
+ if (!url) return null
5733
+ return linkNode.getTextContent() === url ? url : null
5734
+ }
5735
+
5580
5736
  class Extensions {
5581
5737
 
5582
5738
  constructor(lexxyElement) {
@@ -5597,6 +5753,12 @@ class Extensions {
5597
5753
  this.#addExtensionToolbarButtons(toolbar);
5598
5754
  }
5599
5755
 
5756
+ dispose() {
5757
+ while (this.enabledExtensions.length) {
5758
+ this.enabledExtensions.pop().dispose();
5759
+ }
5760
+ }
5761
+
5600
5762
  #clearPreviousExtensionToolbarButtons(toolbar) {
5601
5763
  toolbar.querySelectorAll("[data-lexxy-extension]").forEach(el => el.remove());
5602
5764
  }
@@ -6002,9 +6164,15 @@ class TablesExtension extends LexxyExtension {
6002
6164
  setScrollableTablesActive(editor, true);
6003
6165
 
6004
6166
  return mergeRegister(
6005
- // Register Lexical table plugins
6006
6167
  registerTablePlugin(editor),
6007
- registerTableSelectionObserver(editor, true),
6168
+
6169
+ // Lexxy registers extensions before setRootElement(), but table
6170
+ // drag-selection needs a root before wiring its pointer handlers.
6171
+ editor.registerRootListener((rootElement) => {
6172
+ if (rootElement) {
6173
+ return registerTableSelectionObserver(editor, true)
6174
+ }
6175
+ }),
6008
6176
 
6009
6177
  // Bug fix: Prevent hardcoded background color (Lexical #8089)
6010
6178
  editor.registerNodeTransform(TableCellNode, (node) => {
@@ -6712,7 +6880,8 @@ class FormatEscapeExtension extends LexxyExtension {
6712
6880
  KEY_ARROW_DOWN_COMMAND,
6713
6881
  (event) => $handleArrowDownInCodeBlock(event),
6714
6882
  COMMAND_PRIORITY_NORMAL
6715
- )
6883
+ ),
6884
+ editor.registerNodeTransform(QuoteNode, $ensureQuoteHasParagraphChild)
6716
6885
  )
6717
6886
  }
6718
6887
  })
@@ -6765,6 +6934,13 @@ function $handleArrowDownInCodeBlock(event) {
6765
6934
  return false
6766
6935
  }
6767
6936
 
6937
+ function $ensureQuoteHasParagraphChild(quoteNode) {
6938
+ if (!quoteNode.isEmpty()) return
6939
+
6940
+ quoteNode.append($createParagraphNode());
6941
+ if ($containsRangeSelection(quoteNode)) quoteNode.getFirstChild().select();
6942
+ }
6943
+
6768
6944
  class LinkOpenerExtension extends LexxyExtension {
6769
6945
  get enabled() {
6770
6946
  return this.editorElement.supportsRichText
@@ -6852,6 +7028,7 @@ class LexicalEditorElement extends HTMLElement {
6852
7028
  this.id ||= generateDomId("lexxy-editor");
6853
7029
  this.config = new EditorConfiguration(this);
6854
7030
  this.extensions = new Extensions(this);
7031
+ this.#disposables.push(this.extensions);
6855
7032
 
6856
7033
  this.editor = this.#createEditor();
6857
7034
  this.#disposables.push(this.editor);
@@ -6864,6 +7041,8 @@ class LexicalEditorElement extends HTMLElement {
6864
7041
  this.#disposables.push(this.selection);
6865
7042
 
6866
7043
  this.clipboard = new Clipboard(this);
7044
+ this.#disposables.push(this.clipboard);
7045
+
6867
7046
  this.adapter = new BrowserAdapter();
6868
7047
 
6869
7048
  const commandDispatcher = CommandDispatcher.configureFor(this);
@@ -7828,7 +8007,10 @@ class HighlightDropdown extends ToolbarDropdown {
7828
8007
 
7829
8008
  this.#colorButtons.forEach(button => {
7830
8009
  const matchesSelection = button.dataset.value === textColor || button.dataset.value === backgroundColor;
7831
- button.setAttribute("aria-pressed", matchesSelection);
8010
+ const next = matchesSelection.toString();
8011
+ if (button.getAttribute("aria-pressed") !== next) {
8012
+ button.setAttribute("aria-pressed", next);
8013
+ }
7832
8014
  });
7833
8015
 
7834
8016
  const hasHighlight = textColor !== NO_STYLE || backgroundColor !== NO_STYLE;
@@ -8236,7 +8418,7 @@ class LexicalPromptElement extends HTMLElement {
8236
8418
  }
8237
8419
 
8238
8420
  #selectOption(listItem) {
8239
- this.#clearSelection();
8421
+ this.#clearListItemSelection();
8240
8422
  listItem.toggleAttribute("aria-selected", true);
8241
8423
  listItem.scrollIntoView({ block: "nearest", behavior: "smooth" });
8242
8424
  listItem.focus();
@@ -8246,18 +8428,28 @@ class LexicalPromptElement extends HTMLElement {
8246
8428
  this.#editorElement.focus();
8247
8429
  });
8248
8430
 
8249
- this.#editorContentElement.setAttribute("aria-controls", this.popoverElement.id);
8250
- this.#editorContentElement.setAttribute("aria-activedescendant", listItem.id);
8251
- this.#editorContentElement.setAttribute("aria-haspopup", "listbox");
8431
+ this.#setEditorAssociationAttribute("aria-controls", this.popoverElement.id);
8432
+ this.#setEditorAssociationAttribute("aria-activedescendant", listItem.id);
8433
+ this.#setEditorAssociationAttribute("aria-haspopup", "listbox");
8252
8434
  }
8253
8435
 
8254
- #clearSelection() {
8436
+ #clearListItemSelection() {
8255
8437
  this.#listItemElements.forEach((item) => { item.toggleAttribute("aria-selected", false); });
8438
+ }
8439
+
8440
+ #clearSelection() {
8441
+ this.#clearListItemSelection();
8256
8442
  this.#editorContentElement.removeAttribute("aria-controls");
8257
8443
  this.#editorContentElement.removeAttribute("aria-activedescendant");
8258
8444
  this.#editorContentElement.removeAttribute("aria-haspopup");
8259
8445
  }
8260
8446
 
8447
+ #setEditorAssociationAttribute(name, value) {
8448
+ if (this.#editorContentElement.getAttribute(name) !== value) {
8449
+ this.#editorContentElement.setAttribute(name, value);
8450
+ }
8451
+ }
8452
+
8261
8453
  #positionPopover() {
8262
8454
  const { x, y, fontSize } = this.#selection.cursorPosition;
8263
8455
  const editorRect = this.#editorElement.getBoundingClientRect();
@@ -28,64 +28,6 @@ import 'prismjs/components/prism-kotlin';
28
28
  window.Prism ||= {};
29
29
  window.Prism.manual = true;
30
30
 
31
- function createElement(name, properties, content = "") {
32
- const element = document.createElement(name);
33
- for (const [ key, value ] of Object.entries(properties || {})) {
34
- if (key in element) {
35
- element[key] = value;
36
- } else if (value !== null && value !== undefined) {
37
- element.setAttribute(key, value);
38
- }
39
- }
40
- if (content) {
41
- element.innerHTML = content;
42
- }
43
- return element
44
- }
45
-
46
- function parseHtml(html) {
47
- const parser = new DOMParser();
48
- return parser.parseFromString(html, "text/html")
49
- }
50
-
51
- function createAttachmentFigure(contentType, isPreviewable, fileName) {
52
- const extension = fileName ? fileName.split(".").pop().toLowerCase() : "unknown";
53
- return createElement("figure", {
54
- className: `attachment attachment--${isPreviewable ? "preview" : "file"} attachment--${extension}`,
55
- "data-content-type": contentType
56
- })
57
- }
58
-
59
- function isPreviewableImage(contentType) {
60
- return contentType.startsWith("image/") && !contentType.includes("svg")
61
- }
62
-
63
- function dispatch(element, eventName, detail = null, cancelable = false) {
64
- return element.dispatchEvent(new CustomEvent(eventName, { bubbles: true, detail, cancelable }))
65
- }
66
-
67
- function addBlockSpacing(doc) {
68
- const blocks = doc.querySelectorAll("body > :not(h1, h2, h3, h4, h5, h6) + *");
69
- for (const block of blocks) {
70
- const spacer = doc.createElement("p");
71
- spacer.appendChild(doc.createElement("br"));
72
- block.before(spacer);
73
- }
74
- }
75
-
76
- function generateDomId(prefix) {
77
- const randomPart = Math.random().toString(36).slice(2, 10);
78
- return `${prefix}-${randomPart}`
79
- }
80
-
81
- function extractPlainTextFromHtml(innerHtml = "") {
82
- return parseHtml(innerHtml).body.textContent.trim()
83
- }
84
-
85
- function isActiveAndVisible(element) {
86
- return element && !element.disabled && element.checkVisibility()
87
- }
88
-
89
31
  function highlightCode() {
90
32
  const elements = document.querySelectorAll("pre[data-language]");
91
33
 
@@ -108,13 +50,11 @@ function highlightElement(preElement) {
108
50
  code = new DOMParser().parseFromString(code, "text/html").body.textContent || "";
109
51
 
110
52
  const highlightedHtml = Prism.highlight(code, grammar, language);
111
- const codeElement = createElement("code", { "data-language": language, innerHTML: highlightedHtml });
53
+ preElement.innerHTML = highlightedHtml;
112
54
 
113
55
  if (highlights.length > 0) {
114
- applyHighlightRanges(codeElement, highlights);
56
+ applyHighlightRanges(preElement, highlights);
115
57
  }
116
-
117
- preElement.replaceChildren(codeElement);
118
58
  }
119
59
 
120
60
  // Walk the DOM tree inside a <pre> element and build a list of
@@ -220,4 +160,4 @@ function collectTextNodes(root) {
220
160
  return nodes
221
161
  }
222
162
 
223
- export { addBlockSpacing, createAttachmentFigure, createElement, dispatch, extractPlainTextFromHtml, generateDomId, highlightCode, isActiveAndVisible, isPreviewableImage, parseHtml };
163
+ export { highlightCode };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@37signals/lexxy",
3
- "version": "0.9.9-beta.preview6",
3
+ "version": "0.9.11-beta",
4
4
  "description": "Lexxy - A modern rich text editor for Rails.",
5
5
  "module": "dist/lexxy.esm.js",
6
6
  "type": "module",