@37signals/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.
- package/dist/lexxy.esm.js +107 -33
- package/package.json +1 -1
package/dist/lexxy.esm.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { highlightCode } from './lexxy_helpers.esm.js';
|
|
2
2
|
import DOMPurify from 'dompurify';
|
|
3
3
|
import { getStyleObjectFromCSS, getCSSFromStyleObject, $getSelectionStyleValueForProperty, $ensureForwardRangeSelection, $isAtNodeEnd, $patchStyleText, $setBlocksType, $forEachSelectedTextNode } from '@lexical/selection';
|
|
4
|
-
import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_REDO_COMMAND, $getSelection, $isRangeSelection, DecoratorNode, $createTextNode, $caretFromPoint, $setSelectionFromCaretRange, $getCaretRange, $normalizeCaret, $getChildCaret, $getCaretInDirection, $isParagraphNode, $isLineBreakNode, $createParagraphNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $createNodeSelection, $isDecoratorNode, $isTextNode, $getSiblingCaret, $rewindSiblingCaret, $splitAtPointCaretNext, $isChildCaret, $isTextPointCaret, $isExtendableTextPointCaret, $isSiblingCaret, $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,
|
|
4
|
+
import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_REDO_COMMAND, $getSelection, $isRangeSelection, DecoratorNode, $createTextNode, $getRoot, $caretFromPoint, $setSelectionFromCaretRange, $getCaretRange, $normalizeCaret, $getChildCaret, $getCaretInDirection, $isParagraphNode, $isLineBreakNode, $createParagraphNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $createNodeSelection, $isDecoratorNode, $isTextNode, $getSiblingCaret, $rewindSiblingCaret, $splitAtPointCaretNext, $isChildCaret, $isTextPointCaret, $isExtendableTextPointCaret, $isSiblingCaret, $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, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, DELETE_CHARACTER_COMMAND, SELECTION_CHANGE_COMMAND, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, $addUpdateTag, ElementNode, $splitNode, $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, INPUT_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
|
|
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';
|
|
@@ -1527,14 +1527,15 @@ function $isShadowRoot(node) {
|
|
|
1527
1527
|
return $isElementNode(node) && $isRootOrShadowRoot(node) && !$isRootNode(node)
|
|
1528
1528
|
}
|
|
1529
1529
|
|
|
1530
|
+
function $isSafeForRoot(node) {
|
|
1531
|
+
return ($isElementNode(node) || $isDecoratorNode(node)) && !node.isParentRequired()
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1530
1534
|
function $makeSafeForRoot(node) {
|
|
1531
|
-
if ($
|
|
1532
|
-
return $wrapNodeInElement(node, $createParagraphNode)
|
|
1533
|
-
} else if (node.isParentRequired()) {
|
|
1534
|
-
const parent = node.createRequiredParent();
|
|
1535
|
-
return $wrapNodeInElement(node, parent)
|
|
1536
|
-
} else {
|
|
1535
|
+
if ($isSafeForRoot(node)) {
|
|
1537
1536
|
return node
|
|
1537
|
+
} else {
|
|
1538
|
+
return $wrapNodeInElement(node, () => node.createParentElementNode())
|
|
1538
1539
|
}
|
|
1539
1540
|
}
|
|
1540
1541
|
|
|
@@ -1649,6 +1650,39 @@ function $isListItemStructurallyEmpty(listItem) {
|
|
|
1649
1650
|
return true
|
|
1650
1651
|
}
|
|
1651
1652
|
|
|
1653
|
+
// Returns the document text up to `offset` inside `targetNode`. Non-inline
|
|
1654
|
+
// element siblings are joined with `\n\n`, matching Lexical's own
|
|
1655
|
+
// ElementNode.getTextContent behavior.
|
|
1656
|
+
function $textBeforeOffset(targetNode, offset) {
|
|
1657
|
+
const parts = [];
|
|
1658
|
+
let done = false;
|
|
1659
|
+
|
|
1660
|
+
function visit(node) {
|
|
1661
|
+
if (done) return
|
|
1662
|
+
if (node === targetNode) {
|
|
1663
|
+
parts.push(node.getTextContent().slice(0, offset));
|
|
1664
|
+
done = true;
|
|
1665
|
+
return
|
|
1666
|
+
}
|
|
1667
|
+
if ($isElementNode(node)) {
|
|
1668
|
+
const children = node.getChildren();
|
|
1669
|
+
for (let i = 0; i < children.length; i++) {
|
|
1670
|
+
visit(children[i]);
|
|
1671
|
+
if (done) return
|
|
1672
|
+
const child = children[i];
|
|
1673
|
+
if ($isElementNode(child) && !child.isInline() && i < children.length - 1) {
|
|
1674
|
+
parts.push("\n\n");
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
} else {
|
|
1678
|
+
parts.push(node.getTextContent());
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
visit($getRoot());
|
|
1683
|
+
return parts.join("")
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1652
1686
|
function isAttachmentSpacerTextNode(node, previousNode, index, childCount) {
|
|
1653
1687
|
return $isTextNode(node)
|
|
1654
1688
|
&& node.getTextContent() === " "
|
|
@@ -1934,6 +1968,11 @@ function safeCloneEditorState(editorState) {
|
|
|
1934
1968
|
return clone
|
|
1935
1969
|
}
|
|
1936
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
|
+
|
|
1937
1976
|
class ActionTextAttachmentNode extends DecoratorNode {
|
|
1938
1977
|
static getType() {
|
|
1939
1978
|
return "action_text_attachment"
|
|
@@ -2008,7 +2047,7 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2008
2047
|
return Lexxy.global.get("attachmentTagName")
|
|
2009
2048
|
}
|
|
2010
2049
|
|
|
2011
|
-
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) {
|
|
2012
2051
|
super(key);
|
|
2013
2052
|
|
|
2014
2053
|
this.tagName = tagName || ActionTextAttachmentNode.TAG_NAME;
|
|
@@ -2016,6 +2055,7 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2016
2055
|
this.src = src;
|
|
2017
2056
|
this.previewSrc = previewSrc;
|
|
2018
2057
|
this.previewable = parseBoolean(previewable);
|
|
2058
|
+
this.previewStatusUrl = previewStatusUrl;
|
|
2019
2059
|
this.pendingPreview = pendingPreview;
|
|
2020
2060
|
this.altText = altText || "";
|
|
2021
2061
|
this.caption = caption || "";
|
|
@@ -2094,6 +2134,8 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2094
2134
|
sgid: this.sgid,
|
|
2095
2135
|
src: this.src,
|
|
2096
2136
|
previewable: this.previewable,
|
|
2137
|
+
previewStatusUrl: this.previewStatusUrl,
|
|
2138
|
+
pendingPreview: this.pendingPreview,
|
|
2097
2139
|
altText: this.altText,
|
|
2098
2140
|
caption: this.caption,
|
|
2099
2141
|
contentType: this.contentType,
|
|
@@ -2212,41 +2254,61 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2212
2254
|
});
|
|
2213
2255
|
}
|
|
2214
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.
|
|
2215
2260
|
#pollForPreview(figure) {
|
|
2261
|
+
if (this.previewStatusUrl) {
|
|
2262
|
+
this.#waitForPreviewByPollingStatus(figure);
|
|
2263
|
+
} else {
|
|
2264
|
+
this.#waitForPreviewByPreloadingImage(figure);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
#waitForPreviewByPollingStatus(figure) {
|
|
2216
2269
|
let attempt = 0;
|
|
2217
|
-
const maxAttempts = 10;
|
|
2218
2270
|
|
|
2219
|
-
const
|
|
2271
|
+
const tryStatus = async () => {
|
|
2220
2272
|
if (!this.editor.read(() => this.isAttached())) return
|
|
2221
2273
|
|
|
2222
|
-
|
|
2223
|
-
|
|
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" });
|
|
2224
2280
|
|
|
2225
|
-
img.onload = () => {
|
|
2226
2281
|
if (!this.editor.read(() => this.isAttached())) return
|
|
2227
2282
|
|
|
2228
|
-
|
|
2229
|
-
// generated from PDF/video content is significantly larger.
|
|
2230
|
-
if (img.naturalWidth > 150 && img.naturalHeight > 150) {
|
|
2231
|
-
this.#swapToPreviewDOM(figure, cacheBustedSrc);
|
|
2232
|
-
} else {
|
|
2283
|
+
if (response.ok) {
|
|
2233
2284
|
retry();
|
|
2285
|
+
} else {
|
|
2286
|
+
this.#swapToPreviewDOM(figure, this.src);
|
|
2234
2287
|
}
|
|
2235
|
-
}
|
|
2236
|
-
|
|
2237
|
-
|
|
2288
|
+
} catch {
|
|
2289
|
+
retry();
|
|
2290
|
+
}
|
|
2238
2291
|
};
|
|
2239
2292
|
|
|
2240
2293
|
const retry = () => {
|
|
2241
2294
|
attempt++;
|
|
2242
|
-
if (attempt <
|
|
2243
|
-
const delay = Math.min(2000 * Math.pow(1.5, attempt),
|
|
2244
|
-
setTimeout(
|
|
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);
|
|
2245
2298
|
}
|
|
2246
2299
|
};
|
|
2247
2300
|
|
|
2248
2301
|
// Give the server time to start processing before the first attempt
|
|
2249
|
-
setTimeout(
|
|
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.src = this.src;
|
|
2250
2312
|
}
|
|
2251
2313
|
|
|
2252
2314
|
#swapToPreviewDOM(figure, previewSrc) {
|
|
@@ -4825,7 +4887,7 @@ class ImageGalleryNode extends ElementNode {
|
|
|
4825
4887
|
replaceWithSingularChild() {
|
|
4826
4888
|
if (this.#hasSingularChild) {
|
|
4827
4889
|
const child = this.getFirstChild();
|
|
4828
|
-
return this.replace(child)
|
|
4890
|
+
return this.replace($makeSafeForRoot(child))
|
|
4829
4891
|
}
|
|
4830
4892
|
}
|
|
4831
4893
|
|
|
@@ -5253,6 +5315,7 @@ class AttachmentNodeConversion {
|
|
|
5253
5315
|
fileName: blob.filename,
|
|
5254
5316
|
fileSize: blob.byte_size,
|
|
5255
5317
|
previewable: blob.previewable,
|
|
5318
|
+
previewStatusUrl: blob.preview_status_url
|
|
5256
5319
|
}
|
|
5257
5320
|
}
|
|
5258
5321
|
|
|
@@ -8552,6 +8615,9 @@ class RemoteFilterSource extends BaseSource {
|
|
|
8552
8615
|
const NOTHING_FOUND_DEFAULT_MESSAGE = "Nothing found";
|
|
8553
8616
|
const FILTER_DEBOUNCE_INTERVAL = 50;
|
|
8554
8617
|
|
|
8618
|
+
// Start of line, or after a space or newline.
|
|
8619
|
+
const DEFAULT_ONLY_AT_PATTERN = "^|[ \\n]";
|
|
8620
|
+
|
|
8555
8621
|
class LexicalPromptElement extends HTMLElement {
|
|
8556
8622
|
#globalListeners = new ListenerBin()
|
|
8557
8623
|
#popoverListeners = new ListenerBin()
|
|
@@ -8597,6 +8663,10 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8597
8663
|
return this.hasAttribute("supports-space-in-searches")
|
|
8598
8664
|
}
|
|
8599
8665
|
|
|
8666
|
+
get onlyAt() {
|
|
8667
|
+
return this.getAttribute("only-at")
|
|
8668
|
+
}
|
|
8669
|
+
|
|
8600
8670
|
get open() {
|
|
8601
8671
|
return this.popoverElement?.classList?.contains("lexxy-prompt-menu--visible")
|
|
8602
8672
|
}
|
|
@@ -8640,14 +8710,10 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8640
8710
|
if (offset >= triggerLength) {
|
|
8641
8711
|
const textBeforeCursor = fullText.slice(offset - triggerLength, offset);
|
|
8642
8712
|
|
|
8643
|
-
// Check if trigger is at the start of the text node (new line case) or preceded by space or newline
|
|
8644
8713
|
if (textBeforeCursor === this.trigger) {
|
|
8645
|
-
const
|
|
8714
|
+
const textBeforeTrigger = $textBeforeOffset(node, offset - triggerLength);
|
|
8646
8715
|
|
|
8647
|
-
|
|
8648
|
-
const isPrecededBySpaceOrNewline = charBeforeTrigger === " " || charBeforeTrigger === "\n";
|
|
8649
|
-
|
|
8650
|
-
if (isAtStart || isPrecededBySpaceOrNewline) {
|
|
8716
|
+
if (this.#onlyAtRegExp.test(textBeforeTrigger)) {
|
|
8651
8717
|
this.#popoverListeners.dispose();
|
|
8652
8718
|
this.#showPopover();
|
|
8653
8719
|
}
|
|
@@ -8658,7 +8724,15 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8658
8724
|
}));
|
|
8659
8725
|
}
|
|
8660
8726
|
|
|
8727
|
+
get #onlyAtRegExp() {
|
|
8728
|
+
return new RegExp(`(?:${this.onlyAt ?? DEFAULT_ONLY_AT_PATTERN})$`)
|
|
8729
|
+
}
|
|
8730
|
+
|
|
8661
8731
|
get #promptContentTypePermitted() {
|
|
8732
|
+
// `insert-editable-text` prompts never create attachments, so the
|
|
8733
|
+
// editor's attachment support and content-type allowlist don't apply.
|
|
8734
|
+
if (this.hasAttribute("insert-editable-text")) return true
|
|
8735
|
+
|
|
8662
8736
|
const el = this.#editorElement;
|
|
8663
8737
|
if (!el.supportsAttachments) {
|
|
8664
8738
|
return false
|
|
@@ -8821,7 +8895,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8821
8895
|
|
|
8822
8896
|
const popoverRect = this.popoverElement.getBoundingClientRect();
|
|
8823
8897
|
|
|
8824
|
-
if (popoverRect.right >
|
|
8898
|
+
if (popoverRect.right > editorRect.right) {
|
|
8825
8899
|
this.popoverElement.toggleAttribute("data-clipped-at-right", true);
|
|
8826
8900
|
}
|
|
8827
8901
|
|