@37signals/lexxy 0.9.25 → 0.9.27
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/README.md +1 -0
- package/dist/lexxy.esm.js +80 -7
- package/dist/stylesheets/lexxy-editor.css +10 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ A modern rich text editor for Rails.
|
|
|
21
21
|
|
|
22
22
|
- Bug reports and pull requests are welcome on [GitHub Issues](https://github.com/basecamp/lexxy/issues). Help is especially welcome with [those tagged as "Help Wanted"](https://github.com/basecamp/lexxy/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22).
|
|
23
23
|
- For questions and general Lexxy discussion, please use the [Discussions section](https://github.com/basecamp/lexxy/discussions)
|
|
24
|
+
- To build with a development version of Lexical, use `bin/link-to-local-lexical`.
|
|
24
25
|
|
|
25
26
|
## License
|
|
26
27
|
|
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, 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
|
|
@@ -5923,6 +5947,7 @@ class PastedContentFormatter {
|
|
|
5923
5947
|
this.#stripStyleElements();
|
|
5924
5948
|
this.#unwrapPlaceholderAnchors();
|
|
5925
5949
|
this.#stripTableCellColorStyles();
|
|
5950
|
+
this.#unwrapWrappedListChildren();
|
|
5926
5951
|
this.#nestStrayListChildren();
|
|
5927
5952
|
this.#stripStrayListChildren();
|
|
5928
5953
|
return this.doc
|
|
@@ -5964,6 +5989,29 @@ class PastedContentFormatter {
|
|
|
5964
5989
|
}
|
|
5965
5990
|
}
|
|
5966
5991
|
|
|
5992
|
+
// Some sources wrap runs of <li>s in a stray element (e.g. a <div> directly
|
|
5993
|
+
// inside the list). Dissolve such wrappers so their items become direct
|
|
5994
|
+
// list children and any nested list they hide becomes visible to
|
|
5995
|
+
// #nestStrayListChildren below.
|
|
5996
|
+
#unwrapWrappedListChildren() {
|
|
5997
|
+
for (const list of this.doc.querySelectorAll("ol, ul")) {
|
|
5998
|
+
let wrapper = this.#wrappedListChild(list);
|
|
5999
|
+
while (wrapper) {
|
|
6000
|
+
wrapper.replaceWith(...wrapper.childNodes);
|
|
6001
|
+
wrapper = this.#wrappedListChild(list);
|
|
6002
|
+
}
|
|
6003
|
+
}
|
|
6004
|
+
}
|
|
6005
|
+
|
|
6006
|
+
#wrappedListChild(list) {
|
|
6007
|
+
for (const child of list.children) {
|
|
6008
|
+
if (child.tagName !== "LI" && !this.#isNestedList(child) && this.#containsListItems(child)) {
|
|
6009
|
+
return child
|
|
6010
|
+
}
|
|
6011
|
+
}
|
|
6012
|
+
return null
|
|
6013
|
+
}
|
|
6014
|
+
|
|
5967
6015
|
// Some sources (e.g. Gmail) nest a sublist as a direct child of the parent
|
|
5968
6016
|
// <ol>/<ul> instead of inside a <li>. Move each nested list into its
|
|
5969
6017
|
// preceding <li> so the import preserves the nesting instead of dropping it.
|
|
@@ -5980,16 +6028,41 @@ class PastedContentFormatter {
|
|
|
5980
6028
|
}
|
|
5981
6029
|
}
|
|
5982
6030
|
|
|
5983
|
-
// Only <li> is a valid child of a list
|
|
5984
|
-
//
|
|
6031
|
+
// Only <li> is a valid child of a list. Unwrap remaining stray children
|
|
6032
|
+
// that still hold list items (a nested list with no preceding <li> to nest
|
|
6033
|
+
// under) so the items survive, and drop stray <br>/whitespace so the import
|
|
6034
|
+
// doesn't wrap them into an empty leading item.
|
|
5985
6035
|
#stripStrayListChildren() {
|
|
5986
6036
|
for (const list of this.doc.querySelectorAll("ol, ul")) {
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
6037
|
+
let stray = this.#firstStrayListChild(list);
|
|
6038
|
+
while (stray) {
|
|
6039
|
+
if (this.#containsListItems(stray)) {
|
|
6040
|
+
stray.replaceWith(...stray.childNodes);
|
|
6041
|
+
} else {
|
|
6042
|
+
stray.remove();
|
|
6043
|
+
}
|
|
6044
|
+
|
|
6045
|
+
stray = this.#firstStrayListChild(list);
|
|
5990
6046
|
}
|
|
5991
6047
|
}
|
|
5992
6048
|
}
|
|
6049
|
+
|
|
6050
|
+
#firstStrayListChild(list) {
|
|
6051
|
+
for (const child of list.childNodes) {
|
|
6052
|
+
if (child.nodeType !== Node.ELEMENT_NODE || child.tagName !== "LI") {
|
|
6053
|
+
return child
|
|
6054
|
+
}
|
|
6055
|
+
}
|
|
6056
|
+
return null
|
|
6057
|
+
}
|
|
6058
|
+
|
|
6059
|
+
#isNestedList(node) {
|
|
6060
|
+
return node.nodeType === Node.ELEMENT_NODE && (node.tagName === "OL" || node.tagName === "UL")
|
|
6061
|
+
}
|
|
6062
|
+
|
|
6063
|
+
#containsListItems(node) {
|
|
6064
|
+
return node.nodeType === Node.ELEMENT_NODE && node.querySelector("li") !== null
|
|
6065
|
+
}
|
|
5993
6066
|
}
|
|
5994
6067
|
|
|
5995
6068
|
class Contents {
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
}
|
|
10
10
|
--lexxy-toolbar-gap: 2px;
|
|
11
11
|
--lexxy-toolbar-spacing: 0.5ch;
|
|
12
|
+
--lexxy-toolbar-padding: 2px;
|
|
13
|
+
--lexxy-toolbar-border-size: 1px;
|
|
14
|
+
--lexxy-toolbar-height: calc(var(--lexxy-toolbar-button-size) + 2 * var(--lexxy-toolbar-padding) + var(--lexxy-toolbar-border-size));
|
|
12
15
|
|
|
13
16
|
border: 1px solid var(--lexxy-color-ink-lighter);
|
|
14
17
|
border-radius: calc(var(--lexxy-radius) + var(--lexxy-toolbar-gap));
|
|
@@ -408,6 +411,11 @@
|
|
|
408
411
|
}
|
|
409
412
|
}
|
|
410
413
|
|
|
414
|
+
/* Reserve the mounted height before upgrade so it doesn't shift layout on mount. */
|
|
415
|
+
:where(lexxy-editor:not(:defined)) {
|
|
416
|
+
min-block-size: calc(var(--lexxy-toolbar-height) + var(--lexxy-editor-rows) + 2 * var(--lexxy-editor-padding));
|
|
417
|
+
}
|
|
418
|
+
|
|
411
419
|
/* Placeholder */
|
|
412
420
|
:where(.lexxy-editor--empty) {
|
|
413
421
|
.lexxy-editor__content:not(:has(h1, h2, h3, h4, h5, h6, table, ul, ol, figure, .attachment-gallery))::before {
|
|
@@ -461,13 +469,13 @@
|
|
|
461
469
|
:where(lexxy-toolbar) {
|
|
462
470
|
--lexxy-toolbar-icon-size: 1.125em;
|
|
463
471
|
|
|
464
|
-
border-block-end:
|
|
472
|
+
border-block-end: var(--lexxy-toolbar-border-size) solid var(--lexxy-color-ink-lighter);
|
|
465
473
|
color: currentColor;
|
|
466
474
|
display: flex;
|
|
467
475
|
font-size: inherit;
|
|
468
476
|
gap: var(--lexxy-toolbar-gap);
|
|
469
477
|
max-inline-size: 100%;
|
|
470
|
-
padding:
|
|
478
|
+
padding: var(--lexxy-toolbar-padding);
|
|
471
479
|
position: relative;
|
|
472
480
|
|
|
473
481
|
@container (width < 300px) {
|