lexxy 0.9.19.alpha.2 → 0.9.19.alpha.3

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: 3f4e64b061d290f12430a666006c5845f242f1776474b8b8691c53e83a7a5674
4
- data.tar.gz: 8f539bac381f8273f97d18c9d40093c917272d89e74a36ef3d8dee9888ab149d
3
+ metadata.gz: bf2150a951764595691835dfe6bb97aa8a0bf4be41600e611f94156646aaba51
4
+ data.tar.gz: f24a3dc76fec3b0d924b3f9cd2839c1faa747ff9a051e5c57075530c6ca0abbd
5
5
  SHA512:
6
- metadata.gz: 2245d2ea20bae119bda7464502ef6c83390f08425ea998e9e2e4a84b4ac20d389fc967ae4d4aab1d821d462050c3358cf617d3170cf11a4fcad526d368d59453
7
- data.tar.gz: db3cef1b72fcc42d00f66160292ce2f13b2c1b82a57bbe44fa53a7100a31e65143a52c68674ada67d20901be052c26a037516b5a13b72994673e1391d8c24883
6
+ metadata.gz: 9e1e04f1e9bd1f32513f1c64efbd69ca49519307906851d6c11958232a3a0307a0dd31f512007df583bee752c52e97dac2fa87be8bc13dcd2d1d7ef57aa0dc06
7
+ data.tar.gz: 231f6d4ad4a687800ac5ed43941af7cdea2386549cbc26c3df538db628a6847406013065f0c452c0bc2acfa093afa759a7007cce0639ecbdd9953ecd727da77f
@@ -6301,6 +6301,14 @@ class ListenerBin {
6301
6301
  }
6302
6302
  }
6303
6303
 
6304
+ function handlingDefault(handler) {
6305
+ return event => {
6306
+ const handled = handler(event);
6307
+ if (handled) event.preventDefault();
6308
+ return handled
6309
+ }
6310
+ }
6311
+
6304
6312
  function createElement(name, properties, content = "") {
6305
6313
  const element = document.createElement(name);
6306
6314
  for (const [ key, value ] of Object.entries(properties || {})) {
@@ -9189,6 +9197,30 @@ var theme = {
9189
9197
  }
9190
9198
  };
9191
9199
 
9200
+ class UploadRequests {
9201
+ #requestsByKey = new Map()
9202
+
9203
+ track(key, request) {
9204
+ this.#requestsByKey.set(key, request);
9205
+ }
9206
+
9207
+ forget(key) {
9208
+ this.#requestsByKey.delete(key);
9209
+ }
9210
+
9211
+ abort(key) {
9212
+ const request = this.#requestsByKey.get(key);
9213
+ if (request) {
9214
+ this.#requestsByKey.delete(key);
9215
+ request.abort();
9216
+ }
9217
+ }
9218
+
9219
+ clear() {
9220
+ this.#requestsByKey.clear();
9221
+ }
9222
+ }
9223
+
9192
9224
  /**
9193
9225
  * Copyright (c) Meta Platforms, Inc. and affiliates.
9194
9226
  *
@@ -10574,10 +10606,10 @@ class Selection {
10574
10606
 
10575
10607
  #processSelectionChangeCommands() {
10576
10608
  this.#listeners.track(
10577
- this.editor.registerCommand(Ne$1, this.#selectPreviousNode.bind(this), io),
10578
- this.editor.registerCommand(ke$2, this.#selectNextNode.bind(this), io),
10579
- this.editor.registerCommand(we$1, this.#selectPreviousTopLevelNode.bind(this), io),
10580
- this.editor.registerCommand(Ee$3, this.#selectNextTopLevelNode.bind(this), io),
10609
+ this.editor.registerCommand(Ne$1, handlingDefault(this.#selectPreviousNode.bind(this)), io),
10610
+ this.editor.registerCommand(ke$2, handlingDefault(this.#selectNextNode.bind(this)), io),
10611
+ this.editor.registerCommand(we$1, handlingDefault(this.#selectPreviousTopLevelNode.bind(this)), io),
10612
+ this.editor.registerCommand(Ee$3, handlingDefault(this.#selectNextTopLevelNode.bind(this)), io),
10581
10613
 
10582
10614
  this.editor.registerCommand(fe$4, this.#selectDecoratorNodeBeforeDeletion.bind(this), io),
10583
10615
 
@@ -10676,49 +10708,58 @@ class Selection {
10676
10708
  }
10677
10709
  }
10678
10710
 
10679
- async #selectPreviousNode(event) {
10680
- if (event?.shiftKey) return false
10711
+ #selectPreviousNode(event) {
10712
+ if (event.shiftKey) {
10713
+ return this.#withCurrentNodeSelectionNode((currentNode) => {
10714
+ const selection = this.#rangeSelectDecorator(currentNode, "forward");
10681
10715
 
10682
- if (this.hasNodeSelection) {
10683
- return await this.#withCurrentNode((currentNode) => currentNode.selectPrevious())
10716
+ // Can't rely on native pass-through with Playwright on firefox
10717
+ selection.modify("extend", true, "character");
10718
+ return true
10719
+ })
10684
10720
  } else {
10685
- return this.#selectInLexical(this.nodeBeforeCursor)
10721
+ return this.#withCurrentNodeSelectionNode((currentNode) => currentNode.selectPrevious())
10722
+ || this.#selectInLexical(this.nodeBeforeCursor)
10686
10723
  }
10687
10724
  }
10688
10725
 
10689
- async #selectNextNode(event) {
10690
- if (event?.shiftKey) return false
10726
+ #selectNextNode(event) {
10727
+ if (event.shiftKey) {
10728
+ return this.#withCurrentNodeSelectionNode((currentNode) => {
10729
+ const selection = this.#rangeSelectDecorator(currentNode, "forward");
10691
10730
 
10692
- if (this.hasNodeSelection) {
10693
- return await this.#withCurrentNode((currentNode) => currentNode.selectNext(0, 0))
10731
+ // Can't rely on native pass-through with Playwright on firefox
10732
+ selection.modify("extend", false, "character");
10733
+ return true
10734
+ })
10694
10735
  } else {
10695
- return this.#selectInLexical(this.nodeAfterCursor)
10736
+ return this.#withCurrentNodeSelectionNode((currentNode) => currentNode.selectNext(0, 0))
10737
+ || this.#selectInLexical(this.nodeAfterCursor)
10696
10738
  }
10697
10739
  }
10698
10740
 
10699
- async #selectPreviousTopLevelNode() {
10700
- if (this.hasNodeSelection) {
10701
- return await this.#withCurrentNode((currentNode) => currentNode.getTopLevelElement().selectPrevious())
10702
- } else {
10703
- return this.#selectInLexical(this.topLevelNodeBeforeCursor)
10704
- }
10741
+ #selectPreviousTopLevelNode() {
10742
+ return this.#withCurrentNodeSelectionNode((currentNode) => currentNode.getTopLevelElement().selectPrevious())
10743
+ || this.#selectInLexical(this.topLevelNodeBeforeCursor)
10744
+ }
10745
+
10746
+ #selectNextTopLevelNode() {
10747
+ return this.#withCurrentNodeSelectionNode((currentNode) => currentNode.getTopLevelElement().selectNext(0, 0))
10748
+ || this.#selectInLexical(this.topLevelNodeAfterCursor)
10705
10749
  }
10706
10750
 
10707
- async #selectNextTopLevelNode() {
10751
+ #withCurrentNodeSelectionNode(fn) {
10708
10752
  if (this.hasNodeSelection) {
10709
- return await this.#withCurrentNode((currentNode) => currentNode.getTopLevelElement().selectNext(0, 0))
10710
- } else {
10711
- return this.#selectInLexical(this.topLevelNodeAfterCursor)
10753
+ return fn(Qr().getNodes()[0])
10712
10754
  }
10713
10755
  }
10714
10756
 
10715
- async #withCurrentNode(fn) {
10716
- await nextFrame();
10717
- if (this.hasNodeSelection) {
10718
- this.editor.update(() => {
10719
- fn(Qr().getNodes()[0]);
10720
- this.editor.focus();
10721
- });
10757
+ #rangeSelectDecorator(node, direction = "forward") {
10758
+ if (ji(node)) {
10759
+ const [ anchorOffset, focusOffset ] = direction === "forward" ? [ 0, 1 ] : [ 1, 0 ];
10760
+ const indexAtNode = node.getIndexWithinParent();
10761
+
10762
+ return node.getParent().select(indexAtNode + anchorOffset, indexAtNode + focusOffset)
10722
10763
  }
10723
10764
  }
10724
10765
 
@@ -11340,9 +11381,9 @@ function $findOrCreateGalleryForImage(node) {
11340
11381
  class Uploader {
11341
11382
  #files
11342
11383
 
11343
- static for(editorElement, files) {
11384
+ static for(editorElement, files, options = {}) {
11344
11385
  const UploaderKlass = GalleryUploader.handle(editorElement, files) ? GalleryUploader : Uploader;
11345
- return new UploaderKlass(editorElement, files)
11386
+ return new UploaderKlass(editorElement, files, options)
11346
11387
  }
11347
11388
 
11348
11389
  constructor(editorElement, files, options = {}) {
@@ -11364,7 +11405,13 @@ class Uploader {
11364
11405
  }
11365
11406
 
11366
11407
  $createUploadNodes() {
11367
- this.nodes = this.files.map(file => this.contents.$createUploadNode(file));
11408
+ this.nodes = this.files.map(file => this.#createUploadNode(file));
11409
+ }
11410
+
11411
+ #createUploadNode(file) {
11412
+ return this.options.pending
11413
+ ? this.contents.$createPendingUploadNode(file)
11414
+ : this.contents.$createUploadNode(file)
11368
11415
  }
11369
11416
 
11370
11417
  $insertUploadNodes() {
@@ -11622,6 +11669,8 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
11622
11669
  this.#dispatchEvent("lexxy:upload-start", { file: this.file });
11623
11670
 
11624
11671
  upload.create((error, blob) => {
11672
+ this.#forgetUploadRequest();
11673
+
11625
11674
  if (error) {
11626
11675
  this.#dispatchEvent("lexxy:upload-end", { file: this.file, error });
11627
11676
  this.#handleUploadError(error);
@@ -11644,12 +11693,26 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
11644
11693
  directUploadWillStoreFileWithXHR: (request) => {
11645
11694
  if (shouldAuthenticateUploads) request.withCredentials = true;
11646
11695
 
11696
+ this.#rememberUploadRequest(request);
11697
+
11647
11698
  const uploadProgressHandler = (event) => this.#handleUploadProgress(event, request);
11648
11699
  request.upload.addEventListener("progress", uploadProgressHandler);
11649
11700
  }
11650
11701
  }
11651
11702
  }
11652
11703
 
11704
+ #forgetUploadRequest() {
11705
+ this.#editorElement.uploadRequests.forget(this.getKey());
11706
+ }
11707
+
11708
+ #rememberUploadRequest(request) {
11709
+ this.#editorElement.uploadRequests.track(this.getKey(), request);
11710
+ }
11711
+
11712
+ get #editorElement() {
11713
+ return this.editor.getRootElement()?.closest("lexxy-editor")
11714
+ }
11715
+
11653
11716
  #setUploadStarted() {
11654
11717
  this.#setProgress(1);
11655
11718
  }
@@ -12027,11 +12090,6 @@ class Contents {
12027
12090
  inserter.insertNodes(nodes);
12028
12091
  }
12029
12092
 
12030
- insertAtCursorEnsuringLineBelow(node) {
12031
- this.insertAtCursor(node);
12032
- this.#insertLineBelowIfLastNode(node);
12033
- }
12034
-
12035
12093
  applyParagraphFormat() {
12036
12094
  const selection = Qr();
12037
12095
  if (!Fr(selection)) return
@@ -12173,17 +12231,27 @@ class Contents {
12173
12231
  const fullText = anchorNode.getTextContent();
12174
12232
  const offset = anchor.offset;
12175
12233
 
12176
- const textBeforeCursor = fullText.slice(0, offset);
12177
-
12178
- const lastIndex = textBeforeCursor.lastIndexOf(string);
12234
+ const lastIndex = fullText.slice(0, offset).lastIndexOf(string);
12179
12235
  if (lastIndex !== -1) {
12180
- result = textBeforeCursor.slice(lastIndex + string.length);
12236
+ result = fullText.slice(lastIndex + string.length, this.#endOffsetAt(fullText, offset));
12181
12237
  }
12182
12238
  });
12183
12239
 
12184
12240
  return result
12185
12241
  }
12186
12242
 
12243
+ // The query runs from the trigger up to the next whitespace, even when the
12244
+ // cursor sits inside an existing word — inserting "@" before "Jack" must
12245
+ // filter by "Jack" rather than treating the prompt as empty.
12246
+ #endOffsetAt(fullText, cursorOffset) {
12247
+ const whitespaceOffset = fullText.slice(cursorOffset).search(/\s/);
12248
+ if (whitespaceOffset === -1) {
12249
+ return fullText.length
12250
+ } else {
12251
+ return cursorOffset + whitespaceOffset
12252
+ }
12253
+ }
12254
+
12187
12255
  containsTextBackUntil(string) {
12188
12256
  let result = false;
12189
12257
 
@@ -12214,10 +12282,10 @@ class Contents {
12214
12282
  const { anchorNode, offset } = this.#getTextAnchorData();
12215
12283
  if (!anchorNode) return
12216
12284
 
12217
- const lastIndex = this.#findLastIndexBeforeCursor(anchorNode, offset, stringToReplace);
12285
+ const lastIndex = this.#findReplacementStart(anchorNode, offset, stringToReplace);
12218
12286
  if (lastIndex === -1) return
12219
12287
 
12220
- this.#performTextReplacement(anchorNode, selection, offset, lastIndex, replacementNodes);
12288
+ this.#performTextReplacement(anchorNode, selection, lastIndex, stringToReplace, replacementNodes);
12221
12289
  }
12222
12290
 
12223
12291
  uploadFiles(files, { selectLast } = {}) {
@@ -12250,24 +12318,46 @@ class Contents {
12250
12318
  })
12251
12319
  }
12252
12320
 
12321
+ $createPendingUploadNode(file) {
12322
+ return $createActionTextAttachmentUploadNode({
12323
+ file,
12324
+ uploadUrl: null,
12325
+ blobUrlTemplate: this.editorElement.blobUrlTemplate,
12326
+ contentType: file.type,
12327
+ })
12328
+ }
12329
+
12253
12330
  insertPendingAttachment(file) {
12254
12331
  if (!this.editorElement.supportsAttachments) return null
12255
12332
 
12256
12333
  let nodeKey = null;
12257
12334
  this.editor.update(() => {
12258
- const uploadNode = new ActionTextAttachmentUploadNode({
12259
- file,
12260
- uploadUrl: null,
12261
- blobUrlTemplate: this.editorElement.blobUrlTemplate,
12262
- editor: this.editor
12263
- });
12335
+ const uploadNode = this.$createPendingUploadNode(file);
12264
12336
  this.insertAtCursor(uploadNode);
12265
12337
  nodeKey = uploadNode.getKey();
12266
- }, { tag: Jn });
12338
+ });
12339
+
12340
+ return nodeKey ? this.#pendingAttachmentHandle(nodeKey) : null
12341
+ }
12267
12342
 
12268
- if (!nodeKey) return null
12343
+ insertPendingAttachments(files) {
12344
+ const fileList = Array.from(files);
12345
+ if (!this.editorElement.supportsAttachments || fileList.length === 0) return []
12269
12346
 
12347
+ let nodeKeys = [];
12348
+ this.editor.update(() => {
12349
+ const uploader = Uploader.for(this.editorElement, fileList, { pending: true });
12350
+ uploader.$uploadFiles();
12351
+ nodeKeys = (uploader.nodes ?? []).map(node => node.getKey());
12352
+ });
12353
+
12354
+ return nodeKeys.map(nodeKey => this.#pendingAttachmentHandle(nodeKey))
12355
+ }
12356
+
12357
+ #pendingAttachmentHandle(initialNodeKey) {
12270
12358
  const editor = this.editor;
12359
+ let nodeKey = initialNodeKey;
12360
+
12271
12361
  return {
12272
12362
  setAttributes(blob) {
12273
12363
  editor.update(() => {
@@ -12472,17 +12562,6 @@ class Contents {
12472
12562
  }
12473
12563
  }
12474
12564
 
12475
- #insertLineBelowIfLastNode(node) {
12476
- this.editor.update(() => {
12477
- const nextSibling = node.getNextSibling();
12478
- if (!nextSibling) {
12479
- const newParagraph = eo();
12480
- node.insertAfter(newParagraph);
12481
- newParagraph.selectStart();
12482
- }
12483
- });
12484
- }
12485
-
12486
12565
  #unwrap(node) {
12487
12566
  const children = node.getChildren();
12488
12567
 
@@ -12515,19 +12594,27 @@ class Contents {
12515
12594
  return { anchorNode, offset: anchor.offset }
12516
12595
  }
12517
12596
 
12518
- #findLastIndexBeforeCursor(anchorNode, offset, stringToReplace) {
12597
+ // The replaced span can straddle the cursor (e.g. "@Jack" when "@" was just
12598
+ // inserted before "Jack"), so we anchor on the trigger before the cursor and
12599
+ // verify the whole string matches there rather than searching text up to it.
12600
+ #findReplacementStart(anchorNode, offset, stringToReplace) {
12519
12601
  const fullText = anchorNode.getTextContent();
12520
- const textBeforeCursor = fullText.slice(0, offset);
12521
- return textBeforeCursor.lastIndexOf(stringToReplace)
12602
+ const triggerIndex = fullText.slice(0, offset).lastIndexOf(stringToReplace[0]);
12603
+
12604
+ if (triggerIndex !== -1 && fullText.startsWith(stringToReplace, triggerIndex)) {
12605
+ return triggerIndex
12606
+ } else {
12607
+ return -1
12608
+ }
12522
12609
  }
12523
12610
 
12524
- #performTextReplacement(anchorNode, selection, offset, lastIndex, replacementNodes) {
12611
+ #performTextReplacement(anchorNode, selection, startIndex, stringToReplace, replacementNodes) {
12525
12612
  const fullText = anchorNode.getTextContent();
12526
- const textBeforeString = fullText.slice(0, lastIndex);
12527
- const textAfterCursor = fullText.slice(offset);
12613
+ const textBeforeString = fullText.slice(0, startIndex);
12614
+ const textAfterString = fullText.slice(startIndex + stringToReplace.length);
12528
12615
 
12529
12616
  const textNodeBefore = this.#cloneTextNodeFormatting(anchorNode, selection, textBeforeString);
12530
- const textNodeAfter = this.#cloneTextNodeFormatting(anchorNode, selection, textAfterCursor || " ");
12617
+ const textNodeAfter = this.#cloneTextNodeFormatting(anchorNode, selection, textAfterString || " ");
12531
12618
 
12532
12619
  anchorNode.replace(textNodeBefore);
12533
12620
 
@@ -12535,7 +12622,7 @@ class Contents {
12535
12622
  lastInsertedNode.insertAfter(textNodeAfter);
12536
12623
 
12537
12624
  this.#appendLineBreakIfNeeded(textNodeAfter.getParentOrThrow());
12538
- const cursorOffset = textAfterCursor ? 0 : 1;
12625
+ const cursorOffset = textAfterString ? 0 : 1;
12539
12626
  textNodeAfter.select(cursorOffset, cursorOffset);
12540
12627
  }
12541
12628
 
@@ -13851,11 +13938,12 @@ class AttachmentsExtension extends LexxyExtension {
13851
13938
 
13852
13939
  #handleUploadMutations(mutations) {
13853
13940
  const previousUploadsCount = this.#uploadsCount;
13854
- for (const [ , mutation ] of mutations) {
13941
+ for (const [ key, mutation ] of mutations) {
13855
13942
  if (mutation === "created") {
13856
13943
  this.#uploadsCount++;
13857
13944
  } else if (mutation === "destroyed") {
13858
13945
  this.#uploadsCount--;
13946
+ this.editorElement.uploadRequests.abort(key);
13859
13947
  }
13860
13948
  }
13861
13949
 
@@ -14299,6 +14387,7 @@ class LexicalEditorElement extends HTMLElement {
14299
14387
 
14300
14388
  #validity = new Map()
14301
14389
  #validationTextArea = document.createElement("textarea")
14390
+ #uploadRequests
14302
14391
 
14303
14392
  constructor() {
14304
14393
  super();
@@ -14306,6 +14395,10 @@ class LexicalEditorElement extends HTMLElement {
14306
14395
  this.internals.role = "presentation";
14307
14396
  }
14308
14397
 
14398
+ get uploadRequests() {
14399
+ return this.#uploadRequests
14400
+ }
14401
+
14309
14402
  connectedCallback() {
14310
14403
  this.id ||= generateDomId("lexxy-editor");
14311
14404
  this.config = new EditorConfiguration(this);
@@ -14326,6 +14419,7 @@ class LexicalEditorElement extends HTMLElement {
14326
14419
  this.#disposables.push(this.clipboard);
14327
14420
 
14328
14421
  this.adapter = new BrowserAdapter();
14422
+ this.#uploadRequests = new UploadRequests();
14329
14423
 
14330
14424
  const commandDispatcher = CommandDispatcher.configureFor(this);
14331
14425
  this.#disposables.push(commandDispatcher);
@@ -15099,6 +15193,7 @@ class LexicalEditorElement extends HTMLElement {
15099
15193
  #reset() {
15100
15194
  this.#dispose();
15101
15195
  this.#resetValidity();
15196
+ this.#uploadRequests?.clear();
15102
15197
  this.editorContentElement?.remove();
15103
15198
  this.editorContentElement = null;
15104
15199
 
@@ -15465,24 +15560,33 @@ class LexicalPromptElement extends HTMLElement {
15465
15560
  const { node, offset } = this.#selection.selectedNodeWithOffset();
15466
15561
  if (!node) return
15467
15562
 
15468
- if (Tr(node) && offset > 0) {
15469
- const fullText = node.getTextContent();
15470
- const textBeforeCursor = fullText.slice(0, offset);
15471
- const lastTriggerIndex = textBeforeCursor.lastIndexOf(this.trigger);
15472
- const triggerEndIndex = lastTriggerIndex + this.trigger.length - 1;
15473
-
15474
- // If trigger is not found, or cursor is at or before the trigger end position, hide popover
15475
- if (lastTriggerIndex === -1 || offset <= triggerEndIndex) {
15476
- this.#hidePopover();
15477
- }
15563
+ if (this.#cursorIsTypingSearchTerm(node, offset)) {
15564
+ return
15478
15565
  } else {
15479
- // Cursor is not in a text node or at offset 0, hide popover
15480
15566
  this.#hidePopover();
15481
15567
  }
15482
15568
  });
15483
15569
  }));
15484
15570
  }
15485
15571
 
15572
+ // The popover should stay open only while the cursor sits at the end of the
15573
+ // trigger and its search term. When the cursor moves away — before the
15574
+ // trigger, or past the token into later text — the text between the trigger
15575
+ // and the cursor breaks that run and we dismiss. A newline always breaks the
15576
+ // run; a space breaks it only for triggers that don't support spaces in
15577
+ // searches, since those that do (e.g. `person:`) expect multi-word terms.
15578
+ #cursorIsTypingSearchTerm(node, offset) {
15579
+ if (!Tr(node) || offset === 0) return false
15580
+
15581
+ const textBeforeCursor = node.getTextContent().slice(0, offset);
15582
+ const lastTriggerIndex = textBeforeCursor.lastIndexOf(this.trigger);
15583
+ if (lastTriggerIndex === -1) return false
15584
+
15585
+ const searchTerm = textBeforeCursor.slice(lastTriggerIndex + this.trigger.length);
15586
+ const breakPattern = this.supportsSpaceInSearches ? /\n/ : /[ \n]/;
15587
+ return !breakPattern.test(searchTerm)
15588
+ }
15589
+
15486
15590
  get #editor() {
15487
15591
  return this.#editorElement.editor
15488
15592
  }
Binary file
Binary file