@37signals/lexxy 0.9.19-alpha.3 → 0.9.19
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 +338 -34
- package/dist/stylesheets/lexxy-editor.css +17 -1
- package/package.json +1 -1
package/dist/lexxy.esm.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { highlightCode, highlightElement } 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, $getRoot, $caretFromPoint, $setSelectionFromCaretRange, $getCaretRange, $normalizeCaret, $getChildCaret, $getCaretInDirection, $isParagraphNode, $isLineBreakNode, $createParagraphNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $createNodeSelection, $isDecoratorNode, $isTextNode, $getSiblingCaret, $rewindSiblingCaret, $splitAtPointCaretNext, $normalizeSelection__EXPERIMENTAL, $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, INSERT_LINE_BREAK_COMMAND, COMMAND_PRIORITY_HIGH, INSERT_PARAGRAPH_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, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, PASTE_COMMAND, $onUpdate, ParagraphNode, RootNode, DRAGSTART_COMMAND, DROP_COMMAND, mergeRegister as mergeRegister$1, CLEAR_HISTORY_COMMAND, KEY_ENTER_COMMAND, COMMAND_PRIORITY_CRITICAL, KEY_SPACE_COMMAND, INPUT_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
|
|
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, $normalizeSelection__EXPERIMENTAL, $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, INSERT_LINE_BREAK_COMMAND, COMMAND_PRIORITY_HIGH, INSERT_PARAGRAPH_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, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, PASTE_COMMAND, $onUpdate, ParagraphNode, RootNode, DRAGSTART_COMMAND, DROP_COMMAND, mergeRegister as mergeRegister$1, $createRangeSelectionFromDom, CLEAR_HISTORY_COMMAND, 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, $isListNode, $isListItemNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, $createListNode, $createListItemNode, registerList } from '@lexical/list';
|
|
@@ -1384,7 +1384,8 @@ class CustomActionTextAttachmentNode extends DecoratorNode {
|
|
|
1384
1384
|
}
|
|
1385
1385
|
|
|
1386
1386
|
createDOM() {
|
|
1387
|
-
const figure = createElement(this.tagName, { "content-type": this.contentType, "data-lexxy-decorator": true });
|
|
1387
|
+
const figure = createElement(this.tagName, { "content-type": this.contentType, "data-lexxy-decorator": true, draggable: true });
|
|
1388
|
+
figure.dataset.lexicalNodeKey = this.__key;
|
|
1388
1389
|
|
|
1389
1390
|
figure.insertAdjacentHTML("beforeend", sanitize(this.innerHtml));
|
|
1390
1391
|
|
|
@@ -1437,6 +1438,10 @@ class CustomActionTextAttachmentNode extends DecoratorNode {
|
|
|
1437
1438
|
}
|
|
1438
1439
|
}
|
|
1439
1440
|
|
|
1441
|
+
function $isCustomActionTextAttachmentNode(node) {
|
|
1442
|
+
return node instanceof CustomActionTextAttachmentNode
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1440
1445
|
function dasherize(value) {
|
|
1441
1446
|
return value.replace(/([A-Z])/g, (_, char) => `-${char.toLowerCase()}`)
|
|
1442
1447
|
}
|
|
@@ -3928,7 +3933,7 @@ class CommandDispatcher {
|
|
|
3928
3933
|
}
|
|
3929
3934
|
|
|
3930
3935
|
#isInternalDrag(event) {
|
|
3931
|
-
return event.dataTransfer?.types.
|
|
3936
|
+
return event.dataTransfer?.types.some((type) => type.startsWith("application/x-lexxy-"))
|
|
3932
3937
|
}
|
|
3933
3938
|
|
|
3934
3939
|
#handleTabKey(event) {
|
|
@@ -3982,14 +3987,14 @@ class Selection {
|
|
|
3982
3987
|
}
|
|
3983
3988
|
|
|
3984
3989
|
get cursorPosition() {
|
|
3985
|
-
let position =
|
|
3990
|
+
let position = null;
|
|
3986
3991
|
|
|
3987
3992
|
this.editor.getEditorState().read(() => {
|
|
3988
3993
|
const range = this.#getValidSelectionRange();
|
|
3989
3994
|
if (!range) return
|
|
3990
3995
|
|
|
3991
3996
|
const rect = this.#getReliableRectFromRange(range);
|
|
3992
|
-
if (
|
|
3997
|
+
if (this.#isRectUnreliable(rect)) return
|
|
3993
3998
|
|
|
3994
3999
|
position = this.#calculateCursorPosition(rect, range);
|
|
3995
4000
|
});
|
|
@@ -5559,7 +5564,7 @@ class ShadowRootNodeInserter extends BaseNodeInserter {
|
|
|
5559
5564
|
|
|
5560
5565
|
class NodeSelectionNodeInserter extends BaseNodeInserter {
|
|
5561
5566
|
static handles(selection) {
|
|
5562
|
-
return $isNodeSelection(selection)
|
|
5567
|
+
return $isNodeSelection(selection) && selection.getNodes().length > 0
|
|
5563
5568
|
}
|
|
5564
5569
|
|
|
5565
5570
|
insertNodes(nodes) {
|
|
@@ -5757,8 +5762,15 @@ class Contents {
|
|
|
5757
5762
|
}, { tag });
|
|
5758
5763
|
}
|
|
5759
5764
|
|
|
5765
|
+
insertText(text, { tag } = {}) {
|
|
5766
|
+
this.editor.update(() => {
|
|
5767
|
+
const paragraph = $createParagraphNode().append($createTextNode(text));
|
|
5768
|
+
this.insertAtCursor(paragraph);
|
|
5769
|
+
}, { tag });
|
|
5770
|
+
}
|
|
5771
|
+
|
|
5760
5772
|
insertAtCursor(...nodes) {
|
|
5761
|
-
const selection =
|
|
5773
|
+
const selection = this.#insertableSelection();
|
|
5762
5774
|
const inserter = BaseNodeInserter.for(selection);
|
|
5763
5775
|
|
|
5764
5776
|
inserter.insertNodes(nodes);
|
|
@@ -5952,14 +5964,13 @@ class Contents {
|
|
|
5952
5964
|
replaceTextBackUntil(stringToReplace, replacementNodes) {
|
|
5953
5965
|
replacementNodes = Array.isArray(replacementNodes) ? replacementNodes : [ replacementNodes ];
|
|
5954
5966
|
|
|
5955
|
-
const selection = $getSelection();
|
|
5956
5967
|
const { anchorNode, offset } = this.#getTextAnchorData();
|
|
5957
5968
|
if (!anchorNode) return
|
|
5958
5969
|
|
|
5959
5970
|
const lastIndex = this.#findReplacementStart(anchorNode, offset, stringToReplace);
|
|
5960
5971
|
if (lastIndex === -1) return
|
|
5961
5972
|
|
|
5962
|
-
this.#performTextReplacement(anchorNode,
|
|
5973
|
+
this.#performTextReplacement(anchorNode, lastIndex, stringToReplace, replacementNodes);
|
|
5963
5974
|
}
|
|
5964
5975
|
|
|
5965
5976
|
uploadFiles(files, { selectLast } = {}) {
|
|
@@ -6099,6 +6110,15 @@ class Contents {
|
|
|
6099
6110
|
});
|
|
6100
6111
|
}
|
|
6101
6112
|
|
|
6113
|
+
#insertableSelection() {
|
|
6114
|
+
const selection = $getSelection();
|
|
6115
|
+
if ($isNodeSelection(selection) && selection.getNodes().length === 0) {
|
|
6116
|
+
return $getRoot().selectEnd()
|
|
6117
|
+
}
|
|
6118
|
+
|
|
6119
|
+
return selection ?? $getRoot().selectEnd()
|
|
6120
|
+
}
|
|
6121
|
+
|
|
6102
6122
|
#formatPastedDOM(doc) {
|
|
6103
6123
|
new PastedContentFormatter(doc).format();
|
|
6104
6124
|
}
|
|
@@ -6282,13 +6302,13 @@ class Contents {
|
|
|
6282
6302
|
}
|
|
6283
6303
|
}
|
|
6284
6304
|
|
|
6285
|
-
#performTextReplacement(anchorNode,
|
|
6305
|
+
#performTextReplacement(anchorNode, startIndex, stringToReplace, replacementNodes) {
|
|
6286
6306
|
const fullText = anchorNode.getTextContent();
|
|
6287
6307
|
const textBeforeString = fullText.slice(0, startIndex);
|
|
6288
6308
|
const textAfterString = fullText.slice(startIndex + stringToReplace.length);
|
|
6289
6309
|
|
|
6290
|
-
const textNodeBefore = this.#cloneTextNodeFormatting(anchorNode,
|
|
6291
|
-
const textNodeAfter = this.#cloneTextNodeFormatting(anchorNode,
|
|
6310
|
+
const textNodeBefore = this.#cloneTextNodeFormatting(anchorNode, textBeforeString);
|
|
6311
|
+
const textNodeAfter = this.#cloneTextNodeFormatting(anchorNode, textAfterString || " ");
|
|
6292
6312
|
|
|
6293
6313
|
anchorNode.replace(textNodeBefore);
|
|
6294
6314
|
|
|
@@ -6300,18 +6320,12 @@ class Contents {
|
|
|
6300
6320
|
textNodeAfter.select(cursorOffset, cursorOffset);
|
|
6301
6321
|
}
|
|
6302
6322
|
|
|
6303
|
-
#cloneTextNodeFormatting(anchorNode,
|
|
6304
|
-
const parent = anchorNode.getParent();
|
|
6305
|
-
const fallbackFormat = parent?.getTextFormat?.() || 0;
|
|
6306
|
-
const fallbackStyle = parent?.getTextStyle?.() || "";
|
|
6307
|
-
const format = $isRangeSelection(selection) && selection.format ? selection.format : (anchorNode.getFormat() || fallbackFormat);
|
|
6308
|
-
const style = $isRangeSelection(selection) && selection.style ? selection.style : (anchorNode.getStyle() || fallbackStyle);
|
|
6309
|
-
|
|
6323
|
+
#cloneTextNodeFormatting(anchorNode, text) {
|
|
6310
6324
|
return $createTextNode(text)
|
|
6311
|
-
.setFormat(
|
|
6325
|
+
.setFormat(anchorNode.getFormat())
|
|
6312
6326
|
.setDetail(anchorNode.getDetail())
|
|
6313
6327
|
.setMode(anchorNode.getMode())
|
|
6314
|
-
.setStyle(
|
|
6328
|
+
.setStyle(anchorNode.getStyle())
|
|
6315
6329
|
}
|
|
6316
6330
|
|
|
6317
6331
|
#insertReplacementNodes(startNode, replacementNodes) {
|
|
@@ -6529,14 +6543,31 @@ class Clipboard {
|
|
|
6529
6543
|
#pasteMarkdown(text) {
|
|
6530
6544
|
const html = marked(text, { breaks: true });
|
|
6531
6545
|
const doc = parseHtml(html);
|
|
6532
|
-
const detail = Object.freeze({
|
|
6533
|
-
markdown: text,
|
|
6534
|
-
document: doc,
|
|
6535
|
-
addBlockSpacing: () => addBlockSpacing(doc)
|
|
6536
|
-
});
|
|
6537
6546
|
|
|
6538
|
-
|
|
6539
|
-
|
|
6547
|
+
if (this.#isPlainTextWithoutMarkdown(doc)) {
|
|
6548
|
+
this.contents.insertText(text, { tag: PASTE_TAG });
|
|
6549
|
+
} else {
|
|
6550
|
+
const detail = Object.freeze({
|
|
6551
|
+
markdown: text,
|
|
6552
|
+
document: doc,
|
|
6553
|
+
addBlockSpacing: () => addBlockSpacing(doc)
|
|
6554
|
+
});
|
|
6555
|
+
|
|
6556
|
+
dispatch(this.editorElement, "lexxy:insert-markdown", detail);
|
|
6557
|
+
this.contents.insertDOM(doc, { tag: PASTE_TAG });
|
|
6558
|
+
}
|
|
6559
|
+
}
|
|
6560
|
+
|
|
6561
|
+
// Markdown conversion collapses runs of whitespace and unescapes backslashes,
|
|
6562
|
+
// silently corrupting plain text such as Windows/UNC file paths. When the text
|
|
6563
|
+
// carries no Markdown structure, paste it verbatim instead.
|
|
6564
|
+
#isPlainTextWithoutMarkdown(doc) {
|
|
6565
|
+
const elements = Array.from(doc.body.children);
|
|
6566
|
+
if (elements.length !== 1) return false
|
|
6567
|
+
|
|
6568
|
+
const paragraph = elements[0];
|
|
6569
|
+
return paragraph.nodeName === "P"
|
|
6570
|
+
&& Array.from(paragraph.childNodes).every((node) => node.nodeType === Node.TEXT_NODE)
|
|
6540
6571
|
}
|
|
6541
6572
|
|
|
6542
6573
|
#pasteRichText(clipboardData) {
|
|
@@ -7144,7 +7175,7 @@ class TablesExtension extends LexxyExtension {
|
|
|
7144
7175
|
}
|
|
7145
7176
|
}
|
|
7146
7177
|
|
|
7147
|
-
const MIME_TYPE = "application/x-lexxy-node-key";
|
|
7178
|
+
const MIME_TYPE$1 = "application/x-lexxy-node-key";
|
|
7148
7179
|
|
|
7149
7180
|
class AttachmentDragAndDrop {
|
|
7150
7181
|
#editor
|
|
@@ -7191,7 +7222,7 @@ class AttachmentDragAndDrop {
|
|
|
7191
7222
|
if (!figure) return false
|
|
7192
7223
|
|
|
7193
7224
|
this.#draggedNodeKey = figure.dataset.lexicalNodeKey;
|
|
7194
|
-
event.dataTransfer.setData(MIME_TYPE, this.#draggedNodeKey);
|
|
7225
|
+
event.dataTransfer.setData(MIME_TYPE$1, this.#draggedNodeKey);
|
|
7195
7226
|
event.dataTransfer.effectAllowed = "move";
|
|
7196
7227
|
|
|
7197
7228
|
// Add dragging class after a tick so it doesn't affect the drag image
|
|
@@ -7971,6 +8002,268 @@ class PreventLexicalTripleClickExtension extends LexxyExtension {
|
|
|
7971
8002
|
}
|
|
7972
8003
|
}
|
|
7973
8004
|
|
|
8005
|
+
function caretRect(node, offset) {
|
|
8006
|
+
const range = document.createRange();
|
|
8007
|
+
range.setStart(node, offset);
|
|
8008
|
+
range.collapse(true);
|
|
8009
|
+
|
|
8010
|
+
const rect = range.getBoundingClientRect();
|
|
8011
|
+
if (rect.height > 0) {
|
|
8012
|
+
return rect
|
|
8013
|
+
} else {
|
|
8014
|
+
return null
|
|
8015
|
+
}
|
|
8016
|
+
}
|
|
8017
|
+
|
|
8018
|
+
function caretFromPoint(clientX, clientY) {
|
|
8019
|
+
if (document.caretPositionFromPoint) {
|
|
8020
|
+
const position = document.caretPositionFromPoint(clientX, clientY);
|
|
8021
|
+
if (position) return { node: position.offsetNode, offset: position.offset }
|
|
8022
|
+
} else if (document.caretRangeFromPoint) {
|
|
8023
|
+
const range = document.caretRangeFromPoint(clientX, clientY);
|
|
8024
|
+
if (range) return { node: range.startContainer, offset: range.startOffset }
|
|
8025
|
+
}
|
|
8026
|
+
|
|
8027
|
+
return null
|
|
8028
|
+
}
|
|
8029
|
+
|
|
8030
|
+
const MIME_TYPE = "application/x-lexxy-custom-attachment-key";
|
|
8031
|
+
|
|
8032
|
+
// Custom inline attachments reorder by dropping at a text caret, unlike block
|
|
8033
|
+
// attachments which insert between blocks or into galleries.
|
|
8034
|
+
class CustomAttachmentDragAndDrop {
|
|
8035
|
+
#editor
|
|
8036
|
+
#draggedNodeKey = null
|
|
8037
|
+
#draggingRafId = null
|
|
8038
|
+
#dragOverRafId = null
|
|
8039
|
+
#dropIndicator = null
|
|
8040
|
+
#listeners = new ListenerBin()
|
|
8041
|
+
|
|
8042
|
+
constructor(editor) {
|
|
8043
|
+
this.#editor = editor;
|
|
8044
|
+
|
|
8045
|
+
// Register at HIGH priority to intercept before the base @lexical/rich-text
|
|
8046
|
+
// handlers, which consume drag events. The block-attachment handler also
|
|
8047
|
+
// registers here but bails for inline custom attachments, so we get our turn.
|
|
8048
|
+
this.#listeners.track(
|
|
8049
|
+
editor.registerCommand(DRAGSTART_COMMAND, (event) => this.#handleDragStart(event), COMMAND_PRIORITY_HIGH),
|
|
8050
|
+
editor.registerCommand(DROP_COMMAND, (event) => this.#handleDrop(event), COMMAND_PRIORITY_HIGH)
|
|
8051
|
+
);
|
|
8052
|
+
|
|
8053
|
+
this.#listeners.track(editor.registerRootListener((root, prevRoot) => {
|
|
8054
|
+
if (prevRoot) {
|
|
8055
|
+
prevRoot.removeEventListener("dragover", this.#onDragOver);
|
|
8056
|
+
prevRoot.removeEventListener("dragend", this.#onDragEnd);
|
|
8057
|
+
}
|
|
8058
|
+
if (root) {
|
|
8059
|
+
root.addEventListener("dragover", this.#onDragOver);
|
|
8060
|
+
root.addEventListener("dragend", this.#onDragEnd);
|
|
8061
|
+
}
|
|
8062
|
+
}));
|
|
8063
|
+
}
|
|
8064
|
+
|
|
8065
|
+
destroy() {
|
|
8066
|
+
this.#cleanup();
|
|
8067
|
+
this.#dropIndicator?.remove();
|
|
8068
|
+
this.#dropIndicator = null;
|
|
8069
|
+
this.#listeners.dispose();
|
|
8070
|
+
}
|
|
8071
|
+
|
|
8072
|
+
#handleDragStart(event) {
|
|
8073
|
+
const attachment = this.#customAttachmentElementFrom(event.target);
|
|
8074
|
+
if (!attachment) return false
|
|
8075
|
+
|
|
8076
|
+
this.#draggedNodeKey = attachment.dataset.lexicalNodeKey;
|
|
8077
|
+
event.dataTransfer.setData(MIME_TYPE, this.#draggedNodeKey);
|
|
8078
|
+
event.dataTransfer.effectAllowed = "move";
|
|
8079
|
+
|
|
8080
|
+
this.#draggingRafId = requestAnimationFrame(() => {
|
|
8081
|
+
this.#draggingRafId = null;
|
|
8082
|
+
attachment.classList.add("lexxy-dragging");
|
|
8083
|
+
});
|
|
8084
|
+
|
|
8085
|
+
return true
|
|
8086
|
+
}
|
|
8087
|
+
|
|
8088
|
+
#onDragOver = (event) => {
|
|
8089
|
+
if (!this.#draggedNodeKey) return
|
|
8090
|
+
|
|
8091
|
+
event.preventDefault();
|
|
8092
|
+
event.dataTransfer.dropEffect = "move";
|
|
8093
|
+
|
|
8094
|
+
if (!this.#dragOverRafId) {
|
|
8095
|
+
this.#dragOverRafId = requestAnimationFrame(() => {
|
|
8096
|
+
this.#dragOverRafId = null;
|
|
8097
|
+
this.#updateDropIndicator(event);
|
|
8098
|
+
});
|
|
8099
|
+
}
|
|
8100
|
+
}
|
|
8101
|
+
|
|
8102
|
+
#onDragEnd = () => {
|
|
8103
|
+
this.#cleanup();
|
|
8104
|
+
}
|
|
8105
|
+
|
|
8106
|
+
#handleDrop(event) {
|
|
8107
|
+
if (!this.#draggedNodeKey) return false
|
|
8108
|
+
|
|
8109
|
+
event.preventDefault();
|
|
8110
|
+
|
|
8111
|
+
const dropPoint = this.#resolveDropPoint(event);
|
|
8112
|
+
const draggedKey = this.#draggedNodeKey;
|
|
8113
|
+
this.#cleanup();
|
|
8114
|
+
|
|
8115
|
+
if (dropPoint) {
|
|
8116
|
+
this.#moveAttachment(draggedKey, dropPoint);
|
|
8117
|
+
}
|
|
8118
|
+
|
|
8119
|
+
return true
|
|
8120
|
+
}
|
|
8121
|
+
|
|
8122
|
+
#resolveDropPoint(event) {
|
|
8123
|
+
const rootElement = this.#editor.getRootElement();
|
|
8124
|
+
if (!rootElement) return null
|
|
8125
|
+
|
|
8126
|
+
const caret = caretFromPoint(event.clientX, event.clientY);
|
|
8127
|
+
if (!caret || !rootElement.contains(caret.node)) return null
|
|
8128
|
+
|
|
8129
|
+
// A caret on the root itself points between blocks. Mentions behave like text:
|
|
8130
|
+
// they only drop onto an existing line, so snap to the nearest one.
|
|
8131
|
+
if (caret.node === rootElement) {
|
|
8132
|
+
return this.#nearestLineCaret(rootElement, event.clientY)
|
|
8133
|
+
} else {
|
|
8134
|
+
return caret
|
|
8135
|
+
}
|
|
8136
|
+
}
|
|
8137
|
+
|
|
8138
|
+
#nearestLineCaret(rootElement, clientY) {
|
|
8139
|
+
let nearestLine = null;
|
|
8140
|
+
let nearestDistance = Infinity;
|
|
8141
|
+
|
|
8142
|
+
for (const line of rootElement.children) {
|
|
8143
|
+
const rect = line.getBoundingClientRect();
|
|
8144
|
+
const distance = Math.min(Math.abs(clientY - rect.top), Math.abs(clientY - rect.bottom));
|
|
8145
|
+
if (distance < nearestDistance) {
|
|
8146
|
+
nearestDistance = distance;
|
|
8147
|
+
nearestLine = line;
|
|
8148
|
+
}
|
|
8149
|
+
}
|
|
8150
|
+
|
|
8151
|
+
if (!nearestLine) return null
|
|
8152
|
+
|
|
8153
|
+
const rect = nearestLine.getBoundingClientRect();
|
|
8154
|
+
if (clientY < rect.top) {
|
|
8155
|
+
return { node: nearestLine, offset: 0 }
|
|
8156
|
+
} else {
|
|
8157
|
+
return { node: nearestLine, offset: nearestLine.childNodes.length }
|
|
8158
|
+
}
|
|
8159
|
+
}
|
|
8160
|
+
|
|
8161
|
+
#moveAttachment(draggedKey, dropPoint) {
|
|
8162
|
+
this.#editor.update(() => {
|
|
8163
|
+
const draggedNode = $getNodeByKey(draggedKey);
|
|
8164
|
+
if (!$isCustomActionTextAttachmentNode(draggedNode)) return
|
|
8165
|
+
|
|
8166
|
+
const selection = $createRangeSelectionFromDom({
|
|
8167
|
+
anchorNode: dropPoint.node,
|
|
8168
|
+
anchorOffset: dropPoint.offset,
|
|
8169
|
+
focusNode: dropPoint.node,
|
|
8170
|
+
focusOffset: dropPoint.offset
|
|
8171
|
+
}, this.#editor);
|
|
8172
|
+
if (!selection) return
|
|
8173
|
+
|
|
8174
|
+
$setSelection(selection);
|
|
8175
|
+
|
|
8176
|
+
draggedNode.remove();
|
|
8177
|
+
selection.insertNodes([ draggedNode ]);
|
|
8178
|
+
});
|
|
8179
|
+
}
|
|
8180
|
+
|
|
8181
|
+
#updateDropIndicator(event) {
|
|
8182
|
+
this.#hideCaret();
|
|
8183
|
+
|
|
8184
|
+
const dropPoint = this.#resolveDropPoint(event);
|
|
8185
|
+
if (dropPoint) this.#showCaret(this.#caretRectFor(dropPoint));
|
|
8186
|
+
}
|
|
8187
|
+
|
|
8188
|
+
#caretRectFor({ node, offset }) {
|
|
8189
|
+
const rect = caretRect(node, offset);
|
|
8190
|
+
if (rect) return rect
|
|
8191
|
+
|
|
8192
|
+
// A blank line has no text to measure, so fall back to the line's own box.
|
|
8193
|
+
const line = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
|
|
8194
|
+
if (!line) return null
|
|
8195
|
+
|
|
8196
|
+
const lineRect = line.getBoundingClientRect();
|
|
8197
|
+
return { left: lineRect.left, top: lineRect.top, height: lineRect.height }
|
|
8198
|
+
}
|
|
8199
|
+
|
|
8200
|
+
#showCaret(rect) {
|
|
8201
|
+
if (!rect) return
|
|
8202
|
+
|
|
8203
|
+
const caret = this.#ensureCaretIndicator();
|
|
8204
|
+
caret.style.blockSize = `${rect.height}px`;
|
|
8205
|
+
caret.style.insetInlineStart = `${rect.left}px`;
|
|
8206
|
+
caret.style.insetBlockStart = `${rect.top}px`;
|
|
8207
|
+
}
|
|
8208
|
+
|
|
8209
|
+
#ensureCaretIndicator() {
|
|
8210
|
+
this.#dropIndicator ||= createElement("div", { className: "lexxy-drop-caret" });
|
|
8211
|
+
|
|
8212
|
+
this.#editorElement().appendChild(this.#dropIndicator);
|
|
8213
|
+
this.#dropIndicator.style.display = "block";
|
|
8214
|
+
return this.#dropIndicator
|
|
8215
|
+
}
|
|
8216
|
+
|
|
8217
|
+
#editorElement() {
|
|
8218
|
+
return this.#editor.getRootElement().closest("lexxy-editor")
|
|
8219
|
+
}
|
|
8220
|
+
|
|
8221
|
+
#hideCaret() {
|
|
8222
|
+
if (this.#dropIndicator) this.#dropIndicator.style.display = "none";
|
|
8223
|
+
}
|
|
8224
|
+
|
|
8225
|
+
#customAttachmentElementFrom(target) {
|
|
8226
|
+
return target?.closest?.("[data-lexxy-decorator][data-lexical-node-key]")
|
|
8227
|
+
}
|
|
8228
|
+
|
|
8229
|
+
#cleanup() {
|
|
8230
|
+
if (this.#draggedNodeKey) {
|
|
8231
|
+
const rootElement = this.#editor.getRootElement();
|
|
8232
|
+
const attachment = rootElement?.querySelector(`[data-lexical-node-key="${this.#draggedNodeKey}"]`);
|
|
8233
|
+
attachment?.classList.remove("lexxy-dragging");
|
|
8234
|
+
}
|
|
8235
|
+
|
|
8236
|
+
this.#hideCaret();
|
|
8237
|
+
this.#draggedNodeKey = null;
|
|
8238
|
+
|
|
8239
|
+
if (this.#draggingRafId) {
|
|
8240
|
+
cancelAnimationFrame(this.#draggingRafId);
|
|
8241
|
+
this.#draggingRafId = null;
|
|
8242
|
+
}
|
|
8243
|
+
|
|
8244
|
+
if (this.#dragOverRafId) {
|
|
8245
|
+
cancelAnimationFrame(this.#dragOverRafId);
|
|
8246
|
+
this.#dragOverRafId = null;
|
|
8247
|
+
}
|
|
8248
|
+
}
|
|
8249
|
+
}
|
|
8250
|
+
|
|
8251
|
+
class CustomAttachmentDragAndDropExtension extends LexxyExtension {
|
|
8252
|
+
get enabled() {
|
|
8253
|
+
return this.editorElement.supportsRichText
|
|
8254
|
+
}
|
|
8255
|
+
|
|
8256
|
+
get lexicalExtension() {
|
|
8257
|
+
return defineExtension({
|
|
8258
|
+
name: "lexxy/custom-attachment-drag-and-drop",
|
|
8259
|
+
register: (editor) => {
|
|
8260
|
+
const dragAndDrop = new CustomAttachmentDragAndDrop(editor);
|
|
8261
|
+
return () => dragAndDrop.destroy()
|
|
8262
|
+
}
|
|
8263
|
+
})
|
|
8264
|
+
}
|
|
8265
|
+
}
|
|
8266
|
+
|
|
7974
8267
|
class LexicalEditorElement extends HTMLElement {
|
|
7975
8268
|
static formAssociated = true
|
|
7976
8269
|
static debug = false
|
|
@@ -8127,7 +8420,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
8127
8420
|
AttachmentsExtension,
|
|
8128
8421
|
FormatEscapeExtension,
|
|
8129
8422
|
LinkOpenerExtension,
|
|
8130
|
-
PreventLexicalTripleClickExtension
|
|
8423
|
+
PreventLexicalTripleClickExtension,
|
|
8424
|
+
CustomAttachmentDragAndDropExtension
|
|
8131
8425
|
]
|
|
8132
8426
|
}
|
|
8133
8427
|
|
|
@@ -9163,7 +9457,9 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
9163
9457
|
if (!node) return
|
|
9164
9458
|
|
|
9165
9459
|
if (this.#cursorIsTypingSearchTerm(node, offset)) {
|
|
9166
|
-
|
|
9460
|
+
if (!this.popoverElement.hasAttribute("data-anchored")) {
|
|
9461
|
+
this.#positionPopover();
|
|
9462
|
+
}
|
|
9167
9463
|
} else {
|
|
9168
9464
|
this.#hidePopover();
|
|
9169
9465
|
}
|
|
@@ -9294,8 +9590,16 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
9294
9590
|
}
|
|
9295
9591
|
}
|
|
9296
9592
|
|
|
9593
|
+
// Right after a Turbo history restore the editor reconnects before the DOM selection
|
|
9594
|
+
// is re-established, so the cursor geometry is momentarily unavailable. Anchoring then
|
|
9595
|
+
// would pin the menu to the editor's left edge for the rest of the open cycle, so we
|
|
9596
|
+
// skip it and let a later reposition anchor it once the selection is ready. The menu
|
|
9597
|
+
// stays hidden until anchored (see the `[data-anchored]` rule in the stylesheet).
|
|
9297
9598
|
#positionPopover() {
|
|
9298
|
-
const
|
|
9599
|
+
const cursorPosition = this.#selection.cursorPosition;
|
|
9600
|
+
if (!cursorPosition) return
|
|
9601
|
+
|
|
9602
|
+
const { x, y, fontSize } = cursorPosition;
|
|
9299
9603
|
const editorRect = this.#editorElement.getBoundingClientRect();
|
|
9300
9604
|
const contentRect = this.#editorContentElement.getBoundingClientRect();
|
|
9301
9605
|
const verticalOffset = contentRect.top - editorRect.top;
|
|
@@ -10598,4 +10902,4 @@ const configure = Lexxy.configure;
|
|
|
10598
10902
|
// Pushing elements definition to after the current call stack to allow global configuration to take place first
|
|
10599
10903
|
setTimeout(defineElements, 0);
|
|
10600
10904
|
|
|
10601
|
-
export { $createActionTextAttachmentNode, $createActionTextAttachmentUploadNode, $isActionTextAttachmentNode, ActionTextAttachmentNode, ActionTextAttachmentUploadNode, CustomActionTextAttachmentNode, LexxyExtension as Extension, HorizontalDividerNode, NativeAdapter, configure };
|
|
10905
|
+
export { $createActionTextAttachmentNode, $createActionTextAttachmentUploadNode, $isActionTextAttachmentNode, $isCustomActionTextAttachmentNode, ActionTextAttachmentNode, ActionTextAttachmentUploadNode, CustomActionTextAttachmentNode, LexxyExtension as Extension, HorizontalDividerNode, NativeAdapter, configure };
|
|
@@ -249,6 +249,22 @@
|
|
|
249
249
|
opacity: 0.4;
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
[data-lexxy-decorator][draggable] {
|
|
253
|
+
cursor: grab;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
[data-lexxy-decorator].lexxy-dragging {
|
|
257
|
+
opacity: 0.4;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.lexxy-drop-caret {
|
|
261
|
+
background-color: var(--lexxy-focus-ring-color);
|
|
262
|
+
border-radius: 1px;
|
|
263
|
+
inline-size: 2px;
|
|
264
|
+
pointer-events: none;
|
|
265
|
+
position: fixed;
|
|
266
|
+
}
|
|
267
|
+
|
|
252
268
|
[class*="lexxy-drop-target--"] {
|
|
253
269
|
position: relative;
|
|
254
270
|
}
|
|
@@ -1063,7 +1079,7 @@
|
|
|
1063
1079
|
}
|
|
1064
1080
|
}
|
|
1065
1081
|
|
|
1066
|
-
:where(.lexxy-prompt-menu--visible) {
|
|
1082
|
+
:where(.lexxy-prompt-menu--visible[data-anchored]) {
|
|
1067
1083
|
visibility: initial;
|
|
1068
1084
|
}
|
|
1069
1085
|
|