@37signals/lexxy 0.9.8-beta → 0.9.9-beta
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 +147 -48
- package/dist/lexxy_helpers.esm.js +5 -1
- package/package.json +1 -1
package/dist/lexxy.esm.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { createElement, extractPlainTextFromHtml, createAttachmentFigure, isPreviewableImage, dispatch, parseHtml, addBlockSpacing, generateDomId } from './lexxy_helpers.esm.js';
|
|
1
|
+
import { isActiveAndVisible, createElement, extractPlainTextFromHtml, createAttachmentFigure, isPreviewableImage, dispatch, parseHtml, addBlockSpacing, generateDomId } from './lexxy_helpers.esm.js';
|
|
2
2
|
export { highlightCode } from './lexxy_helpers.esm.js';
|
|
3
3
|
import DOMPurify from 'dompurify';
|
|
4
4
|
import { getStyleObjectFromCSS, getCSSFromStyleObject, $isAtNodeEnd, $getSelectionStyleValueForProperty, $patchStyleText, $setBlocksType, $forEachSelectedTextNode, $ensureForwardRangeSelection } from '@lexical/selection';
|
|
5
5
|
import { SKIP_DOM_SELECTION_TAG, $getSelection, $isRangeSelection, DecoratorNode, $createParagraphNode, $getNodeByKey, $isTextNode, $createRangeSelection, $setSelection, $createTextNode, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $createNodeSelection, $isDecoratorNode, $isLineBreakNode, $isElementNode, TextNode, createCommand, createState, defineExtension, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, KEY_ARROW_RIGHT_COMMAND, KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $getEditor, $getNearestRootOrShadowRoot, $isNodeSelection, $getRoot, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, DELETE_CHARACTER_COMMAND, SELECTION_CHANGE_COMMAND, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, ParagraphNode, $isRootOrShadowRoot, ElementNode, $splitNode, $isParagraphNode, $createLineBreakNode, $isRootNode, $getChildCaretAtIndex, RootNode, COMMAND_PRIORITY_HIGH, DRAGSTART_COMMAND, DROP_COMMAND, INSERT_PARAGRAPH_COMMAND, CLEAR_HISTORY_COMMAND, $addUpdateTag, KEY_ENTER_COMMAND, COMMAND_PRIORITY_CRITICAL, KEY_SPACE_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
|
|
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 { $createAutoLinkNode, $toggleLink,
|
|
9
|
-
import { $getNearestNodeOfType, $wrapNodeInElement, $lastToFirstIterator, mergeRegister, $insertFirst, $unwrapAndFilterDescendants, $firstToLastIterator, $getNearestBlockElementAncestorOrThrow, $descendantsMatching } from '@lexical/utils';
|
|
8
|
+
import { LinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, AutoLinkNode, $isLinkNode } from '@lexical/link';
|
|
9
|
+
import { $getNearestNodeOfType, $wrapNodeInElement, $lastToFirstIterator, mergeRegister, $insertFirst, $unwrapAndFilterDescendants, $firstToLastIterator, $getNearestBlockElementAncestorOrThrow, $descendantsMatching, IS_APPLE } from '@lexical/utils';
|
|
10
10
|
import { registerPlainText } from '@lexical/plain-text';
|
|
11
11
|
import { RichTextExtension, $isQuoteNode, $isHeadingNode, $createHeadingNode, $createQuoteNode, QuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
|
|
12
12
|
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
@@ -209,10 +209,6 @@ class NextElementFinder {
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
function isActiveAndVisible(element) {
|
|
213
|
-
return element && !element.disabled && element.checkVisibility()
|
|
214
|
-
}
|
|
215
|
-
|
|
216
212
|
var ToolbarIcons = {
|
|
217
213
|
"bold":
|
|
218
214
|
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
@@ -486,7 +482,8 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
486
482
|
}
|
|
487
483
|
|
|
488
484
|
#handleEditorFocus = () => {
|
|
489
|
-
this.#focusableItems
|
|
485
|
+
const firstVisible = this.#focusableItems.find(isActiveAndVisible);
|
|
486
|
+
if (firstVisible) firstVisible.tabIndex = 0;
|
|
490
487
|
}
|
|
491
488
|
|
|
492
489
|
#handleEditorBlur = () => {
|
|
@@ -579,12 +576,6 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
579
576
|
}
|
|
580
577
|
}
|
|
581
578
|
|
|
582
|
-
#toolbarIsOverflowing() {
|
|
583
|
-
// Safari can report inconsistent clientWidth values on more than 100% window zoom level,
|
|
584
|
-
// that was affecting the toolbar overflow calculation. We're adding +1 to get around this issue.
|
|
585
|
-
return (this.scrollWidth - this.#overflow.clientWidth) > this.clientWidth + 1
|
|
586
|
-
}
|
|
587
|
-
|
|
588
579
|
#refreshToolbarOverflow = () => {
|
|
589
580
|
this.#resetToolbarOverflow();
|
|
590
581
|
this.#compactMenu();
|
|
@@ -597,29 +588,46 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
597
588
|
this.#overflowMenu.toggleAttribute("disabled", !isOverflowing);
|
|
598
589
|
}
|
|
599
590
|
|
|
591
|
+
// Separates layout reads from DOM writes to avoid forced reflows during init.
|
|
592
|
+
// Measures every button's right edge in a single read pass, figures out which
|
|
593
|
+
// buttons overflow using math, and then moves them in a single write pass.
|
|
594
|
+
// The previous implementation interleaved `scrollWidth`/`clientWidth` reads with
|
|
595
|
+
// `prepend()` writes inside a loop, forcing one full browser reflow per button.
|
|
600
596
|
#compactMenu() {
|
|
601
|
-
const buttons = this.#buttons
|
|
602
|
-
|
|
597
|
+
const buttons = this.#buttons;
|
|
598
|
+
if (buttons.length === 0) return
|
|
603
599
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
600
|
+
const availableWidth = this.clientWidth + 1; // +1 for Safari zoom rounding
|
|
601
|
+
const buttonRightEdges = buttons.map(button => button.offsetLeft + button.offsetWidth);
|
|
602
|
+
|
|
603
|
+
let firstOverflowing = -1;
|
|
604
|
+
for (let i = 0; i < buttons.length; i++) {
|
|
605
|
+
if (buttonRightEdges[i] > availableWidth) {
|
|
606
|
+
firstOverflowing = i;
|
|
610
607
|
break
|
|
611
608
|
}
|
|
612
609
|
}
|
|
610
|
+
|
|
611
|
+
if (firstOverflowing === -1) return
|
|
612
|
+
|
|
613
|
+
// Move one extra button to reserve space for the overflow control, which is
|
|
614
|
+
// `display: none` until we show it — matching the previous implementation's
|
|
615
|
+
// "move one more after it stops overflowing" behaviour.
|
|
616
|
+
const overflowIndex = Math.max(0, firstOverflowing - 1);
|
|
617
|
+
const overflowButtons = buttons.slice(overflowIndex).reverse();
|
|
618
|
+
for (const button of overflowButtons) {
|
|
619
|
+
this.#overflowMenu.prepend(button);
|
|
620
|
+
}
|
|
613
621
|
}
|
|
614
622
|
|
|
615
623
|
#resetToolbarOverflow() {
|
|
616
624
|
const items = Array.from(this.#overflowMenu.children);
|
|
617
625
|
items.sort((a, b) => this.#itemPosition(b) - this.#itemPosition(a));
|
|
618
626
|
|
|
619
|
-
|
|
627
|
+
for (const item of items) {
|
|
620
628
|
const nextItem = this.querySelector(`[data-position="${this.#itemPosition(item) + 1}"]`) ?? this.#overflow;
|
|
621
629
|
this.insertBefore(item, nextItem);
|
|
622
|
-
}
|
|
630
|
+
}
|
|
623
631
|
}
|
|
624
632
|
|
|
625
633
|
#itemPosition(item) {
|
|
@@ -1525,26 +1533,38 @@ class StyleCanonicalizer {
|
|
|
1525
1533
|
|
|
1526
1534
|
#resolveCannonicalValue(value) {
|
|
1527
1535
|
let index = this.#computedAllowedValues.indexOf(value);
|
|
1528
|
-
index
|
|
1536
|
+
if (index === -1) {
|
|
1537
|
+
index = this.#computedAllowedValues.indexOf(computeStyleValues(this._property, [ value ])[0]);
|
|
1538
|
+
}
|
|
1529
1539
|
return index === -1 ? null : this._allowedValues[index]
|
|
1530
1540
|
}
|
|
1531
1541
|
|
|
1532
1542
|
get #computedAllowedValues() {
|
|
1533
|
-
return this._computedAllowedValues ||= this._allowedValues
|
|
1534
|
-
value => getComputedStyleForProperty(this._property, value)
|
|
1535
|
-
)
|
|
1543
|
+
return this._computedAllowedValues ||= computeStyleValues(this._property, this._allowedValues)
|
|
1536
1544
|
}
|
|
1537
1545
|
}
|
|
1538
1546
|
|
|
1539
|
-
|
|
1540
|
-
|
|
1547
|
+
// Separates DOM writes from layout reads to avoid forced reflows. All resolver
|
|
1548
|
+
// elements are built inside a fragment, attached once, then read in a single pass.
|
|
1549
|
+
// Reading `getComputedStyle` after a write forces the browser to recompute layout,
|
|
1550
|
+
// so interleaving writes and reads inside a loop turns one reflow into N.
|
|
1551
|
+
function computeStyleValues(property, values) {
|
|
1552
|
+
const fragment = document.createDocumentFragment();
|
|
1553
|
+
|
|
1554
|
+
const elements = values.map(value => {
|
|
1555
|
+
const element = createElement("span", { style: `display: none; ${property}: ${value};` });
|
|
1556
|
+
fragment.appendChild(element);
|
|
1557
|
+
return element
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
document.body.appendChild(fragment);
|
|
1541
1561
|
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
element.remove();
|
|
1562
|
+
const computed = elements.map(element =>
|
|
1563
|
+
window.getComputedStyle(element).getPropertyValue(property)
|
|
1564
|
+
);
|
|
1546
1565
|
|
|
1547
|
-
|
|
1566
|
+
elements.forEach(element => element.remove());
|
|
1567
|
+
return computed
|
|
1548
1568
|
}
|
|
1549
1569
|
|
|
1550
1570
|
class LexxyExtension {
|
|
@@ -2162,7 +2182,9 @@ class CommandDispatcher {
|
|
|
2162
2182
|
const selection = $getSelection();
|
|
2163
2183
|
if (!$isRangeSelection(selection)) return
|
|
2164
2184
|
|
|
2165
|
-
|
|
2185
|
+
const anchorNode = selection.anchor.getNode();
|
|
2186
|
+
|
|
2187
|
+
if (selection.isCollapsed() && !$getNearestNodeOfType(anchorNode, LinkNode)) {
|
|
2166
2188
|
const autoLinkNode = $createAutoLinkNode(url);
|
|
2167
2189
|
const textNode = $createTextNode(url);
|
|
2168
2190
|
autoLinkNode.append(textNode);
|
|
@@ -6478,6 +6500,10 @@ class FormatEscapeExtension extends LexxyExtension {
|
|
|
6478
6500
|
return this.editorElement.supportsRichText
|
|
6479
6501
|
}
|
|
6480
6502
|
|
|
6503
|
+
get allowedElements() {
|
|
6504
|
+
return [ { tag: "li", attributes: [ "value" ] } ]
|
|
6505
|
+
}
|
|
6506
|
+
|
|
6481
6507
|
get lexicalExtension() {
|
|
6482
6508
|
return defineExtension({
|
|
6483
6509
|
name: "lexxy/format-escape",
|
|
@@ -6551,6 +6577,65 @@ function $handleArrowDownInCodeBlock(event) {
|
|
|
6551
6577
|
return false
|
|
6552
6578
|
}
|
|
6553
6579
|
|
|
6580
|
+
class LinkOpenerExtension extends LexxyExtension {
|
|
6581
|
+
get enabled() {
|
|
6582
|
+
return this.editorElement.supportsRichText
|
|
6583
|
+
}
|
|
6584
|
+
|
|
6585
|
+
get lexicalExtension() {
|
|
6586
|
+
return defineExtension({
|
|
6587
|
+
name: "lexxy/link-opener",
|
|
6588
|
+
register: () => {
|
|
6589
|
+
return mergeRegister(
|
|
6590
|
+
registerEventListener(window, "keydown", this.#update.bind(this)),
|
|
6591
|
+
registerEventListener(window, "keyup", this.#update.bind(this)),
|
|
6592
|
+
registerEventListener(window, "blur", this.#disable.bind(this)),
|
|
6593
|
+
registerEventListener(window, "focus", this.#refresh.bind(this))
|
|
6594
|
+
)
|
|
6595
|
+
}
|
|
6596
|
+
})
|
|
6597
|
+
}
|
|
6598
|
+
|
|
6599
|
+
#update(event) {
|
|
6600
|
+
if (this.#isModified(event)) {
|
|
6601
|
+
this.#enable();
|
|
6602
|
+
} else {
|
|
6603
|
+
this.#disable();
|
|
6604
|
+
}
|
|
6605
|
+
}
|
|
6606
|
+
|
|
6607
|
+
#refresh() {
|
|
6608
|
+
// Chrome dispatches events without modifier keys *for a while* after changing tabs
|
|
6609
|
+
setTimeout(() => {
|
|
6610
|
+
window.addEventListener("mousemove", this.#update.bind(this), { once: true });
|
|
6611
|
+
}, 200);
|
|
6612
|
+
}
|
|
6613
|
+
|
|
6614
|
+
#isModified(event) {
|
|
6615
|
+
return IS_APPLE ? event.metaKey : event.ctrlKey
|
|
6616
|
+
}
|
|
6617
|
+
|
|
6618
|
+
#enable() {
|
|
6619
|
+
for (const anchor of this.#anchors) {
|
|
6620
|
+
anchor.setAttribute("contenteditable", "false");
|
|
6621
|
+
anchor.setAttribute("target", "_blank");
|
|
6622
|
+
anchor.setAttribute("rel", "noopener noreferrer");
|
|
6623
|
+
}
|
|
6624
|
+
}
|
|
6625
|
+
|
|
6626
|
+
#disable() {
|
|
6627
|
+
for (const anchor of this.#anchors) {
|
|
6628
|
+
anchor.removeAttribute("contenteditable");
|
|
6629
|
+
anchor.removeAttribute("target");
|
|
6630
|
+
anchor.removeAttribute("rel");
|
|
6631
|
+
}
|
|
6632
|
+
}
|
|
6633
|
+
|
|
6634
|
+
get #anchors() {
|
|
6635
|
+
return this.editorElement.editorContentElement?.querySelectorAll("a") ?? []
|
|
6636
|
+
}
|
|
6637
|
+
}
|
|
6638
|
+
|
|
6554
6639
|
class LexicalEditorElement extends HTMLElement {
|
|
6555
6640
|
static formAssociated = true
|
|
6556
6641
|
static debug = false
|
|
@@ -6655,7 +6740,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6655
6740
|
TrixContentExtension,
|
|
6656
6741
|
TablesExtension,
|
|
6657
6742
|
AttachmentsExtension,
|
|
6658
|
-
FormatEscapeExtension
|
|
6743
|
+
FormatEscapeExtension,
|
|
6744
|
+
LinkOpenerExtension
|
|
6659
6745
|
]
|
|
6660
6746
|
}
|
|
6661
6747
|
|
|
@@ -6905,7 +6991,9 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6905
6991
|
}
|
|
6906
6992
|
|
|
6907
6993
|
#handleTurboBeforeCache = (event) => {
|
|
6908
|
-
this
|
|
6994
|
+
if (!this.closest("[data-turbo-permanent]")) {
|
|
6995
|
+
this.#reset();
|
|
6996
|
+
}
|
|
6909
6997
|
}
|
|
6910
6998
|
|
|
6911
6999
|
#synchronizeWithChanges() {
|
|
@@ -7178,19 +7266,30 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7178
7266
|
]
|
|
7179
7267
|
}
|
|
7180
7268
|
|
|
7269
|
+
// Builds one resolver element per CSS value inside a hidden container, attaches
|
|
7270
|
+
// the container in a single DOM write, then reads all computed values in one pass
|
|
7271
|
+
// — triggering at most one forced reflow. The previous implementation interleaved
|
|
7272
|
+
// setProperty/getComputedStyle/removeProperty on the same element, forcing a style
|
|
7273
|
+
// recalc on every iteration during editor initialization.
|
|
7181
7274
|
#resolveColors(property, cssValues) {
|
|
7182
|
-
const
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
return { name: cssValue, value }
|
|
7275
|
+
const container = document.createElement("span");
|
|
7276
|
+
container.style.display = "none";
|
|
7277
|
+
|
|
7278
|
+
const resolvers = cssValues.map(cssValue => {
|
|
7279
|
+
const element = document.createElement("span");
|
|
7280
|
+
element.style.setProperty(property, cssValue);
|
|
7281
|
+
container.appendChild(element);
|
|
7282
|
+
return { element, name: cssValue }
|
|
7191
7283
|
});
|
|
7192
7284
|
|
|
7193
|
-
|
|
7285
|
+
this.appendChild(container);
|
|
7286
|
+
|
|
7287
|
+
const resolved = resolvers.map(({ element, name }) => ({
|
|
7288
|
+
name,
|
|
7289
|
+
value: window.getComputedStyle(element).getPropertyValue(property)
|
|
7290
|
+
}));
|
|
7291
|
+
|
|
7292
|
+
container.remove();
|
|
7194
7293
|
return resolved
|
|
7195
7294
|
}
|
|
7196
7295
|
|
|
@@ -82,6 +82,10 @@ function extractPlainTextFromHtml(innerHtml = "") {
|
|
|
82
82
|
return parseHtml(innerHtml).body.textContent.trim()
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
function isActiveAndVisible(element) {
|
|
86
|
+
return element && !element.disabled && element.checkVisibility()
|
|
87
|
+
}
|
|
88
|
+
|
|
85
89
|
function highlightCode() {
|
|
86
90
|
const elements = document.querySelectorAll("pre[data-language]");
|
|
87
91
|
|
|
@@ -216,4 +220,4 @@ function collectTextNodes(root) {
|
|
|
216
220
|
return nodes
|
|
217
221
|
}
|
|
218
222
|
|
|
219
|
-
export { addBlockSpacing, createAttachmentFigure, createElement, dispatch, extractPlainTextFromHtml, generateDomId, highlightCode, isPreviewableImage, parseHtml };
|
|
223
|
+
export { addBlockSpacing, createAttachmentFigure, createElement, dispatch, extractPlainTextFromHtml, generateDomId, highlightCode, isActiveAndVisible, isPreviewableImage, parseHtml };
|