@37signals/lexxy 0.9.23 → 0.9.24
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 -1
- package/dist/lexxy.esm.js +286 -154
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A modern rich text editor for Rails.
|
|
4
4
|
|
|
5
|
-
[Documentation](https://
|
|
5
|
+
[Documentation](https://lexxy.dev/docs/) | <mark>[Try it out!](https://lexxy.dev/sandbox/)</mark>
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
package/dist/lexxy.esm.js
CHANGED
|
@@ -12,7 +12,7 @@ import { registerPlainText } from '@lexical/plain-text';
|
|
|
12
12
|
import { RichTextExtension, $isQuoteNode, QuoteNode, $isHeadingNode, $createHeadingNode, $createQuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
|
|
13
13
|
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
14
14
|
import { HistoryExtension } from '@lexical/history';
|
|
15
|
-
import { $isCodeNode, CodeHighlightNode,
|
|
15
|
+
import { $isCodeNode, CodeHighlightNode, PrismTokenizer, $isCodeHighlightNode, $createCodeNode, $createCodeHighlightNode, CodeNode, registerCodeHighlighting, normalizeCodeLang, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
|
|
16
16
|
import { TRANSFORMERS, registerMarkdownShortcuts } from '@lexical/markdown';
|
|
17
17
|
import { INSERT_TABLE_COMMAND, $getTableCellNodeFromLexicalNode, TableCellNode, TableNode, TableRowNode, setScrollableTablesActive, registerTablePlugin, registerTableSelectionObserver, TableCellHeaderStates, $insertTableRowAtSelection, $insertTableColumnAtSelection, $deleteTableRowAtSelection, $deleteTableColumnAtSelection, $findTableNode, $getTableRowIndexFromTableCellNode, $getTableColumnIndexFromTableCellNode, $findCellNode, $getElementForTableNode } from '@lexical/table';
|
|
18
18
|
import { marked } from 'marked';
|
|
@@ -318,6 +318,11 @@ var ToolbarIcons = {
|
|
|
318
318
|
<path d="M11.5 2C12.0523 2 12.5 2.44772 12.5 3V3.5C12.5 4.05228 12.0523 4.5 11.5 4.5H8V15C8 15.5523 7.55228 16 7 16H6.5C5.94772 16 5.5 15.5523 5.5 15V4.5H2C1.44772 4.5 1 4.05228 1 3.5V3C1 2.44772 1.44772 2 2 2H11.5ZM16 7C16.5523 7 17 7.44772 17 8V8.5C17 9.05228 16.5523 9.5 16 9.5H15V15C15 15.5523 14.5523 16 14 16H13.5C12.9477 16 12.5 15.5523 12.5 15V9.5H11.5C10.9477 9.5 10.5 9.05228 10.5 8.5V8C10.5 7.44772 10.9477 7 11.5 7H16Z"/>
|
|
319
319
|
</svg>`,
|
|
320
320
|
|
|
321
|
+
"h1":
|
|
322
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
323
|
+
<path d="M8.12207 4.30078C8.84668 4.30078 9.25684 4.74512 9.25684 5.51758V12.5518C9.25677 13.3241 8.84662 13.7686 8.12207 13.7686C7.39752 13.7686 6.9942 13.3309 6.99414 12.5518V9.87207H3.56934V12.5518C3.56927 13.3241 3.15912 13.7686 2.43457 13.7686C1.71002 13.7686 1.3067 13.3309 1.30664 12.5518V5.51758C1.30664 4.73828 1.70996 4.30078 2.43457 4.30078C3.15918 4.30078 3.56934 4.74512 3.56934 5.51758V8.07422H6.99414V5.51758C6.99414 4.73828 7.39746 4.30078 8.12207 4.30078ZM13.8418 4.125H14.8271C15.543 4.125 16.0449 4.56934 16.0449 5.23828V12.7207C16.0449 13.4111 15.6348 13.8486 14.9854 13.8486C14.336 13.8486 13.9395 13.418 13.9395 12.7207V6.11133H13.8887L12.9541 6.74219C12.7354 6.88574 12.5303 6.95312 12.3047 6.95312C11.7646 6.95312 11.3955 6.58398 11.3955 6.06445C11.3955 5.69531 11.5801 5.42578 11.9766 5.15625L13.4023 4.32031C13.5459 4.23633 13.6826 4.17578 13.8418 4.125Z"/>
|
|
324
|
+
</svg>`,
|
|
325
|
+
|
|
321
326
|
"h2":
|
|
322
327
|
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
323
328
|
<path d="M8.12207 4.30078C8.84668 4.30078 9.25684 4.74512 9.25684 5.51758V12.5518C9.25677 13.3241 8.84662 13.7686 8.12207 13.7686C7.39752 13.7686 6.9942 13.3309 6.99414 12.5518V9.87207H3.56934V12.5518C3.56927 13.3241 3.15912 13.7686 2.43457 13.7686C1.71002 13.7686 1.3067 13.3309 1.30664 12.5518V5.51758C1.30664 4.73828 1.70996 4.30078 2.43457 4.30078C3.15918 4.30078 3.56934 4.74512 3.56934 5.51758V8.07422H6.99414V5.51758C6.99414 4.73828 7.39746 4.30078 8.12207 4.30078ZM13.6445 4.19824C15.5244 4.19824 16.8984 5.34668 16.8984 6.91211C16.8984 7.8759 16.4335 8.7237 15.292 9.84473L13.3438 11.8135V11.9092H16.1875C16.8232 11.9092 17.2197 12.251 17.2197 12.8115C17.2196 13.3651 16.83 13.7002 16.1875 13.7002H11.5117C10.8487 13.7002 10.4112 13.3241 10.4111 12.75C10.4111 12.3399 10.6368 11.9843 11.3203 11.3145L13.6855 8.88086C14.4169 8.13583 14.7245 7.64349 14.7246 7.12402C14.7246 6.4541 14.2393 6.00293 13.5215 6.00293C12.9404 6.00293 12.5166 6.29688 12.2158 6.90527C11.9151 7.37002 11.6552 7.54785 11.2588 7.54785C10.7188 7.54785 10.3429 7.17861 10.3428 6.65918C10.3428 5.3877 11.7783 4.19824 13.6445 4.19824Z"/>
|
|
@@ -333,6 +338,16 @@ var ToolbarIcons = {
|
|
|
333
338
|
<path d="M14.6357 4.22559C15.7432 4.22559 16.4336 4.80664 16.4336 5.73633V10.3164H16.7275C17.2881 10.3164 17.6436 10.6787 17.6436 11.2256C17.6435 11.7655 17.3017 12.1006 16.7275 12.1006H16.4336V12.6611C16.4335 13.3515 16.0234 13.7891 15.374 13.7891C14.7247 13.7891 14.3282 13.3583 14.3281 12.6611V12.1006H11.04C10.2335 12.1006 9.76863 11.6766 9.76855 10.918C9.76855 10.5762 9.85064 10.3026 10.1104 9.74219C10.7666 8.42289 11.5733 7.0146 12.5713 5.54492C13.2549 4.56738 13.7812 4.22559 14.6357 4.22559ZM7.88965 4.30078C8.61426 4.30078 9.02441 4.74512 9.02441 5.51758V12.5518C9.02435 13.3241 8.6142 13.7686 7.88965 13.7686C7.1651 13.7686 6.76178 13.3309 6.76172 12.5518V9.87207H3.33691V12.5518C3.33685 13.3241 2.9267 13.7686 2.20215 13.7686C1.4776 13.7686 1.07428 13.3309 1.07422 12.5518V5.51758C1.07422 4.73828 1.47754 4.30078 2.20215 4.30078C2.92676 4.30078 3.33691 4.74512 3.33691 5.51758V8.07422H6.76172V5.51758C6.76172 4.73828 7.16504 4.30078 7.88965 4.30078ZM14.2188 6.07812C13.6035 7.02841 12.2158 9.48929 11.7988 10.2686V10.3164H14.3281V6.07812H14.2188Z"/>
|
|
334
339
|
</svg>`,
|
|
335
340
|
|
|
341
|
+
"h5":
|
|
342
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
343
|
+
<path d="M8.12207 4.30078C8.84668 4.30078 9.25684 4.74512 9.25684 5.51758V12.5518C9.25677 13.3241 8.84662 13.7686 8.12207 13.7686C7.39752 13.7686 6.9942 13.3309 6.99414 12.5518V9.87207H3.56934V12.5518C3.56927 13.3241 3.15912 13.7686 2.43457 13.7686C1.71002 13.7686 1.3067 13.3309 1.30664 12.5518V5.51758C1.30664 4.73828 1.70996 4.30078 2.43457 4.30078C3.15918 4.30078 3.56934 4.74512 3.56934 5.51758V8.07422H6.99414V5.51758C6.99414 4.73828 7.39746 4.30078 8.12207 4.30078ZM16.1191 4.30078C16.6934 4.30078 17.0762 4.63672 17.0762 5.16602C17.0762 5.69531 16.6934 6.03125 16.1191 6.03125H13.3457L12.9219 8.25586H13.0176C13.248 8.03711 13.6855 7.84375 14.2871 7.84375C15.9277 7.84375 17.1445 9.07422 17.1445 10.7832C17.1445 12.5605 15.8184 13.9053 13.7695 13.9053C11.9512 13.9053 10.6934 12.9277 10.5566 11.7178C10.5566 11.2021 10.9121 10.8193 11.4277 10.8193C11.8379 10.8193 12.1328 11.0244 12.3516 11.4893C12.625 12.0361 13.1133 12.2549 13.7285 12.2549C14.5488 12.2549 15.1367 11.6533 15.1367 10.7695C15.1367 9.92188 14.5625 9.31348 13.7559 9.31348C13.1953 9.31348 12.7578 9.56055 12.4629 10.0254C12.2168 10.3887 11.9355 10.5527 11.5527 10.5527C10.9648 10.5527 10.5684 10.1699 10.5684 9.55469C10.5684 9.42773 10.582 9.30762 10.6094 9.17383L11.2383 5.55273C11.3516 4.88379 11.7207 4.30078 12.6797 4.30078H16.1191Z"/>
|
|
344
|
+
</svg>`,
|
|
345
|
+
|
|
346
|
+
"h6":
|
|
347
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
348
|
+
<path d="M8.12207 4.30078C8.84668 4.30078 9.25684 4.74512 9.25684 5.51758V12.5518C9.25677 13.3241 8.84662 13.7686 8.12207 13.7686C7.39752 13.7686 6.9942 13.3309 6.99414 12.5518V9.87207H3.56934V12.5518C3.56927 13.3241 3.15912 13.7686 2.43457 13.7686C1.71002 13.7686 1.3067 13.3309 1.30664 12.5518V5.51758C1.30664 4.73828 1.70996 4.30078 2.43457 4.30078C3.15918 4.30078 3.56934 4.74512 3.56934 5.51758V8.07422H6.99414V5.51758C6.99414 4.73828 7.39746 4.30078 8.12207 4.30078ZM13.6719 13.9258C15.5518 13.9258 17.1445 12.37306 17.1445 10.35451V10.05763C17.1445 8.10744 16.6016 6.59574 15.5928 5.58694C14.8613 4.85544 13.8975 4.42384 12.7285 4.25294C12.4199 4.21194 12.1592 4.19824 11.8643 4.19824C11.2627 4.19824 10.8389 4.59474 10.8389 5.14164C10.8389 5.65724 11.2354 6.02054 11.7783 6.04784C12.1758 6.06834 12.4844 6.10944 12.7656 6.19144C13.5176 6.42384 14.0742 6.95314 14.3965 7.75974L14.4922 8.00584H14.3828C14.0605 7.56834 13.4863 7.25974 12.6523 7.25974C11.1406 7.25974 10.0674 8.42384 10.0674 10.02345V10.27736C10.0674 12.2549 11.4668 13.9258 13.6719 13.9258ZM13.5215 12.24123C12.6797 12.24123 12.0918 11.48244 12.0918 10.4131V10.1592C12.0918 9.26174 12.6523 8.71291 13.5352 8.71291C14.3965 8.71291 14.9844 9.27638 14.9844 10.1592V10.4131C14.9844 11.48244 14.3828 12.24123 13.5215 12.24123Z"/>
|
|
349
|
+
</svg>`,
|
|
350
|
+
|
|
336
351
|
"paragraph":
|
|
337
352
|
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
338
353
|
<path d="M9 12C9.55228 12 10 12.4477 10 13C10 13.5523 9.55228 14 9 14H3C2.44772 14 2 13.5523 2 13C2 12.4477 2.44772 12 3 12H9ZM15 8C15.5523 8 16 8.44772 16 9C16 9.55228 15.5523 10 15 10H3C2.44772 10 2 9.55228 2 9C2 8.44772 2.44772 8 3 8H15ZM15 4C15.5523 4 16 4.44772 16 5C16 5.55228 15.5523 6 15 6H3C2.44772 6 2 5.55228 2 5C2 4.44772 2.44772 4 3 4H15Z"/>
|
|
@@ -634,9 +649,9 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
634
649
|
|
|
635
650
|
this.#setButtonPressed("format", isInHeading);
|
|
636
651
|
this.#setButtonPressed("paragraph", !isInHeading);
|
|
637
|
-
|
|
638
|
-
this
|
|
639
|
-
|
|
652
|
+
|
|
653
|
+
this.querySelector("lexxy-heading-dropdown")
|
|
654
|
+
?.updateActiveHeading(headingTag);
|
|
640
655
|
|
|
641
656
|
this.#setButtonPressed("lists", isInList);
|
|
642
657
|
this.#setButtonPressed("unordered-list", isInList && listType === "bullet");
|
|
@@ -815,15 +830,9 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
815
830
|
<button type="button" name="paragraph" data-command="setFormatParagraph" title="Paragraph" role="menuitem">
|
|
816
831
|
${ToolbarIcons.paragraph} <span>Normal</span>
|
|
817
832
|
</button>
|
|
818
|
-
<
|
|
819
|
-
|
|
820
|
-
</
|
|
821
|
-
<button type="button" name="heading-medium" data-command="setFormatHeadingMedium" title="Medium heading" role="menuitem">
|
|
822
|
-
${ToolbarIcons.h3} <span>Medium Heading</span>
|
|
823
|
-
</button>
|
|
824
|
-
<button class="lexxy-editor__toolbar-group-end" type="button" name="heading-small" data-command="setFormatHeadingSmall" title="Small heading" role="menuitem">
|
|
825
|
-
${ToolbarIcons.h4} <span>Small Heading</span>
|
|
826
|
-
</button>
|
|
833
|
+
<lexxy-heading-dropdown>
|
|
834
|
+
<div class="lexxy-heading-options"></div>
|
|
835
|
+
</lexxy-heading-dropdown>
|
|
827
836
|
<div class="lexxy-editor__toolbar-separator" role="separator"></div>
|
|
828
837
|
<button type="button" name="clear-formatting" data-command="clearFormatting" title="Clear formatting" role="menuitem">
|
|
829
838
|
${ToolbarIcons.clearFormatting} <span>Clear formatting</span>
|
|
@@ -1049,6 +1058,141 @@ class ToolbarDropdown extends HTMLElement {
|
|
|
1049
1058
|
}
|
|
1050
1059
|
}
|
|
1051
1060
|
|
|
1061
|
+
const HEADING_BUTTON_SELECTOR = "button.lexxy-heading-button";
|
|
1062
|
+
|
|
1063
|
+
const HEADING_PRESETS = [
|
|
1064
|
+
{ label: "Large Heading", name: "heading-large" },
|
|
1065
|
+
{ label: "Medium Heading", name: "heading-medium" },
|
|
1066
|
+
{ label: "Small Heading", name: "heading-small" }
|
|
1067
|
+
];
|
|
1068
|
+
|
|
1069
|
+
class HeadingDropdown extends HTMLElement {
|
|
1070
|
+
static labelFor(tag, index) {
|
|
1071
|
+
if (index < HEADING_PRESETS.length) {
|
|
1072
|
+
return HEADING_PRESETS[index].label
|
|
1073
|
+
} else {
|
|
1074
|
+
const level = tag.match(/^h(\d+)$/)?.[1];
|
|
1075
|
+
if (level) {
|
|
1076
|
+
return `Heading ${level}`
|
|
1077
|
+
} else {
|
|
1078
|
+
return tag.toUpperCase()
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
static nameFor(tag, index) {
|
|
1084
|
+
if (index < HEADING_PRESETS.length) {
|
|
1085
|
+
return HEADING_PRESETS[index].name
|
|
1086
|
+
} else {
|
|
1087
|
+
const level = tag.match(/^h(\d+)$/)?.[1];
|
|
1088
|
+
if (level) {
|
|
1089
|
+
return `heading-${level}`
|
|
1090
|
+
} else {
|
|
1091
|
+
return `heading-${tag}`
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
#listeners = new ListenerBin()
|
|
1097
|
+
|
|
1098
|
+
connectedCallback() {
|
|
1099
|
+
this.style.display = "contents";
|
|
1100
|
+
|
|
1101
|
+
this.#onToolbarEditor(() => {
|
|
1102
|
+
this.#buttonContainer.style.display = "contents";
|
|
1103
|
+
this.#setUpButtons();
|
|
1104
|
+
this.#registerButtonHandlers();
|
|
1105
|
+
this.#syncActiveHeading();
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
disconnectedCallback() {
|
|
1110
|
+
this.#listeners.dispose();
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
updateActiveHeading(tag) {
|
|
1114
|
+
this.#headingButtons.forEach(button => {
|
|
1115
|
+
const next = (button.dataset.heading === tag).toString();
|
|
1116
|
+
if (button.getAttribute("aria-pressed") !== next) {
|
|
1117
|
+
button.setAttribute("aria-pressed", next);
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
get #toolbar() {
|
|
1123
|
+
return this.closest("lexxy-toolbar")
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
get #editorElement() {
|
|
1127
|
+
return this.#toolbar?.editorElement
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
get #editor() {
|
|
1131
|
+
return this.#toolbar?.editor
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
async #onToolbarEditor(callback) {
|
|
1135
|
+
const toolbar = this.#toolbar;
|
|
1136
|
+
if (!toolbar) return
|
|
1137
|
+
|
|
1138
|
+
await toolbar.getEditorElement();
|
|
1139
|
+
if (this.isConnected && this.#toolbar) callback();
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
#setUpButtons() {
|
|
1143
|
+
this.#buttonContainer.innerHTML = "";
|
|
1144
|
+
|
|
1145
|
+
this.#configuredHeadings.forEach((tag, index) => {
|
|
1146
|
+
this.#buttonContainer.appendChild(this.#createButton(tag, index));
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
#createButton(tag, index) {
|
|
1151
|
+
const label = HeadingDropdown.labelFor(tag, index);
|
|
1152
|
+
const name = HeadingDropdown.nameFor(tag, index);
|
|
1153
|
+
const icon = ToolbarIcons[tag] || "";
|
|
1154
|
+
|
|
1155
|
+
const button = document.createElement("button");
|
|
1156
|
+
button.type = "button";
|
|
1157
|
+
button.dataset.heading = tag;
|
|
1158
|
+
button.classList.add("lexxy-heading-button");
|
|
1159
|
+
button.name = name;
|
|
1160
|
+
button.title = label;
|
|
1161
|
+
button.setAttribute("role", "menuitem");
|
|
1162
|
+
button.innerHTML = `${icon} <span>${label}</span>`;
|
|
1163
|
+
return button
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
#registerButtonHandlers() {
|
|
1167
|
+
this.#headingButtons.forEach(button => {
|
|
1168
|
+
this.#listeners.track(registerEventListener(button, "click", (event) => {
|
|
1169
|
+
if (!this.#editor) return
|
|
1170
|
+
|
|
1171
|
+
event.preventDefault();
|
|
1172
|
+
this.#editor.dispatchCommand("applyHeadingFormat", button.dataset.heading);
|
|
1173
|
+
this.#editor.focus();
|
|
1174
|
+
}));
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
#syncActiveHeading() {
|
|
1179
|
+
const { headingTag } = this.#editor.read(() => this.#editorElement.selection.getFormat());
|
|
1180
|
+
this.updateActiveHeading(headingTag ?? null);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
get #configuredHeadings() {
|
|
1184
|
+
return this.#editorElement.config.get("headings")
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
get #buttonContainer() {
|
|
1188
|
+
return this.querySelector(".lexxy-heading-options")
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
get #headingButtons() {
|
|
1192
|
+
return Array.from(this.querySelectorAll(HEADING_BUTTON_SELECTOR))
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1052
1196
|
const APPLY_HIGHLIGHT_SELECTOR = "button.lexxy-highlight-button";
|
|
1053
1197
|
const REMOVE_HIGHLIGHT_SELECTOR = "[data-command='removeHighlight']";
|
|
1054
1198
|
|
|
@@ -1256,6 +1400,7 @@ const presets = new Configuration({
|
|
|
1256
1400
|
toolbar: {
|
|
1257
1401
|
upload: "both"
|
|
1258
1402
|
},
|
|
1403
|
+
headings: [ "h2", "h3", "h4" ],
|
|
1259
1404
|
highlight: {
|
|
1260
1405
|
buttons: {
|
|
1261
1406
|
color: range(1, 9).map(n => `var(--highlight-${n})`),
|
|
@@ -3104,9 +3249,9 @@ const hasPastedStylesState = createState("hasPastedStyles", {
|
|
|
3104
3249
|
});
|
|
3105
3250
|
|
|
3106
3251
|
// Stores pending highlight ranges extracted during HTML import, keyed by CodeNode key.
|
|
3107
|
-
//
|
|
3108
|
-
//
|
|
3109
|
-
//
|
|
3252
|
+
// The highlight-preserving tokenizer consumes this map when the code retokenizer
|
|
3253
|
+
// first tokenizes the block. Scoped per editor instance so entries don't leak
|
|
3254
|
+
// across editors or outlive a torn-down editor.
|
|
3110
3255
|
const pendingCodeHighlights = new WeakMap();
|
|
3111
3256
|
|
|
3112
3257
|
class HighlightExtension extends LexxyExtension {
|
|
@@ -3141,10 +3286,7 @@ class HighlightExtension extends LexxyExtension {
|
|
|
3141
3286
|
editor.registerCommand(REMOVE_HIGHLIGHT_COMMAND, () => $toggleSelectionStyles(editor, BLANK_STYLES), COMMAND_PRIORITY_NORMAL),
|
|
3142
3287
|
editor.registerNodeTransform(TextNode, $syncHighlightWithStyle),
|
|
3143
3288
|
editor.registerNodeTransform(CodeHighlightNode, $syncHighlightWithCodeHighlightNode),
|
|
3144
|
-
editor.registerNodeTransform(TextNode, (textNode) => $canonicalizePastedStyles(textNode, canonicalizers))
|
|
3145
|
-
editor.registerMutationListener(CodeNode, (mutations) => {
|
|
3146
|
-
$applyPendingCodeHighlights(editor, mutations);
|
|
3147
|
-
}, { skipInitialization: true })
|
|
3289
|
+
editor.registerNodeTransform(TextNode, (textNode) => $canonicalizePastedStyles(textNode, canonicalizers))
|
|
3148
3290
|
)
|
|
3149
3291
|
}
|
|
3150
3292
|
});
|
|
@@ -3192,7 +3334,7 @@ function $registerPreConversion(editor) {
|
|
|
3192
3334
|
// Returns a <pre> converter factory scoped to a specific editor instance.
|
|
3193
3335
|
// The factory extracts highlight ranges from <mark> elements before the code
|
|
3194
3336
|
// retokenizer can destroy them. The ranges are stored in pendingCodeHighlights
|
|
3195
|
-
// and
|
|
3337
|
+
// and restored by the highlight-preserving tokenizer during retokenization.
|
|
3196
3338
|
function $preConversionWithHighlightsFactory(editor) {
|
|
3197
3339
|
return function $preConversionWithHighlights(domNode) {
|
|
3198
3340
|
const highlights = extractHighlightRanges(domNode);
|
|
@@ -3268,98 +3410,116 @@ function extractHighlightStyleFromElement(element) {
|
|
|
3268
3410
|
return css.length > 0 ? css : null
|
|
3269
3411
|
}
|
|
3270
3412
|
|
|
3271
|
-
//
|
|
3272
|
-
//
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3413
|
+
// The code retokenizer replaces a code block's children with freshly created
|
|
3414
|
+
// tokens that carry no styles, which would drop color highlights on every
|
|
3415
|
+
// edit. This tokenizer wraps the stock Prism tokenizer to restore them: it
|
|
3416
|
+
// recovers the block's highlight ranges — staged during HTML import, or read
|
|
3417
|
+
// from the children the fresh tokens are about to replace — and reapplies
|
|
3418
|
+
// them to the fresh tokens before the retokenizer splices them in.
|
|
3419
|
+
function buildHighlightPreservingTokenizer(editor) {
|
|
3420
|
+
return {
|
|
3421
|
+
defaultLanguage: PrismTokenizer.defaultLanguage,
|
|
3422
|
+
tokenize(code, language) {
|
|
3423
|
+
return PrismTokenizer.tokenize(code, language)
|
|
3424
|
+
},
|
|
3425
|
+
$tokenize(codeNode, language) {
|
|
3426
|
+
const tokens = PrismTokenizer.$tokenize(codeNode, language);
|
|
3427
|
+
const highlights = $takeHighlightRanges(editor, codeNode);
|
|
3428
|
+
return $applyHighlightRangesToTokens(tokens, highlights)
|
|
3280
3429
|
}
|
|
3281
3430
|
}
|
|
3431
|
+
}
|
|
3282
3432
|
|
|
3283
|
-
|
|
3433
|
+
function $takeHighlightRanges(editor, codeNode) {
|
|
3434
|
+
const pending = $getPendingHighlights(editor);
|
|
3435
|
+
const key = codeNode.getKey();
|
|
3436
|
+
|
|
3437
|
+
if (pending.has(key)) {
|
|
3438
|
+
const highlights = pending.get(key);
|
|
3439
|
+
pending.delete(key);
|
|
3440
|
+
return highlights
|
|
3441
|
+
} else if (codeNode.getChildren().some($isCodeHighlightNode)) {
|
|
3442
|
+
return $extractHighlightRangesFromCodeNode(codeNode)
|
|
3443
|
+
} else {
|
|
3444
|
+
// A block that was never tokenized has only plain text children. Styles
|
|
3445
|
+
// on those come from import paths that didn't stage ranges (e.g. colored
|
|
3446
|
+
// spans in Trix HTML), and the first tokenization discards them.
|
|
3447
|
+
return []
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3284
3450
|
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
editor.update(() => {
|
|
3288
|
-
for (const key of keysToProcess) {
|
|
3289
|
-
const highlights = pending.get(key);
|
|
3290
|
-
pending.delete(key);
|
|
3291
|
-
if (!highlights) continue
|
|
3451
|
+
function $applyHighlightRangesToTokens(tokens, highlights) {
|
|
3452
|
+
if (highlights.length === 0) return tokens
|
|
3292
3453
|
|
|
3293
|
-
|
|
3294
|
-
|
|
3454
|
+
const styledTokens = [];
|
|
3455
|
+
let offset = 0;
|
|
3295
3456
|
|
|
3296
|
-
|
|
3457
|
+
for (const token of tokens) {
|
|
3458
|
+
if ($isCodeHighlightNode(token)) {
|
|
3459
|
+
styledTokens.push(...$splitTokenAtHighlightBoundaries(token, offset, highlights));
|
|
3460
|
+
} else {
|
|
3461
|
+
styledTokens.push(token);
|
|
3297
3462
|
}
|
|
3298
|
-
|
|
3463
|
+
offset += token.getTextContentSize();
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
return styledTokens
|
|
3299
3467
|
}
|
|
3300
3468
|
|
|
3301
|
-
//
|
|
3302
|
-
//
|
|
3303
|
-
//
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
if (highlights.length === 0) return
|
|
3308
|
-
|
|
3309
|
-
for (const { start: hlStart, end: hlEnd, style } of highlights) {
|
|
3310
|
-
// Rebuild the child-to-offset mapping for each highlight range because
|
|
3311
|
-
// earlier ranges may have split nodes, invalidating previous mappings.
|
|
3312
|
-
const childRanges = $buildChildRanges(codeNode);
|
|
3313
|
-
|
|
3314
|
-
for (const { node, start: nodeStart, end: nodeEnd } of childRanges) {
|
|
3315
|
-
// Skip plain TextNodes: only CodeHighlightNodes can be split into
|
|
3316
|
-
// styled replacements here. The retokenizer normally converts any
|
|
3317
|
-
// TextNode children back to CodeHighlightNodes before this runs,
|
|
3318
|
-
// but the iteration over $buildChildRanges has to keep counting
|
|
3319
|
-
// them so character offsets stay aligned with the saved ranges.
|
|
3320
|
-
if (!$isCodeHighlightNode(node)) continue
|
|
3321
|
-
|
|
3322
|
-
// Check if this child overlaps with the highlight range
|
|
3323
|
-
const overlapStart = Math.max(hlStart, nodeStart);
|
|
3324
|
-
const overlapEnd = Math.min(hlEnd, nodeEnd);
|
|
3325
|
-
|
|
3326
|
-
if (overlapStart >= overlapEnd) continue
|
|
3327
|
-
|
|
3328
|
-
// Calculate offsets relative to this node
|
|
3329
|
-
const relStart = overlapStart - nodeStart;
|
|
3330
|
-
const relEnd = overlapEnd - nodeStart;
|
|
3331
|
-
const nodeLength = nodeEnd - nodeStart;
|
|
3332
|
-
|
|
3333
|
-
if (relStart === 0 && relEnd === nodeLength) {
|
|
3334
|
-
// Entire node is highlighted - apply style directly
|
|
3335
|
-
node.setStyle(style);
|
|
3336
|
-
$setCodeHighlightFormat(node, true);
|
|
3337
|
-
} else {
|
|
3338
|
-
// Need to split: replace the node with 2 or 3 CodeHighlightNodes
|
|
3339
|
-
const text = node.getTextContent();
|
|
3340
|
-
const highlightType = node.getHighlightType();
|
|
3341
|
-
const replacements = [];
|
|
3469
|
+
// Split a token into segments at highlight boundaries, styling the covered
|
|
3470
|
+
// segments. We can't use TextNode.splitText() because the fresh tokens aren't
|
|
3471
|
+
// attached to the tree yet, so we create CodeHighlightNode replacements.
|
|
3472
|
+
function $splitTokenAtHighlightBoundaries(token, tokenStart, highlights) {
|
|
3473
|
+
const text = token.getTextContent();
|
|
3474
|
+
const segments = segmentTextByHighlights(text, tokenStart, highlights);
|
|
3342
3475
|
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3476
|
+
if (segments.length === 1) {
|
|
3477
|
+
const [ segment ] = segments;
|
|
3478
|
+
if (segment.style) {
|
|
3479
|
+
$applyHighlightStyleToToken(token, segment.style);
|
|
3480
|
+
}
|
|
3481
|
+
return [ token ]
|
|
3482
|
+
} else {
|
|
3483
|
+
return segments.map((segment) => {
|
|
3484
|
+
const segmentToken = $createCodeHighlightNode(text.slice(segment.start, segment.end), token.getHighlightType());
|
|
3485
|
+
if (segment.style) {
|
|
3486
|
+
$applyHighlightStyleToToken(segmentToken, segment.style);
|
|
3487
|
+
}
|
|
3488
|
+
return segmentToken
|
|
3489
|
+
})
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3346
3492
|
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3493
|
+
// Partition a token's text into consecutive { start, end, style } segments,
|
|
3494
|
+
// where offsets are relative to the token and style is null for the stretches
|
|
3495
|
+
// no highlight covers.
|
|
3496
|
+
function segmentTextByHighlights(text, tokenStart, highlights) {
|
|
3497
|
+
const segments = [];
|
|
3498
|
+
let cursor = 0;
|
|
3351
3499
|
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3500
|
+
for (const { start, end, style } of highlights) {
|
|
3501
|
+
const from = Math.max(start - tokenStart, cursor);
|
|
3502
|
+
const to = Math.min(end - tokenStart, text.length);
|
|
3355
3503
|
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
}
|
|
3359
|
-
node.remove();
|
|
3504
|
+
if (from < to) {
|
|
3505
|
+
if (from > cursor) {
|
|
3506
|
+
segments.push({ start: cursor, end: from, style: null });
|
|
3360
3507
|
}
|
|
3508
|
+
segments.push({ start: from, end: to, style });
|
|
3509
|
+
cursor = to;
|
|
3361
3510
|
}
|
|
3362
3511
|
}
|
|
3512
|
+
|
|
3513
|
+
if (cursor < text.length) {
|
|
3514
|
+
segments.push({ start: cursor, end: text.length, style: null });
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
return segments
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
function $applyHighlightStyleToToken(token, style) {
|
|
3521
|
+
token.setStyle(style);
|
|
3522
|
+
$setCodeHighlightFormat(token, true);
|
|
3363
3523
|
}
|
|
3364
3524
|
|
|
3365
3525
|
function $buildChildRanges(codeNode) {
|
|
@@ -3448,14 +3608,10 @@ function $patchCodeHighlightStyles(editor, selection, patch) {
|
|
|
3448
3608
|
textSize: node.getTextContentSize()
|
|
3449
3609
|
}));
|
|
3450
3610
|
|
|
3451
|
-
// Use skipTransforms
|
|
3452
|
-
//
|
|
3453
|
-
//
|
|
3454
|
-
// are committed before editor.focus() triggers a second update cycle
|
|
3455
|
-
// that would re-run transforms and wipe out the styles.
|
|
3611
|
+
// Use skipTransforms so the retokenizer doesn't rebuild the block in the
|
|
3612
|
+
// middle of the toggle, and discrete to force a synchronous commit so the
|
|
3613
|
+
// styles are in place before editor.focus() triggers a second update cycle.
|
|
3456
3614
|
editor.update(() => {
|
|
3457
|
-
const affectedCodeNodes = new Set();
|
|
3458
|
-
|
|
3459
3615
|
for (const { key, startOffset, endOffset, textSize } of nodeKeys) {
|
|
3460
3616
|
const node = $getNodeByKey(key);
|
|
3461
3617
|
if (!node) continue
|
|
@@ -3464,8 +3620,6 @@ function $patchCodeHighlightStyles(editor, selection, patch) {
|
|
|
3464
3620
|
if (!$isCodeNode(parent)) continue
|
|
3465
3621
|
if (startOffset === endOffset) continue
|
|
3466
3622
|
|
|
3467
|
-
affectedCodeNodes.add(parent);
|
|
3468
|
-
|
|
3469
3623
|
if (startOffset === 0 && endOffset === textSize) {
|
|
3470
3624
|
$applyStylePatchToNode(node, patch);
|
|
3471
3625
|
} else {
|
|
@@ -3474,17 +3628,6 @@ function $patchCodeHighlightStyles(editor, selection, patch) {
|
|
|
3474
3628
|
$applyStylePatchToNode(targetNode, patch);
|
|
3475
3629
|
}
|
|
3476
3630
|
}
|
|
3477
|
-
|
|
3478
|
-
// After applying styles, save highlight ranges for each affected CodeNode.
|
|
3479
|
-
// The code retokenizer will replace the styled nodes with fresh unstyled
|
|
3480
|
-
// tokens when transforms run. The pending highlights are picked up by the
|
|
3481
|
-
// CodeNode mutation listener and reapplied after retokenization.
|
|
3482
|
-
for (const codeNode of affectedCodeNodes) {
|
|
3483
|
-
const ranges = $extractHighlightRangesFromCodeNode(codeNode);
|
|
3484
|
-
if (ranges.length > 0) {
|
|
3485
|
-
$getPendingHighlights(editor).set(codeNode.getKey(), ranges);
|
|
3486
|
-
}
|
|
3487
|
-
}
|
|
3488
3631
|
}, { skipTransforms: true, discrete: true });
|
|
3489
3632
|
}
|
|
3490
3633
|
|
|
@@ -3604,10 +3747,8 @@ const COMMANDS = [
|
|
|
3604
3747
|
"unlink",
|
|
3605
3748
|
"toggleHighlight",
|
|
3606
3749
|
"removeHighlight",
|
|
3607
|
-
"setFormatHeadingLarge",
|
|
3608
|
-
"setFormatHeadingMedium",
|
|
3609
|
-
"setFormatHeadingSmall",
|
|
3610
3750
|
"setFormatParagraph",
|
|
3751
|
+
"applyHeadingFormat",
|
|
3611
3752
|
"clearFormatting",
|
|
3612
3753
|
"insertUnorderedList",
|
|
3613
3754
|
"insertOrderedList",
|
|
@@ -3799,22 +3940,14 @@ class CommandDispatcher {
|
|
|
3799
3940
|
$insertNodeToNearestRoot(new HorizontalDividerNode);
|
|
3800
3941
|
}
|
|
3801
3942
|
|
|
3802
|
-
dispatchSetFormatHeadingLarge() {
|
|
3803
|
-
this.contents.applyHeadingFormat("h2");
|
|
3804
|
-
}
|
|
3805
|
-
|
|
3806
|
-
dispatchSetFormatHeadingMedium() {
|
|
3807
|
-
this.contents.applyHeadingFormat("h3");
|
|
3808
|
-
}
|
|
3809
|
-
|
|
3810
|
-
dispatchSetFormatHeadingSmall() {
|
|
3811
|
-
this.contents.applyHeadingFormat("h4");
|
|
3812
|
-
}
|
|
3813
|
-
|
|
3814
3943
|
dispatchSetFormatParagraph() {
|
|
3815
3944
|
this.contents.applyParagraphFormat();
|
|
3816
3945
|
}
|
|
3817
3946
|
|
|
3947
|
+
dispatchApplyHeadingFormat(tag) {
|
|
3948
|
+
this.contents.applyHeadingFormat(tag);
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3818
3951
|
dispatchClearFormatting() {
|
|
3819
3952
|
this.contents.clearFormatting();
|
|
3820
3953
|
}
|
|
@@ -6379,17 +6512,15 @@ class Contents {
|
|
|
6379
6512
|
return { anchorNode, offset: anchor.offset }
|
|
6380
6513
|
}
|
|
6381
6514
|
|
|
6382
|
-
// The replaced span
|
|
6383
|
-
// inserted before "Jack"), so we
|
|
6384
|
-
//
|
|
6515
|
+
// The replaced span must start before the cursor but can extend past it
|
|
6516
|
+
// (e.g. "@Jack" when "@" was just inserted before "Jack"), so we bound the
|
|
6517
|
+
// match's start with lastIndexOf's fromIndex rather than slicing the text.
|
|
6385
6518
|
#findReplacementStart(anchorNode, offset, stringToReplace) {
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
if (triggerIndex !== -1 && fullText.startsWith(stringToReplace, triggerIndex)) {
|
|
6390
|
-
return triggerIndex
|
|
6391
|
-
} else {
|
|
6519
|
+
if (offset === 0) {
|
|
6520
|
+
// A negative fromIndex clamps to 0 and could match at the cursor
|
|
6392
6521
|
return -1
|
|
6522
|
+
} else {
|
|
6523
|
+
return anchorNode.getTextContent().lastIndexOf(stringToReplace, offset - 1)
|
|
6393
6524
|
}
|
|
6394
6525
|
}
|
|
6395
6526
|
|
|
@@ -7108,7 +7239,7 @@ class CodeHighlightingExtension extends LexxyExtension {
|
|
|
7108
7239
|
return defineExtension({
|
|
7109
7240
|
name: "lexxy/code-highlighting",
|
|
7110
7241
|
register(editor) {
|
|
7111
|
-
return registerCodeHighlighting(editor)
|
|
7242
|
+
return registerCodeHighlighting(editor, buildHighlightPreservingTokenizer(editor))
|
|
7112
7243
|
}
|
|
7113
7244
|
})
|
|
7114
7245
|
}
|
|
@@ -9276,11 +9407,14 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
9276
9407
|
get #supportedHeadingFormats() {
|
|
9277
9408
|
if (!this.supportsRichText) return []
|
|
9278
9409
|
|
|
9410
|
+
const headings = this.config.get("headings");
|
|
9279
9411
|
return [
|
|
9280
9412
|
{ label: "Normal", command: "setFormatParagraph", tag: null },
|
|
9281
|
-
|
|
9282
|
-
|
|
9283
|
-
|
|
9413
|
+
...headings.map((tag, index) => ({
|
|
9414
|
+
label: HeadingDropdown.labelFor(tag, index),
|
|
9415
|
+
command: "applyHeadingFormat",
|
|
9416
|
+
tag
|
|
9417
|
+
}))
|
|
9284
9418
|
]
|
|
9285
9419
|
}
|
|
9286
9420
|
|
|
@@ -10830,13 +10964,15 @@ class TableTools extends HTMLElement {
|
|
|
10830
10964
|
}
|
|
10831
10965
|
|
|
10832
10966
|
#registerKeyboardShortcuts() {
|
|
10833
|
-
this.#listeners.track(this.#editor.registerCommand(KEY_DOWN_COMMAND, this.#
|
|
10967
|
+
this.#listeners.track(this.#editor.registerCommand(KEY_DOWN_COMMAND, this.#focusToolbarOnAltF10, COMMAND_PRIORITY_HIGH));
|
|
10834
10968
|
}
|
|
10835
10969
|
|
|
10836
|
-
#
|
|
10837
|
-
if (this.#hasSelectedTable &&
|
|
10970
|
+
#focusToolbarOnAltF10 = (event) => {
|
|
10971
|
+
if (this.#hasSelectedTable && event.altKey && event.key === "F10") {
|
|
10838
10972
|
event.preventDefault();
|
|
10839
|
-
|
|
10973
|
+
// Ask for the ring explicitly: a programmatic focus coming from a mouse-focused
|
|
10974
|
+
// editor otherwise inherits the mouse modality and paints no focus ring.
|
|
10975
|
+
this.#tableToolsButtons[0]?.focus({ focusVisible: true });
|
|
10840
10976
|
return true
|
|
10841
10977
|
} else {
|
|
10842
10978
|
return false
|
|
@@ -10847,11 +10983,6 @@ class TableTools extends HTMLElement {
|
|
|
10847
10983
|
return this.tableController?.currentTableNodeKey != null
|
|
10848
10984
|
}
|
|
10849
10985
|
|
|
10850
|
-
#isFocusToolbarShortcut(event) {
|
|
10851
|
-
if (event.key !== "F10") return false
|
|
10852
|
-
return event.altKey || ((event.ctrlKey || event.metaKey) && event.shiftKey)
|
|
10853
|
-
}
|
|
10854
|
-
|
|
10855
10986
|
#handleToolsKeydown = (event) => {
|
|
10856
10987
|
if (event.key === "Escape") {
|
|
10857
10988
|
this.#handleEscapeKey();
|
|
@@ -11020,6 +11151,7 @@ function defineElements() {
|
|
|
11020
11151
|
// Toolbar must be registered BEFORE Editor
|
|
11021
11152
|
"lexxy-toolbar": LexicalToolbarElement,
|
|
11022
11153
|
"lexxy-toolbar-dropdown": ToolbarDropdown,
|
|
11154
|
+
"lexxy-heading-dropdown": HeadingDropdown,
|
|
11023
11155
|
"lexxy-highlight-dropdown": HighlightDropdown,
|
|
11024
11156
|
"lexxy-link-dropdown": LinkDropdown,
|
|
11025
11157
|
|