lexxy 0.1.10.beta → 0.1.12.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2fffdc9d43469a6366a3d6fa16b6e5b8284c5a19bdcfa3cbffeb79a96cf16970
4
- data.tar.gz: b1e76d677c2c7e9c55ff713eba32c89f04cc0e267153f4e4def7cf48238dc733
3
+ metadata.gz: 18d165a2a610d44b12147598b0d876c26ac560805e026812b28bedf906fc79ce
4
+ data.tar.gz: b7f6432b4efa2b5674186f11f408ba0d9bebad133a35e6df45b9b70d87102bc5
5
5
  SHA512:
6
- metadata.gz: 0c7752a992efceb4f40f411bb154037a21b7ffeb83db7dc28bf882f68ed2ff57f21ebe4c1d61dc420d6b344fd45fb8d819595a753e33d90134a8e3536d6fd0ba
7
- data.tar.gz: 761ce9eee667c72a4cab67a5572cad3013f468c9248f415477c144b640a26852400c07afcdbe287afe492f858d54a798281ac96c89814fd69683f3ecd4488d77
6
+ metadata.gz: 7283442089ee6b769f928a9551c048da6c69e242fe5f7f7213bee79e03fb6567abaaa14319669bf3de62be571f5bbc60b825b5fa639a0ba0236f0559695498de
7
+ data.tar.gz: d9e94ff8f7a1a1493c9d3073aaa6e4bb56a21431ca42f6b61d6d37d4edca3286764f613dcd4b4ab65dce9c5d515349b1d3f4bbc8d38473ab99a85d010c42a6e6
@@ -5091,6 +5091,17 @@ function getListType(node) {
5091
5091
  return null
5092
5092
  }
5093
5093
 
5094
+ function isPrintableCharacter(event) {
5095
+ // Ignore if modifier keys are pressed (except Shift for uppercase)
5096
+ if (event.ctrlKey || event.metaKey || event.altKey) return false
5097
+
5098
+ // Ignore special keys
5099
+ if (event.key.length > 1 && event.key !== 'Enter' && event.key !== 'Space') return false
5100
+
5101
+ // Accept single character keys (letters, numbers, punctuation)
5102
+ return event.key.length === 1
5103
+ }
5104
+
5094
5105
  class LexicalToolbarElement extends HTMLElement {
5095
5106
  constructor() {
5096
5107
  super();
@@ -5551,6 +5562,7 @@ class ActionTextAttachmentNode extends gi {
5551
5562
  conversion: () => ({
5552
5563
  node: new ActionTextAttachmentNode({
5553
5564
  src: img.getAttribute("src"),
5565
+ caption: img.getAttribute("alt") || "",
5554
5566
  contentType: "image/*",
5555
5567
  width: img.getAttribute("width"),
5556
5568
  height: img.getAttribute("height")
@@ -5950,6 +5962,8 @@ class CommandDispatcher {
5950
5962
 
5951
5963
  dispatchInsertUnorderedList() {
5952
5964
  const selection = Nr();
5965
+ if (!selection) return;
5966
+
5953
5967
  const anchorNode = selection.anchor.getNode();
5954
5968
 
5955
5969
  if (this.selection.isInsideList && anchorNode && getListType(anchorNode) === "bullet") {
@@ -5961,6 +5975,8 @@ class CommandDispatcher {
5961
5975
 
5962
5976
  dispatchInsertOrderedList() {
5963
5977
  const selection = Nr();
5978
+ if (!selection) return;
5979
+
5964
5980
  const anchorNode = selection.anchor.getNode();
5965
5981
 
5966
5982
  if (this.selection.isInsideList && anchorNode && getListType(anchorNode) === "number") {
@@ -6133,11 +6149,14 @@ function nextFrame() {
6133
6149
  class Selection {
6134
6150
  constructor(editorElement) {
6135
6151
  this.editorElement = editorElement;
6152
+ this.editorContentElement = editorElement.editorContentElement;
6136
6153
  this.editor = this.editorElement.editor;
6137
6154
  this.previouslySelectedKeys = new Set();
6138
6155
 
6139
6156
  this.#listenForNodeSelections();
6140
6157
  this.#processSelectionChangeCommands();
6158
+ this.#handleInputWhenDecoratorNodesSelected();
6159
+ this.#containEditorFocus();
6141
6160
  }
6142
6161
 
6143
6162
  clear() {
@@ -6231,6 +6250,21 @@ class Selection {
6231
6250
  return this.#findNextSiblingUp(anchorNode)
6232
6251
  }
6233
6252
 
6253
+ get topLevelNodeAfterCursor() {
6254
+ const { anchorNode, offset } = this.#getCollapsedSelectionData();
6255
+ if (!anchorNode) return null
6256
+
6257
+ if (Qn(anchorNode)) {
6258
+ return this.#getNextNodeFromTextEnd(anchorNode)
6259
+ }
6260
+
6261
+ if (di(anchorNode)) {
6262
+ return this.#getNodeAfterElementNode(anchorNode, offset)
6263
+ }
6264
+
6265
+ return this.#findNextSiblingUp(anchorNode)
6266
+ }
6267
+
6234
6268
  get nodeBeforeCursor() {
6235
6269
  const { anchorNode, offset } = this.#getCollapsedSelectionData();
6236
6270
  if (!anchorNode) return null
@@ -6246,6 +6280,21 @@ class Selection {
6246
6280
  return this.#findPreviousSiblingUp(anchorNode)
6247
6281
  }
6248
6282
 
6283
+ get topLevelNodeBeforeCursor() {
6284
+ const { anchorNode, offset } = this.#getCollapsedSelectionData();
6285
+ if (!anchorNode) return null
6286
+
6287
+ if (Qn(anchorNode)) {
6288
+ return this.#getPreviousNodeFromTextStart(anchorNode)
6289
+ }
6290
+
6291
+ if (di(anchorNode)) {
6292
+ return this.#getNodeBeforeElementNode(anchorNode, offset)
6293
+ }
6294
+
6295
+ return this.#findPreviousSiblingUp(anchorNode)
6296
+ }
6297
+
6249
6298
  get #contents() {
6250
6299
  return this.editorElement.contents
6251
6300
  }
@@ -6266,9 +6315,9 @@ class Selection {
6266
6315
 
6267
6316
  #processSelectionChangeCommands() {
6268
6317
  this.editor.registerCommand(Te$1, this.#selectPreviousNode.bind(this), Ii);
6269
- this.editor.registerCommand(Ne$1, this.#selectPreviousNode.bind(this), Ii);
6270
6318
  this.editor.registerCommand(ve$1, this.#selectNextNode.bind(this), Ii);
6271
- this.editor.registerCommand(we$1, this.#selectNextNode.bind(this), Ii);
6319
+ this.editor.registerCommand(Ne$1, this.#selectPreviousTopLevelNode.bind(this), Ii);
6320
+ this.editor.registerCommand(we$1, this.#selectNextTopLevelNode.bind(this), Ii);
6272
6321
 
6273
6322
  this.editor.registerCommand(De$1, this.#deleteSelectedOrNext.bind(this), Ii);
6274
6323
  this.editor.registerCommand(Ae$1, this.#deletePreviousOrNext.bind(this), Ii);
@@ -6299,6 +6348,93 @@ class Selection {
6299
6348
  });
6300
6349
  }
6301
6350
 
6351
+ // In Safari, when the only node in the document is an attachment, it won't let you enter text
6352
+ // before/below it. There is probably a better fix here, but this workaround solves the problem until
6353
+ // we find it.
6354
+ #handleInputWhenDecoratorNodesSelected() {
6355
+ this.editor.getRootElement().addEventListener("keydown", (event) => {
6356
+ if (isPrintableCharacter(event)) {
6357
+ this.editor.update(() => {
6358
+ const selection = Nr();
6359
+
6360
+ if (cr(selection) && selection.isCollapsed()) {
6361
+ const anchorNode = selection.anchor.getNode();
6362
+ const offset = selection.anchor.offset;
6363
+
6364
+ const nodeBefore = this.#getNodeBeforePosition(anchorNode, offset);
6365
+ const nodeAfter = this.#getNodeAfterPosition(anchorNode, offset);
6366
+
6367
+ if (nodeBefore instanceof gi && !nodeBefore.isInline()) {
6368
+ event.preventDefault();
6369
+ this.#contents.createParagraphAfterNode(nodeBefore, event.key);
6370
+ return
6371
+ } else if (nodeAfter instanceof gi && !nodeAfter.isInline()) {
6372
+ event.preventDefault();
6373
+ this.#contents.createParagraphBeforeNode(nodeAfter, event.key);
6374
+ return
6375
+ }
6376
+ }
6377
+ });
6378
+ }
6379
+ }, true);
6380
+ }
6381
+
6382
+ #getNodeBeforePosition(node, offset) {
6383
+ if (Qn(node) && offset === 0) {
6384
+ return node.getPreviousSibling()
6385
+ }
6386
+ if (di(node) && offset > 0) {
6387
+ return node.getChildAtIndex(offset - 1)
6388
+ }
6389
+ return null
6390
+ }
6391
+
6392
+ #getNodeAfterPosition(node, offset) {
6393
+ if (Qn(node) && offset === node.getTextContentSize()) {
6394
+ return node.getNextSibling()
6395
+ }
6396
+ if (di(node)) {
6397
+ return node.getChildAtIndex(offset)
6398
+ }
6399
+ return null
6400
+ }
6401
+
6402
+ #containEditorFocus() {
6403
+ // Workaround for a bizarre Chrome bug where the cursor abandons the editor to focus on not-focusable elements
6404
+ // above when navigating UP/DOWN when Lexical shows its fake cursor on custom decorator nodes.
6405
+ this.editorContentElement.addEventListener("keydown", (event) => {
6406
+ if (event.key === "ArrowUp") {
6407
+ const lexicalCursor = this.editor.getRootElement().querySelector('[data-lexical-cursor]');
6408
+
6409
+ if (lexicalCursor) {
6410
+ let currentElement = lexicalCursor.previousElementSibling;
6411
+ while (currentElement && currentElement.hasAttribute('data-lexical-cursor')) {
6412
+ currentElement = currentElement.previousElementSibling;
6413
+ }
6414
+
6415
+ if (!currentElement) {
6416
+ event.preventDefault();
6417
+ }
6418
+ }
6419
+ }
6420
+
6421
+ if (event.key === "ArrowDown") {
6422
+ const lexicalCursor = this.editor.getRootElement().querySelector('[data-lexical-cursor]');
6423
+
6424
+ if (lexicalCursor) {
6425
+ let currentElement = lexicalCursor.nextElementSibling;
6426
+ while (currentElement && currentElement.hasAttribute('data-lexical-cursor')) {
6427
+ currentElement = currentElement.nextElementSibling;
6428
+ }
6429
+
6430
+ if (!currentElement) {
6431
+ event.preventDefault();
6432
+ }
6433
+ }
6434
+ }
6435
+ }, true);
6436
+ }
6437
+
6302
6438
  #syncSelectedClasses() {
6303
6439
  this.#clearPreviouslyHighlightedItems();
6304
6440
  this.#highlightNewItems();
@@ -6341,6 +6477,22 @@ class Selection {
6341
6477
  }
6342
6478
  }
6343
6479
 
6480
+ async #selectPreviousTopLevelNode() {
6481
+ if (this.current) {
6482
+ await this.#withCurrentNode((currentNode) => currentNode.selectPrevious());
6483
+ } else {
6484
+ this.#selectInLexical(this.topLevelNodeBeforeCursor);
6485
+ }
6486
+ }
6487
+
6488
+ async #selectNextTopLevelNode() {
6489
+ if (this.current) {
6490
+ await this.#withCurrentNode((currentNode) => currentNode.selectNext(0, 0));
6491
+ } else {
6492
+ this.#selectInLexical(this.topLevelNodeAfterCursor);
6493
+ }
6494
+ }
6495
+
6344
6496
  async #withCurrentNode(fn) {
6345
6497
  await nextFrame();
6346
6498
  if (this.current) {
@@ -6895,6 +7047,30 @@ class Contents {
6895
7047
  });
6896
7048
  }
6897
7049
 
7050
+ createParagraphAfterNode(node, text) {
7051
+ const newParagraph = Pi();
7052
+ node.insertAfter(newParagraph);
7053
+ newParagraph.selectStart();
7054
+
7055
+ // Insert the typed text
7056
+ if (text) {
7057
+ newParagraph.append(Xn(text));
7058
+ newParagraph.select(1, 1); // Place cursor after the text
7059
+ }
7060
+ }
7061
+
7062
+ createParagraphBeforeNode(node, text) {
7063
+ const newParagraph = Pi();
7064
+ node.insertBefore(newParagraph);
7065
+ newParagraph.selectStart();
7066
+
7067
+ // Insert the typed text
7068
+ if (text) {
7069
+ newParagraph.append(Xn(text));
7070
+ newParagraph.select(1, 1); // Place cursor after the text
7071
+ }
7072
+ }
7073
+
6898
7074
  uploadFile(file) {
6899
7075
  if (!this.editorElement.supportsAttachments) {
6900
7076
  console.warn("This editor does not supports attachments (it's configured with [attachments=false])");
@@ -6929,25 +7105,34 @@ class Contents {
6929
7105
  deleteSelectedNodes() {
6930
7106
  this.editor.update(() => {
6931
7107
  if (ur(this.#selection.current)) {
6932
- let nodesWereRemoved = false;
6933
- this.#selection.current.getNodes().forEach((node) => {
7108
+ const nodesToRemove = this.#selection.current.getNodes();
7109
+ if (nodesToRemove.length === 0) return
7110
+
7111
+ // Use splice() instead of node.remove() for proper removal and
7112
+ // reconciliation. Would have issues with removing unintended decorator nodes
7113
+ // with node.remove()
7114
+ nodesToRemove.forEach((node) => {
6934
7115
  const parent = node.getParent();
7116
+ if (!di(parent)) return
6935
7117
 
6936
- node.remove();
7118
+ const children = parent.getChildren();
7119
+ const index = children.indexOf(node);
6937
7120
 
6938
- if (parent.getType() === "root" && parent.getChildrenSize() === 0) {
6939
- parent.append(Pi());
7121
+ if (index >= 0) {
7122
+ parent.splice(index, 1, []);
6940
7123
  }
6941
-
6942
- nodesWereRemoved = true;
6943
7124
  });
6944
7125
 
6945
- if (nodesWereRemoved) {
6946
- this.#selection.clear();
6947
- this.editor.focus();
6948
-
6949
- return true
7126
+ // Check if root is empty after all removals
7127
+ const root = ps();
7128
+ if (root.getChildrenSize() === 0) {
7129
+ root.append(Pi());
6950
7130
  }
7131
+
7132
+ this.#selection.clear();
7133
+ this.editor.focus();
7134
+
7135
+ return true
6951
7136
  }
6952
7137
  });
6953
7138
  }
@@ -7406,6 +7591,9 @@ class Clipboard {
7406
7591
  #handlePastedFiles(clipboardData) {
7407
7592
  if (!this.editorElement.supportsAttachments) return
7408
7593
 
7594
+ const html = clipboardData.getData('text/html');
7595
+ if (html) return // Ignore if image copied from browser since we will load it as a remote image
7596
+
7409
7597
  this.#preservingScrollPosition(() => {
7410
7598
  for (const item of clipboardData.items) {
7411
7599
  const file = item.getAsFile();
@@ -7435,9 +7623,10 @@ class LexicalEditorElement extends HTMLElement {
7435
7623
  static debug = true
7436
7624
  static commands = [ "bold", "italic", "" ]
7437
7625
 
7438
- static observedAttributes = [ "connected" ]
7626
+ static observedAttributes = [ "connected", "required" ]
7439
7627
 
7440
7628
  #initialValue = ""
7629
+ #validationTextArea = document.createElement("textarea")
7441
7630
 
7442
7631
  constructor() {
7443
7632
  super();
@@ -7470,6 +7659,11 @@ class LexicalEditorElement extends HTMLElement {
7470
7659
  if (name === "connected" && this.isConnected && oldValue != null && oldValue !== newValue) {
7471
7660
  requestAnimationFrame(() => this.#reconnect());
7472
7661
  }
7662
+
7663
+ if (name === "required" && this.isConnected) {
7664
+ this.#validationTextArea.required = this.hasAttribute("required");
7665
+ this.#setValidity();
7666
+ }
7473
7667
  }
7474
7668
 
7475
7669
  formResetCallback() {
@@ -7523,7 +7717,7 @@ class LexicalEditorElement extends HTMLElement {
7523
7717
  Ys(Mi);
7524
7718
  const root = ps();
7525
7719
  root.clear();
7526
- root.append(...this.#parseHtmlIntoLexicalNodes(html));
7720
+ if (html !== "") root.append(...this.#parseHtmlIntoLexicalNodes(html));
7527
7721
  root.select();
7528
7722
 
7529
7723
  this.#toggleEmptyStatus();
@@ -7636,6 +7830,7 @@ class LexicalEditorElement extends HTMLElement {
7636
7830
 
7637
7831
  this.internals.setFormValue(html);
7638
7832
  this._internalFormValue = html;
7833
+ this.#validationTextArea.value = this.#isEmpty ? "" : html;
7639
7834
 
7640
7835
  if (changed) {
7641
7836
  dispatch(this, "lexxy:change");
@@ -7664,7 +7859,7 @@ class LexicalEditorElement extends HTMLElement {
7664
7859
  this.cachedValue = null;
7665
7860
  this.#internalFormValue = this.value;
7666
7861
  this.#toggleEmptyStatus();
7667
- this.#validateRequired();
7862
+ this.#setValidity();
7668
7863
  }));
7669
7864
  }
7670
7865
 
@@ -7771,11 +7966,11 @@ class LexicalEditorElement extends HTMLElement {
7771
7966
  return !this.editorContentElement.textContent.trim() && !containsVisuallyRelevantChildren(this.editorContentElement)
7772
7967
  }
7773
7968
 
7774
- #validateRequired() {
7775
- if (this.hasAttribute("required") && this.#isEmpty) {
7776
- this.internals.setValidity({ valueMissing: true }, "Please fill out this field.", this.editorContentElement);
7777
- } else {
7969
+ #setValidity() {
7970
+ if (this.#validationTextArea.validity.valid) {
7778
7971
  this.internals.setValidity({});
7972
+ } else {
7973
+ this.internals.setValidity(this.#validationTextArea.validity, this.#validationTextArea.validationMessage, this.editorContentElement);
7779
7974
  }
7780
7975
  }
7781
7976
 
Binary file
Binary file