@37signals/lexxy 0.9.15-alpha.1 → 0.9.15-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.
Files changed (2) hide show
  1. package/dist/lexxy.esm.js +67 -24
  2. package/package.json +1 -1
package/dist/lexxy.esm.js CHANGED
@@ -5,7 +5,7 @@ import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_RED
5
5
  import { LinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, $isLinkNode, AutoLinkNode } from '@lexical/link';
6
6
  import { buildEditorFromExtensions } from '@lexical/extension';
7
7
  import { ListNode, ListItemNode, $getListDepth, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, $isListItemNode, $isListNode, registerList } from '@lexical/list';
8
- import { $getNearestNodeOfType, $wrapNodeInElement, $lastToFirstIterator, $descendantsMatching, mergeRegister, $insertFirst, $unwrapAndFilterDescendants, $firstToLastIterator, $getNearestBlockElementAncestorOrThrow, IS_APPLE } from '@lexical/utils';
8
+ import { $getNearestNodeOfType, $wrapNodeInElement, $lastToFirstIterator, $descendantsMatching, mergeRegister, $insertNodeToNearestRoot, $insertFirst, $unwrapAndFilterDescendants, $firstToLastIterator, $getNearestBlockElementAncestorOrThrow, IS_APPLE } from '@lexical/utils';
9
9
  import { registerPlainText } from '@lexical/plain-text';
10
10
  import { RichTextExtension, $isQuoteNode, $isHeadingNode, $createHeadingNode, $createQuoteNode, QuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
11
11
  import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
@@ -1968,6 +1968,11 @@ function safeCloneEditorState(editorState) {
1968
1968
  return clone
1969
1969
  }
1970
1970
 
1971
+ const INITIAL_PREVIEW_POLL_DELAY_MS = 3000;
1972
+ const MAX_PREVIEW_POLL_DELAY_MS = 120000;
1973
+ const MAX_PREVIEW_POLL_ATTEMPTS = 20;
1974
+
1975
+
1971
1976
  class ActionTextAttachmentNode extends DecoratorNode {
1972
1977
  static getType() {
1973
1978
  return "action_text_attachment"
@@ -2042,7 +2047,7 @@ class ActionTextAttachmentNode extends DecoratorNode {
2042
2047
  return Lexxy.global.get("attachmentTagName")
2043
2048
  }
2044
2049
 
2045
- constructor({ tagName, sgid, src, previewSrc, previewable, pendingPreview, altText, caption, contentType, fileName, fileSize, width, height, uploadError }, key) {
2050
+ constructor({ tagName, sgid, src, previewSrc, previewable, previewStatusUrl, pendingPreview, altText, caption, contentType, fileName, fileSize, width, height, uploadError }, key) {
2046
2051
  super(key);
2047
2052
 
2048
2053
  this.tagName = tagName || ActionTextAttachmentNode.TAG_NAME;
@@ -2050,6 +2055,7 @@ class ActionTextAttachmentNode extends DecoratorNode {
2050
2055
  this.src = src;
2051
2056
  this.previewSrc = previewSrc;
2052
2057
  this.previewable = parseBoolean(previewable);
2058
+ this.previewStatusUrl = previewStatusUrl;
2053
2059
  this.pendingPreview = pendingPreview;
2054
2060
  this.altText = altText || "";
2055
2061
  this.caption = caption || "";
@@ -2128,6 +2134,8 @@ class ActionTextAttachmentNode extends DecoratorNode {
2128
2134
  sgid: this.sgid,
2129
2135
  src: this.src,
2130
2136
  previewable: this.previewable,
2137
+ previewStatusUrl: this.previewStatusUrl,
2138
+ pendingPreview: this.pendingPreview,
2131
2139
  altText: this.altText,
2132
2140
  caption: this.caption,
2133
2141
  contentType: this.contentType,
@@ -2246,41 +2254,68 @@ class ActionTextAttachmentNode extends DecoratorNode {
2246
2254
  });
2247
2255
  }
2248
2256
 
2257
+ // While the file-icon is shown, watch for the preview to become ready.
2258
+ // With a status URL, poll it (2xx = processing, anything else = ready).
2259
+ // Without one, preload the preview URL once and swap on load.
2249
2260
  #pollForPreview(figure) {
2261
+ if (this.previewStatusUrl) {
2262
+ this.#waitForPreviewByPollingStatus(figure);
2263
+ } else {
2264
+ this.#waitForPreviewByPreloadingImage(figure);
2265
+ }
2266
+ }
2267
+
2268
+ #waitForPreviewByPollingStatus(figure) {
2250
2269
  let attempt = 0;
2251
- const maxAttempts = 10;
2252
2270
 
2253
- const tryLoad = () => {
2271
+ const tryStatus = async () => {
2254
2272
  if (!this.editor.read(() => this.isAttached())) return
2255
2273
 
2256
- const img = new Image();
2257
- const cacheBustedSrc = `${this.src}${this.src.includes("?") ? "&" : "?"}_=${Date.now()}`;
2274
+ try {
2275
+ // redirect: "manual" prevents fetch from transparently following a
2276
+ // 3xx response — without it, a status endpoint that redirected to,
2277
+ // say, the preview URL would resolve to a 200 and look like
2278
+ // "still processing." The contract is "any non-2xx means done."
2279
+ const response = await fetch(this.previewStatusUrl, { credentials: "include", redirect: "manual" });
2258
2280
 
2259
- img.onload = () => {
2260
2281
  if (!this.editor.read(() => this.isAttached())) return
2261
2282
 
2262
- // The placeholder is a file-type icon SVG (86×100). A real thumbnail
2263
- // generated from PDF/video content is significantly larger.
2264
- if (img.naturalWidth > 150 && img.naturalHeight > 150) {
2265
- this.#swapToPreviewDOM(figure, cacheBustedSrc);
2266
- } else {
2283
+ if (response.ok) {
2267
2284
  retry();
2285
+ } else {
2286
+ this.#swapToPreviewDOM(figure, this.src);
2268
2287
  }
2269
- };
2270
- img.onerror = () => retry();
2271
- img.src = cacheBustedSrc;
2288
+ } catch {
2289
+ retry();
2290
+ }
2272
2291
  };
2273
2292
 
2274
2293
  const retry = () => {
2275
2294
  attempt++;
2276
- if (attempt < maxAttempts && this.editor.read(() => this.isAttached())) {
2277
- const delay = Math.min(2000 * Math.pow(1.5, attempt), 15000);
2278
- setTimeout(tryLoad, delay);
2295
+ if (attempt < MAX_PREVIEW_POLL_ATTEMPTS && this.editor.read(() => this.isAttached())) {
2296
+ const delay = Math.min(2000 * Math.pow(1.5, attempt), MAX_PREVIEW_POLL_DELAY_MS);
2297
+ setTimeout(tryStatus, delay);
2279
2298
  }
2280
2299
  };
2281
2300
 
2282
2301
  // Give the server time to start processing before the first attempt
2283
- setTimeout(tryLoad, 3000);
2302
+ setTimeout(tryStatus, INITIAL_PREVIEW_POLL_DELAY_MS);
2303
+ }
2304
+
2305
+ #waitForPreviewByPreloadingImage(figure) {
2306
+ const img = new Image();
2307
+ img.onload = () => {
2308
+ if (!this.editor.read(() => this.isAttached())) return
2309
+ this.#swapToPreviewDOM(figure, this.src);
2310
+ };
2311
+ img.onerror = () => {
2312
+ // Clear pendingPreview so undo/redo or any JSON round-trip doesn't
2313
+ // re-enter the pending flow and issue another fetch. The file icon
2314
+ // stays as the stable fallback.
2315
+ if (!this.editor.read(() => this.isAttached())) return
2316
+ this.patchAndRewriteHistory({ pendingPreview: false });
2317
+ };
2318
+ img.src = this.src;
2284
2319
  }
2285
2320
 
2286
2321
  #swapToPreviewDOM(figure, previewSrc) {
@@ -3625,8 +3660,7 @@ class CommandDispatcher {
3625
3660
  }
3626
3661
 
3627
3662
  dispatchInsertHorizontalDivider() {
3628
- this.contents.insertAtCursorEnsuringLineBelow(new HorizontalDividerNode());
3629
- this.editor.focus();
3663
+ $insertNodeToNearestRoot(new HorizontalDividerNode);
3630
3664
  }
3631
3665
 
3632
3666
  dispatchSetFormatHeadingLarge() {
@@ -4859,7 +4893,7 @@ class ImageGalleryNode extends ElementNode {
4859
4893
  replaceWithSingularChild() {
4860
4894
  if (this.#hasSingularChild) {
4861
4895
  const child = this.getFirstChild();
4862
- return this.replace(child)
4896
+ return this.replace($makeSafeForRoot(child))
4863
4897
  }
4864
4898
  }
4865
4899
 
@@ -5287,6 +5321,7 @@ class AttachmentNodeConversion {
5287
5321
  fileName: blob.filename,
5288
5322
  fileSize: blob.byte_size,
5289
5323
  previewable: blob.previewable,
5324
+ previewStatusUrl: blob.preview_status_url
5290
5325
  }
5291
5326
  }
5292
5327
 
@@ -8638,6 +8673,10 @@ class LexicalPromptElement extends HTMLElement {
8638
8673
  return this.getAttribute("only-at")
8639
8674
  }
8640
8675
 
8676
+ get verticalDirection() {
8677
+ return this.getAttribute("vertical-direction")
8678
+ }
8679
+
8641
8680
  get open() {
8642
8681
  return this.popoverElement?.classList?.contains("lexxy-prompt-menu--visible")
8643
8682
  }
@@ -8866,11 +8905,15 @@ class LexicalPromptElement extends HTMLElement {
8866
8905
 
8867
8906
  const popoverRect = this.popoverElement.getBoundingClientRect();
8868
8907
 
8869
- if (popoverRect.right > window.innerWidth) {
8908
+ if (popoverRect.right > editorRect.right) {
8870
8909
  this.popoverElement.toggleAttribute("data-clipped-at-right", true);
8871
8910
  }
8872
8911
 
8873
- if (popoverRect.bottom > window.innerHeight) {
8912
+ const forceTop = this.verticalDirection === "top";
8913
+ const forceBottom = this.verticalDirection === "bottom";
8914
+ const overflowsWindow = popoverRect.bottom > window.innerHeight;
8915
+
8916
+ if (!forceBottom && (forceTop || overflowsWindow)) {
8874
8917
  this.#setPopoverOffsetY(contentRect.height - y + fontSize);
8875
8918
  this.popoverElement.toggleAttribute("data-clipped-at-bottom", true);
8876
8919
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@37signals/lexxy",
3
- "version": "0.9.15-alpha.1",
3
+ "version": "0.9.15-alpha.3",
4
4
  "description": "Lexxy - A modern rich text editor for Rails.",
5
5
  "module": "dist/lexxy.esm.js",
6
6
  "type": "module",