lexxy 0.9.14.beta → 0.9.15.alpha.2

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: a6400fe4f84e273a2028af153efe203474e103a0f507d1b72ddd4096ed292e52
4
- data.tar.gz: a2f2ca9843421ea5a9fba94b22da2d95c218e5c96744d7f182ed471efdcb5556
3
+ metadata.gz: 86b63d9156e8846e2eaab83979bf9fc696eb37743e560cbe590f8d91f7448bd7
4
+ data.tar.gz: 375a9d775564689646650622857495a59239d65c2e1b6fd48e861f74abddcd84
5
5
  SHA512:
6
- metadata.gz: 5ca317b2a76ab77e177c30113c9568eea2a035a472ee465c4aa479948e9e74e1ebed7354e61a62438bc66a7d079d80df625038d8d4da815bcc31205a022c1726
7
- data.tar.gz: 9cfe9d96efcbda91237d913f6a07df984390d012a97644534a74aff6175fdabdb6820944e70a4614f747e1975b9726f82548c1ae6dd95b5da33d4a44d0511d75
6
+ metadata.gz: fff91b9c87fcabab97ae5375233a3819f5aa7097c913df32dea9dd914952451f889cb29c98888c1d6173c67ba3ae67732422952afc13812af25c81bec66e1afe
7
+ data.tar.gz: bbde40f03c4a7e950e99d0ac2e018b7e1c8251e0a53246bcffdde5a7141117ed860f9b22f2b75fb242a58ed8dbafe0992e24badb07d167028ac6925f1e03896a
@@ -7803,14 +7803,15 @@ function $isShadowRoot(node) {
7803
7803
  return Wi(node) && Is(node) && !Vi(node)
7804
7804
  }
7805
7805
 
7806
+ function $isSafeForRoot(node) {
7807
+ return (Wi(node) || ji(node)) && !node.isParentRequired()
7808
+ }
7809
+
7806
7810
  function $makeSafeForRoot(node) {
7807
- if (Tr(node)) {
7808
- return Tt$4(node, eo)
7809
- } else if (node.isParentRequired()) {
7810
- const parent = node.createRequiredParent();
7811
- return Tt$4(node, parent)
7812
- } else {
7811
+ if ($isSafeForRoot(node)) {
7813
7812
  return node
7813
+ } else {
7814
+ return Tt$4(node, () => node.createParentElementNode())
7814
7815
  }
7815
7816
  }
7816
7817
 
@@ -7925,6 +7926,39 @@ function $isListItemStructurallyEmpty(listItem) {
7925
7926
  return true
7926
7927
  }
7927
7928
 
7929
+ // Returns the document text up to `offset` inside `targetNode`. Non-inline
7930
+ // element siblings are joined with `\n\n`, matching Lexical's own
7931
+ // ElementNode.getTextContent behavior.
7932
+ function $textBeforeOffset(targetNode, offset) {
7933
+ const parts = [];
7934
+ let done = false;
7935
+
7936
+ function visit(node) {
7937
+ if (done) return
7938
+ if (node === targetNode) {
7939
+ parts.push(node.getTextContent().slice(0, offset));
7940
+ done = true;
7941
+ return
7942
+ }
7943
+ if (Wi(node)) {
7944
+ const children = node.getChildren();
7945
+ for (let i = 0; i < children.length; i++) {
7946
+ visit(children[i]);
7947
+ if (done) return
7948
+ const child = children[i];
7949
+ if (Wi(child) && !child.isInline() && i < children.length - 1) {
7950
+ parts.push("\n\n");
7951
+ }
7952
+ }
7953
+ } else {
7954
+ parts.push(node.getTextContent());
7955
+ }
7956
+ }
7957
+
7958
+ visit(Zo());
7959
+ return parts.join("")
7960
+ }
7961
+
7928
7962
  function isAttachmentSpacerTextNode(node, previousNode, index, childCount) {
7929
7963
  return Tr(node)
7930
7964
  && node.getTextContent() === " "
@@ -8210,6 +8244,11 @@ function safeCloneEditorState(editorState) {
8210
8244
  return clone
8211
8245
  }
8212
8246
 
8247
+ const INITIAL_PREVIEW_POLL_DELAY_MS = 3000;
8248
+ const MAX_PREVIEW_POLL_DELAY_MS = 120000;
8249
+ const MAX_PREVIEW_POLL_ATTEMPTS = 20;
8250
+
8251
+
8213
8252
  class ActionTextAttachmentNode extends Ji {
8214
8253
  static getType() {
8215
8254
  return "action_text_attachment"
@@ -8284,7 +8323,7 @@ class ActionTextAttachmentNode extends Ji {
8284
8323
  return Lexxy.global.get("attachmentTagName")
8285
8324
  }
8286
8325
 
8287
- constructor({ tagName, sgid, src, previewSrc, previewable, pendingPreview, altText, caption, contentType, fileName, fileSize, width, height, uploadError }, key) {
8326
+ constructor({ tagName, sgid, src, previewSrc, previewable, previewStatusUrl, pendingPreview, altText, caption, contentType, fileName, fileSize, width, height, uploadError }, key) {
8288
8327
  super(key);
8289
8328
 
8290
8329
  this.tagName = tagName || ActionTextAttachmentNode.TAG_NAME;
@@ -8292,6 +8331,7 @@ class ActionTextAttachmentNode extends Ji {
8292
8331
  this.src = src;
8293
8332
  this.previewSrc = previewSrc;
8294
8333
  this.previewable = parseBoolean(previewable);
8334
+ this.previewStatusUrl = previewStatusUrl;
8295
8335
  this.pendingPreview = pendingPreview;
8296
8336
  this.altText = altText || "";
8297
8337
  this.caption = caption || "";
@@ -8370,6 +8410,8 @@ class ActionTextAttachmentNode extends Ji {
8370
8410
  sgid: this.sgid,
8371
8411
  src: this.src,
8372
8412
  previewable: this.previewable,
8413
+ previewStatusUrl: this.previewStatusUrl,
8414
+ pendingPreview: this.pendingPreview,
8373
8415
  altText: this.altText,
8374
8416
  caption: this.caption,
8375
8417
  contentType: this.contentType,
@@ -8488,41 +8530,61 @@ class ActionTextAttachmentNode extends Ji {
8488
8530
  });
8489
8531
  }
8490
8532
 
8533
+ // While the file-icon is shown, watch for the preview to become ready.
8534
+ // With a status URL, poll it (2xx = processing, anything else = ready).
8535
+ // Without one, preload the preview URL once and swap on load.
8491
8536
  #pollForPreview(figure) {
8537
+ if (this.previewStatusUrl) {
8538
+ this.#waitForPreviewByPollingStatus(figure);
8539
+ } else {
8540
+ this.#waitForPreviewByPreloadingImage(figure);
8541
+ }
8542
+ }
8543
+
8544
+ #waitForPreviewByPollingStatus(figure) {
8492
8545
  let attempt = 0;
8493
- const maxAttempts = 10;
8494
8546
 
8495
- const tryLoad = () => {
8547
+ const tryStatus = async () => {
8496
8548
  if (!this.editor.read(() => this.isAttached())) return
8497
8549
 
8498
- const img = new Image();
8499
- const cacheBustedSrc = `${this.src}${this.src.includes("?") ? "&" : "?"}_=${Date.now()}`;
8550
+ try {
8551
+ // redirect: "manual" prevents fetch from transparently following a
8552
+ // 3xx response — without it, a status endpoint that redirected to,
8553
+ // say, the preview URL would resolve to a 200 and look like
8554
+ // "still processing." The contract is "any non-2xx means done."
8555
+ const response = await fetch(this.previewStatusUrl, { credentials: "include", redirect: "manual" });
8500
8556
 
8501
- img.onload = () => {
8502
8557
  if (!this.editor.read(() => this.isAttached())) return
8503
8558
 
8504
- // The placeholder is a file-type icon SVG (86×100). A real thumbnail
8505
- // generated from PDF/video content is significantly larger.
8506
- if (img.naturalWidth > 150 && img.naturalHeight > 150) {
8507
- this.#swapToPreviewDOM(figure, cacheBustedSrc);
8508
- } else {
8559
+ if (response.ok) {
8509
8560
  retry();
8561
+ } else {
8562
+ this.#swapToPreviewDOM(figure, this.src);
8510
8563
  }
8511
- };
8512
- img.onerror = () => retry();
8513
- img.src = cacheBustedSrc;
8564
+ } catch {
8565
+ retry();
8566
+ }
8514
8567
  };
8515
8568
 
8516
8569
  const retry = () => {
8517
8570
  attempt++;
8518
- if (attempt < maxAttempts && this.editor.read(() => this.isAttached())) {
8519
- const delay = Math.min(2000 * Math.pow(1.5, attempt), 15000);
8520
- setTimeout(tryLoad, delay);
8571
+ if (attempt < MAX_PREVIEW_POLL_ATTEMPTS && this.editor.read(() => this.isAttached())) {
8572
+ const delay = Math.min(2000 * Math.pow(1.5, attempt), MAX_PREVIEW_POLL_DELAY_MS);
8573
+ setTimeout(tryStatus, delay);
8521
8574
  }
8522
8575
  };
8523
8576
 
8524
8577
  // Give the server time to start processing before the first attempt
8525
- setTimeout(tryLoad, 3000);
8578
+ setTimeout(tryStatus, INITIAL_PREVIEW_POLL_DELAY_MS);
8579
+ }
8580
+
8581
+ #waitForPreviewByPreloadingImage(figure) {
8582
+ const img = new Image();
8583
+ img.onload = () => {
8584
+ if (!this.editor.read(() => this.isAttached())) return
8585
+ this.#swapToPreviewDOM(figure, this.src);
8586
+ };
8587
+ img.src = this.src;
8526
8588
  }
8527
8589
 
8528
8590
  #swapToPreviewDOM(figure, previewSrc) {
@@ -11151,7 +11213,7 @@ class ImageGalleryNode extends Bi {
11151
11213
  replaceWithSingularChild() {
11152
11214
  if (this.#hasSingularChild) {
11153
11215
  const child = this.getFirstChild();
11154
- return this.replace(child)
11216
+ return this.replace($makeSafeForRoot(child))
11155
11217
  }
11156
11218
  }
11157
11219
 
@@ -11579,6 +11641,7 @@ class AttachmentNodeConversion {
11579
11641
  fileName: blob.filename,
11580
11642
  fileSize: blob.byte_size,
11581
11643
  previewable: blob.previewable,
11644
+ previewStatusUrl: blob.preview_status_url
11582
11645
  }
11583
11646
  }
11584
11647
 
@@ -14950,6 +15013,9 @@ class RemoteFilterSource extends BaseSource {
14950
15013
  const NOTHING_FOUND_DEFAULT_MESSAGE = "Nothing found";
14951
15014
  const FILTER_DEBOUNCE_INTERVAL = 50;
14952
15015
 
15016
+ // Start of line, or after a space or newline.
15017
+ const DEFAULT_ONLY_AT_PATTERN = "^|[ \\n]";
15018
+
14953
15019
  class LexicalPromptElement extends HTMLElement {
14954
15020
  #globalListeners = new ListenerBin()
14955
15021
  #popoverListeners = new ListenerBin()
@@ -14995,6 +15061,10 @@ class LexicalPromptElement extends HTMLElement {
14995
15061
  return this.hasAttribute("supports-space-in-searches")
14996
15062
  }
14997
15063
 
15064
+ get onlyAt() {
15065
+ return this.getAttribute("only-at")
15066
+ }
15067
+
14998
15068
  get open() {
14999
15069
  return this.popoverElement?.classList?.contains("lexxy-prompt-menu--visible")
15000
15070
  }
@@ -15038,14 +15108,10 @@ class LexicalPromptElement extends HTMLElement {
15038
15108
  if (offset >= triggerLength) {
15039
15109
  const textBeforeCursor = fullText.slice(offset - triggerLength, offset);
15040
15110
 
15041
- // Check if trigger is at the start of the text node (new line case) or preceded by space or newline
15042
15111
  if (textBeforeCursor === this.trigger) {
15043
- const isAtStart = offset === triggerLength;
15112
+ const textBeforeTrigger = $textBeforeOffset(node, offset - triggerLength);
15044
15113
 
15045
- const charBeforeTrigger = offset > triggerLength ? fullText[offset - triggerLength - 1] : null;
15046
- const isPrecededBySpaceOrNewline = charBeforeTrigger === " " || charBeforeTrigger === "\n";
15047
-
15048
- if (isAtStart || isPrecededBySpaceOrNewline) {
15114
+ if (this.#onlyAtRegExp.test(textBeforeTrigger)) {
15049
15115
  this.#popoverListeners.dispose();
15050
15116
  this.#showPopover();
15051
15117
  }
@@ -15056,7 +15122,15 @@ class LexicalPromptElement extends HTMLElement {
15056
15122
  }));
15057
15123
  }
15058
15124
 
15125
+ get #onlyAtRegExp() {
15126
+ return new RegExp(`(?:${this.onlyAt ?? DEFAULT_ONLY_AT_PATTERN})$`)
15127
+ }
15128
+
15059
15129
  get #promptContentTypePermitted() {
15130
+ // `insert-editable-text` prompts never create attachments, so the
15131
+ // editor's attachment support and content-type allowlist don't apply.
15132
+ if (this.hasAttribute("insert-editable-text")) return true
15133
+
15060
15134
  const el = this.#editorElement;
15061
15135
  if (!el.supportsAttachments) {
15062
15136
  return false
@@ -15219,7 +15293,7 @@ class LexicalPromptElement extends HTMLElement {
15219
15293
 
15220
15294
  const popoverRect = this.popoverElement.getBoundingClientRect();
15221
15295
 
15222
- if (popoverRect.right > window.innerWidth) {
15296
+ if (popoverRect.right > editorRect.right) {
15223
15297
  this.popoverElement.toggleAttribute("data-clipped-at-right", true);
15224
15298
  }
15225
15299
 
Binary file
Binary file