@37signals/lexxy 0.9.26 → 0.9.28
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 +193 -2
- 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, defineExtension, $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, 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,
|
|
4
|
+
import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_REDO_COMMAND, $getSelection, $isRangeSelection, DecoratorNode, $createTextNode, defineExtension, $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, 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, COMMAND_PRIORITY_CRITICAL, $addUpdateTag, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, 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, CONTROLLED_TEXT_INSERTION_COMMAND, KEY_SPACE_COMMAND, INPUT_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
|
|
5
5
|
import * as lexical from 'lexical';
|
|
6
6
|
export { lexical as Lexical };
|
|
7
7
|
import { LinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, $isLinkNode, AutoLinkNode } from '@lexical/link';
|
|
@@ -4523,10 +4523,34 @@ class Selection {
|
|
|
4523
4523
|
|
|
4524
4524
|
this.editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
|
|
4525
4525
|
this.#syncSelectedClasses();
|
|
4526
|
-
}, COMMAND_PRIORITY_LOW)
|
|
4526
|
+
}, COMMAND_PRIORITY_LOW),
|
|
4527
|
+
|
|
4528
|
+
this.editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
|
|
4529
|
+
this.#preserveFocusOfOtherFields();
|
|
4530
|
+
return false
|
|
4531
|
+
}, COMMAND_PRIORITY_CRITICAL)
|
|
4527
4532
|
);
|
|
4528
4533
|
}
|
|
4529
4534
|
|
|
4535
|
+
// Firefox (152+) keeps window.getSelection() anchored inside the editor after focus
|
|
4536
|
+
// moves to another field, and fires document selectionchange for every keystroke
|
|
4537
|
+
// typed there. Lexical reads that stale selection, reconciles, and pulls focus back
|
|
4538
|
+
// into the editor — stealing it mid-typing. When another text-entry field owns focus,
|
|
4539
|
+
// tag the update so Lexical leaves the DOM selection (and with it, focus) alone.
|
|
4540
|
+
#preserveFocusOfOtherFields() {
|
|
4541
|
+
if (this.#anotherFieldIsFocused) {
|
|
4542
|
+
$addUpdateTag(SKIP_DOM_SELECTION_TAG);
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4545
|
+
|
|
4546
|
+
get #anotherFieldIsFocused() {
|
|
4547
|
+
const rootElement = this.editor.getRootElement();
|
|
4548
|
+
const activeElement = rootElement?.ownerDocument.activeElement;
|
|
4549
|
+
|
|
4550
|
+
return activeElement && !rootElement.contains(activeElement) &&
|
|
4551
|
+
(activeElement.tagName === "INPUT" || activeElement.tagName === "TEXTAREA" || activeElement.isContentEditable)
|
|
4552
|
+
}
|
|
4553
|
+
|
|
4530
4554
|
#listenForNodeSelections() {
|
|
4531
4555
|
this.#listeners.track(this.editor.registerCommand(CLICK_COMMAND, ({ target }) => {
|
|
4532
4556
|
if (!isDOMNode(target)) return false
|
|
@@ -5914,6 +5938,149 @@ BaseNodeInserter.for = (selection) => {
|
|
|
5914
5938
|
return inserterClass ? new inserterClass(selection) : selection
|
|
5915
5939
|
};
|
|
5916
5940
|
|
|
5941
|
+
// Word and Outlook never emit <ul>/<ol>/<li>. They simulate lists with flat
|
|
5942
|
+
// paragraphs that declare their depth in an inline `mso-list:l0 level2 lfo1`
|
|
5943
|
+
// and carry the bullet or number glyph in a <span style="mso-list:Ignore">.
|
|
5944
|
+
// Nothing downstream understands either, so the items land as plain paragraphs
|
|
5945
|
+
// with the marker baked into the text. Rebuild real lists from the level each
|
|
5946
|
+
// paragraph declares.
|
|
5947
|
+
//
|
|
5948
|
+
// Everything this reads lives in inline attributes, so it stays correct after
|
|
5949
|
+
// the pasted content formatter has dropped Office's style sheet.
|
|
5950
|
+
class OfficeFormatter {
|
|
5951
|
+
constructor(doc) {
|
|
5952
|
+
this.doc = doc;
|
|
5953
|
+
}
|
|
5954
|
+
|
|
5955
|
+
format() {
|
|
5956
|
+
for (const run of this.#listRuns()) {
|
|
5957
|
+
this.#replaceWithLists(run);
|
|
5958
|
+
}
|
|
5959
|
+
}
|
|
5960
|
+
|
|
5961
|
+
// A run is a stretch of Office list paragraphs that are siblings with nothing
|
|
5962
|
+
// but whitespace and Word's conditional comments between them. Anything else
|
|
5963
|
+
// in between (a heading, a plain paragraph) ends one list and starts another.
|
|
5964
|
+
#listRuns() {
|
|
5965
|
+
const runs = [];
|
|
5966
|
+
const claimed = new Set();
|
|
5967
|
+
|
|
5968
|
+
for (const paragraph of this.doc.querySelectorAll("p[style*='mso-list']")) {
|
|
5969
|
+
if (claimed.has(paragraph) || this.#listLevel(paragraph) === null) continue
|
|
5970
|
+
|
|
5971
|
+
const run = [];
|
|
5972
|
+
let node = paragraph;
|
|
5973
|
+
while (node) {
|
|
5974
|
+
run.push(node);
|
|
5975
|
+
claimed.add(node);
|
|
5976
|
+
node = this.#nextListParagraph(node);
|
|
5977
|
+
}
|
|
5978
|
+
runs.push(run);
|
|
5979
|
+
}
|
|
5980
|
+
|
|
5981
|
+
return runs
|
|
5982
|
+
}
|
|
5983
|
+
|
|
5984
|
+
#nextListParagraph(paragraph) {
|
|
5985
|
+
let sibling = paragraph.nextSibling;
|
|
5986
|
+
while (sibling && this.#isIgnorableBetweenListParagraphs(sibling)) {
|
|
5987
|
+
sibling = sibling.nextSibling;
|
|
5988
|
+
}
|
|
5989
|
+
|
|
5990
|
+
if (sibling && sibling.nodeType === Node.ELEMENT_NODE && this.#listLevel(sibling) !== null) {
|
|
5991
|
+
return sibling
|
|
5992
|
+
}
|
|
5993
|
+
|
|
5994
|
+
return null
|
|
5995
|
+
}
|
|
5996
|
+
|
|
5997
|
+
#isIgnorableBetweenListParagraphs(node) {
|
|
5998
|
+
if (node.nodeType === Node.COMMENT_NODE) {
|
|
5999
|
+
return true
|
|
6000
|
+
}
|
|
6001
|
+
return node.nodeType === Node.TEXT_NODE && node.textContent.trim() === ""
|
|
6002
|
+
}
|
|
6003
|
+
|
|
6004
|
+
// Word numbers levels from 1, but a copied selection often starts partway down
|
|
6005
|
+
// a list, so every paragraph declares level3 with no level1 above it. Anchoring
|
|
6006
|
+
// the run at its own shallowest level is what keeps the result from arriving
|
|
6007
|
+
// needlessly deep.
|
|
6008
|
+
#replaceWithLists(paragraphs) {
|
|
6009
|
+
const levels = paragraphs.map((paragraph) => this.#listLevel(paragraph));
|
|
6010
|
+
const baseLevel = Math.min(...levels);
|
|
6011
|
+
const roots = [];
|
|
6012
|
+
const stack = [];
|
|
6013
|
+
|
|
6014
|
+
paragraphs.forEach((paragraph, index) => {
|
|
6015
|
+
const depth = levels[index] - baseLevel + 1;
|
|
6016
|
+
const tagName = this.#listTagName(paragraph);
|
|
6017
|
+
|
|
6018
|
+
while (stack.length > depth) {
|
|
6019
|
+
stack.pop();
|
|
6020
|
+
}
|
|
6021
|
+
if (stack.length === depth && stack[depth - 1].tagName !== tagName.toUpperCase()) {
|
|
6022
|
+
stack.pop();
|
|
6023
|
+
}
|
|
6024
|
+
|
|
6025
|
+
while (stack.length < depth) {
|
|
6026
|
+
const list = this.doc.createElement(tagName);
|
|
6027
|
+
if (stack.length === 0) {
|
|
6028
|
+
roots.push(list);
|
|
6029
|
+
} else {
|
|
6030
|
+
this.#lastItemOf(stack[stack.length - 1]).appendChild(list);
|
|
6031
|
+
}
|
|
6032
|
+
stack.push(list);
|
|
6033
|
+
}
|
|
6034
|
+
|
|
6035
|
+
stack[stack.length - 1].appendChild(this.#createListItem(paragraph));
|
|
6036
|
+
});
|
|
6037
|
+
|
|
6038
|
+
paragraphs[0].replaceWith(...roots);
|
|
6039
|
+
for (const paragraph of paragraphs.slice(1)) {
|
|
6040
|
+
paragraph.remove();
|
|
6041
|
+
}
|
|
6042
|
+
}
|
|
6043
|
+
|
|
6044
|
+
#lastItemOf(list) {
|
|
6045
|
+
if (!list.lastElementChild) {
|
|
6046
|
+
list.appendChild(this.doc.createElement("li"));
|
|
6047
|
+
}
|
|
6048
|
+
return list.lastElementChild
|
|
6049
|
+
}
|
|
6050
|
+
|
|
6051
|
+
// Word writes bullets as the raw glyph of the marker font (Symbol's "·",
|
|
6052
|
+
// Wingdings' "§", Courier New's "o") and numbers as the rendered counter
|
|
6053
|
+
// ("1.", "a)", "iv)"). A bare letter with no delimiter after it is Courier
|
|
6054
|
+
// New's bullet, not a counter, so the delimiter is what tells them apart.
|
|
6055
|
+
#listTagName(paragraph) {
|
|
6056
|
+
const marker = paragraph.querySelector("[style*='mso-list:Ignore']");
|
|
6057
|
+
const text = marker?.textContent || "";
|
|
6058
|
+
|
|
6059
|
+
if (/\d/.test(text) || /^\(?[A-Za-z]+[.)]/.test(text)) {
|
|
6060
|
+
return "ol"
|
|
6061
|
+
}
|
|
6062
|
+
return "ul"
|
|
6063
|
+
}
|
|
6064
|
+
|
|
6065
|
+
#createListItem(paragraph) {
|
|
6066
|
+
for (const marker of paragraph.querySelectorAll("[style*='mso-list:Ignore']")) {
|
|
6067
|
+
marker.remove();
|
|
6068
|
+
}
|
|
6069
|
+
|
|
6070
|
+
const item = this.doc.createElement("li");
|
|
6071
|
+
item.append(...paragraph.childNodes);
|
|
6072
|
+
return item
|
|
6073
|
+
}
|
|
6074
|
+
|
|
6075
|
+
#listLevel(element) {
|
|
6076
|
+
const match = /mso-list:[^;"']*\blevel(\d+)/.exec(element.getAttribute("style") || "");
|
|
6077
|
+
if (match) {
|
|
6078
|
+
return parseInt(match[1], 10)
|
|
6079
|
+
}
|
|
6080
|
+
return null
|
|
6081
|
+
}
|
|
6082
|
+
}
|
|
6083
|
+
|
|
5917
6084
|
class PastedContentFormatter {
|
|
5918
6085
|
constructor(doc) {
|
|
5919
6086
|
this.doc = doc;
|
|
@@ -5923,6 +6090,7 @@ class PastedContentFormatter {
|
|
|
5923
6090
|
this.#stripStyleElements();
|
|
5924
6091
|
this.#unwrapPlaceholderAnchors();
|
|
5925
6092
|
this.#stripTableCellColorStyles();
|
|
6093
|
+
new OfficeFormatter(this.doc).format();
|
|
5926
6094
|
this.#unwrapWrappedListChildren();
|
|
5927
6095
|
this.#nestStrayListChildren();
|
|
5928
6096
|
this.#stripStrayListChildren();
|
|
@@ -6081,6 +6249,20 @@ class Contents {
|
|
|
6081
6249
|
}, { tag });
|
|
6082
6250
|
}
|
|
6083
6251
|
|
|
6252
|
+
// A literal "\n" stays visible in the editor, which Lexical renders with white-space: pre-wrap,
|
|
6253
|
+
// but collapses to a space once serialized to HTML, so it has to become line break nodes.
|
|
6254
|
+
insertTextWithLineBreaks(text) {
|
|
6255
|
+
const normalizedText = text?.replace(/\r\n?/g, "\n");
|
|
6256
|
+
const selection = $getSelection();
|
|
6257
|
+
|
|
6258
|
+
if (normalizedText?.includes("\n") && $isRangeSelection(selection)) {
|
|
6259
|
+
selection.insertRawText(normalizedText);
|
|
6260
|
+
return true
|
|
6261
|
+
} else {
|
|
6262
|
+
return false
|
|
6263
|
+
}
|
|
6264
|
+
}
|
|
6265
|
+
|
|
6084
6266
|
insertAtCursor(...nodes) {
|
|
6085
6267
|
const selection = this.#insertableSelection();
|
|
6086
6268
|
const inserter = BaseNodeInserter.for(selection);
|
|
@@ -9072,6 +9254,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
9072
9254
|
#initialize() {
|
|
9073
9255
|
this.#registerComponents();
|
|
9074
9256
|
this.#handleEnter();
|
|
9257
|
+
this.#handleReplacementText();
|
|
9075
9258
|
this.#registerFocusEvents();
|
|
9076
9259
|
this.#registerHistoryEvents();
|
|
9077
9260
|
this.#registerFileAcceptFilter();
|
|
@@ -9335,6 +9518,14 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
9335
9518
|
));
|
|
9336
9519
|
}
|
|
9337
9520
|
|
|
9521
|
+
#handleReplacementText() {
|
|
9522
|
+
this.#listeners.track(this.editor.registerCommand(
|
|
9523
|
+
CONTROLLED_TEXT_INSERTION_COMMAND,
|
|
9524
|
+
(event) => event instanceof InputEvent && this.contents.insertTextWithLineBreaks(event.data),
|
|
9525
|
+
COMMAND_PRIORITY_NORMAL
|
|
9526
|
+
));
|
|
9527
|
+
}
|
|
9528
|
+
|
|
9338
9529
|
#registerFocusEvents() {
|
|
9339
9530
|
this.#listeners.track(
|
|
9340
9531
|
registerEventListener(this, "focusin", this.#handleFocusIn),
|