@37signals/lexxy 0.9.9-beta.preview5 → 0.9.10-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 +1919 -1845
- package/package.json +1 -1
package/dist/lexxy.esm.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { isActiveAndVisible,
|
|
1
|
+
import { isActiveAndVisible, extractPlainTextFromHtml, createElement, 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, $ensureForwardRangeSelection, $isAtNodeEnd, $getSelectionStyleValueForProperty, $patchStyleText, $setBlocksType, $forEachSelectedTextNode } from '@lexical/selection';
|
|
5
|
-
import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_REDO_COMMAND, $getSelection, $isRangeSelection, DecoratorNode, $
|
|
5
|
+
import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_REDO_COMMAND, $getSelection, $isRangeSelection, DecoratorNode, $createTextNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $createNodeSelection, $isDecoratorNode, $isLineBreakNode, $isTextNode, $isParagraphNode, $splitNode, $getSiblingCaret, LineBreakNode, $createParagraphNode, 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, PASTE_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $isNodeSelection, $getRoot, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, DELETE_CHARACTER_COMMAND, SELECTION_CHANGE_COMMAND, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, $addUpdateTag, ElementNode, $getChildCaretAtIndex, $createLineBreakNode, ParagraphNode, RootNode, COMMAND_PRIORITY_HIGH, DRAGSTART_COMMAND, DROP_COMMAND, INSERT_PARAGRAPH_COMMAND, mergeRegister as mergeRegister$1, $findMatchingParent, CLEAR_HISTORY_COMMAND, $onUpdate, 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
8
|
import { LinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, $isLinkNode, AutoLinkNode } from '@lexical/link';
|
|
9
|
-
import { $getNearestNodeOfType, $wrapNodeInElement, $lastToFirstIterator, mergeRegister, $insertFirst, $unwrapAndFilterDescendants, $firstToLastIterator, $getNearestBlockElementAncestorOrThrow,
|
|
9
|
+
import { $getNearestNodeOfType, $wrapNodeInElement, $lastToFirstIterator, $descendantsMatching, mergeRegister, $insertFirst, $unwrapAndFilterDescendants, $firstToLastIterator, $getNearestBlockElementAncestorOrThrow, 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';
|
|
13
|
+
import { HistoryExtension } from '@lexical/history';
|
|
13
14
|
import { $isCodeNode, CodeHighlightNode, CodeNode, $createCodeNode, $isCodeHighlightNode, $createCodeHighlightNode, normalizeCodeLang, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
|
|
14
15
|
import { TRANSFORMERS, registerMarkdownShortcuts } from '@lexical/markdown';
|
|
15
16
|
import { INSERT_TABLE_COMMAND, $getTableCellNodeFromLexicalNode, TableCellNode, TableNode, TableRowNode, setScrollableTablesActive, registerTablePlugin, registerTableSelectionObserver, TableCellHeaderStates, $insertTableRowAtSelection, $insertTableColumnAtSelection, $deleteTableRowAtSelection, $deleteTableColumnAtSelection, $findTableNode, $getTableRowIndexFromTableCellNode, $getTableColumnIndexFromTableCellNode, $findCellNode, $getElementForTableNode } from '@lexical/table';
|
|
16
|
-
import { HistoryExtension } from '@lexical/history';
|
|
17
17
|
import { marked } from 'marked';
|
|
18
18
|
import { $insertDataTransferForRichText } from '@lexical/clipboard';
|
|
19
19
|
import 'prismjs';
|
|
@@ -774,41 +774,185 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
774
774
|
}
|
|
775
775
|
}
|
|
776
776
|
|
|
777
|
-
|
|
777
|
+
function deepMerge(target, source) {
|
|
778
|
+
const result = { ...target, ...source };
|
|
779
|
+
for (const [ key, value ] of Object.entries(source)) {
|
|
780
|
+
if (arePlainHashes(target[key], value)) {
|
|
781
|
+
result[key] = deepMerge(target[key], value);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return result
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function arePlainHashes(...values) {
|
|
789
|
+
return values.every(value => value && value.constructor == Object)
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
class Configuration {
|
|
793
|
+
#tree = {}
|
|
794
|
+
|
|
795
|
+
constructor(...configs) {
|
|
796
|
+
this.merge(...configs);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
merge(...configs) {
|
|
800
|
+
return this.#tree = configs.reduce(deepMerge, this.#tree)
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
get(path) {
|
|
804
|
+
const keys = path.split(".");
|
|
805
|
+
return keys.reduce((node, key) => node[key], this.#tree)
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function range(from, to) {
|
|
810
|
+
return [ ...Array(1 + to - from).keys() ].map(i => i + from)
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const global = new Configuration({
|
|
814
|
+
attachmentTagName: "action-text-attachment",
|
|
815
|
+
attachmentContentTypeNamespace: "actiontext",
|
|
816
|
+
authenticatedUploads: false,
|
|
817
|
+
extensions: []
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
const presets = new Configuration({
|
|
821
|
+
default: {
|
|
822
|
+
attachments: true,
|
|
823
|
+
markdown: true,
|
|
824
|
+
multiLine: true,
|
|
825
|
+
permittedAttachmentTypes: null,
|
|
826
|
+
richText: true,
|
|
827
|
+
toolbar: {
|
|
828
|
+
upload: "both"
|
|
829
|
+
},
|
|
830
|
+
highlight: {
|
|
831
|
+
buttons: {
|
|
832
|
+
color: range(1, 9).map(n => `var(--highlight-${n})`),
|
|
833
|
+
"background-color": range(1, 9).map(n => `var(--highlight-bg-${n})`),
|
|
834
|
+
},
|
|
835
|
+
permit: {
|
|
836
|
+
color: [],
|
|
837
|
+
"background-color": []
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
var Lexxy = {
|
|
844
|
+
global,
|
|
845
|
+
presets,
|
|
846
|
+
configure({ global: newGlobal, ...newPresets }) {
|
|
847
|
+
if (newGlobal) {
|
|
848
|
+
global.merge(newGlobal);
|
|
849
|
+
}
|
|
850
|
+
presets.merge(newPresets);
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
function setSanitizerConfig(allowedTags) {
|
|
855
|
+
DOMPurify.clearConfig();
|
|
856
|
+
DOMPurify.setConfig(buildConfig(allowedTags));
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function sanitize(html) {
|
|
860
|
+
return DOMPurify.sanitize(html)
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function bytesToHumanSize(bytes) {
|
|
864
|
+
if (bytes === 0) return "0 B"
|
|
865
|
+
const sizes = [ "B", "KB", "MB", "GB", "TB", "PB" ];
|
|
866
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
867
|
+
const value = bytes / Math.pow(1024, i);
|
|
868
|
+
return `${ value.toFixed(2) } ${ sizes[i] }`
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function extractFileName(string) {
|
|
872
|
+
return string.split("/").pop()
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// The content attribute is raw HTML (matching Trix/ActionText). Older Lexxy
|
|
876
|
+
// versions JSON-encoded it, so try JSON.parse first for backward compatibility.
|
|
877
|
+
function parseAttachmentContent(content) {
|
|
878
|
+
try {
|
|
879
|
+
return JSON.parse(content)
|
|
880
|
+
} catch {
|
|
881
|
+
return content
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
class CustomActionTextAttachmentNode extends DecoratorNode {
|
|
778
886
|
static getType() {
|
|
779
|
-
return "
|
|
887
|
+
return "custom_action_text_attachment"
|
|
780
888
|
}
|
|
781
889
|
|
|
782
890
|
static clone(node) {
|
|
783
|
-
return new
|
|
891
|
+
return new CustomActionTextAttachmentNode({ ...node }, node.__key)
|
|
784
892
|
}
|
|
785
893
|
|
|
786
894
|
static importJSON(serializedNode) {
|
|
787
|
-
return new
|
|
895
|
+
return new CustomActionTextAttachmentNode({ ...serializedNode })
|
|
788
896
|
}
|
|
789
897
|
|
|
790
898
|
static importDOM() {
|
|
791
899
|
return {
|
|
792
|
-
|
|
900
|
+
[this.TAG_NAME]: (element) => {
|
|
901
|
+
if (!element.getAttribute("content")) {
|
|
902
|
+
return null
|
|
903
|
+
}
|
|
904
|
+
|
|
793
905
|
return {
|
|
794
|
-
conversion: () =>
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
906
|
+
conversion: (attachment) => {
|
|
907
|
+
// Preserve initial space if present since Lexical removes it
|
|
908
|
+
const nodes = [];
|
|
909
|
+
const previousSibling = attachment.previousSibling;
|
|
910
|
+
if (previousSibling && previousSibling.nodeType === Node.TEXT_NODE && /\s$/.test(previousSibling.textContent)) {
|
|
911
|
+
nodes.push($createTextNode(" "));
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const innerHtml = parseAttachmentContent(attachment.getAttribute("content"));
|
|
915
|
+
|
|
916
|
+
nodes.push(new CustomActionTextAttachmentNode({
|
|
917
|
+
sgid: attachment.getAttribute("sgid"),
|
|
918
|
+
innerHtml,
|
|
919
|
+
plainText: attachment.textContent.trim() || extractPlainTextFromHtml(innerHtml),
|
|
920
|
+
contentType: attachment.getAttribute("content-type")
|
|
921
|
+
}));
|
|
922
|
+
|
|
923
|
+
const nextSibling = attachment.nextSibling;
|
|
924
|
+
if (nextSibling && nextSibling.nodeType === Node.TEXT_NODE && /^\s/.test(nextSibling.textContent)) {
|
|
925
|
+
nodes.push($createTextNode(" "));
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
return { node: nodes }
|
|
929
|
+
},
|
|
930
|
+
priority: 2
|
|
798
931
|
}
|
|
799
932
|
}
|
|
800
933
|
}
|
|
801
934
|
}
|
|
802
935
|
|
|
803
|
-
|
|
936
|
+
static get TAG_NAME() {
|
|
937
|
+
return Lexxy.global.get("attachmentTagName")
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
constructor({ tagName, sgid, contentType, innerHtml, plainText }, key) {
|
|
804
941
|
super(key);
|
|
942
|
+
|
|
943
|
+
const contentTypeNamespace = Lexxy.global.get("attachmentContentTypeNamespace");
|
|
944
|
+
|
|
945
|
+
this.tagName = tagName || CustomActionTextAttachmentNode.TAG_NAME;
|
|
946
|
+
this.sgid = sgid;
|
|
947
|
+
this.contentType = contentType || `application/vnd.${contentTypeNamespace}.unknown`;
|
|
948
|
+
this.innerHtml = innerHtml;
|
|
949
|
+
this.plainText = plainText ?? extractPlainTextFromHtml(innerHtml);
|
|
805
950
|
}
|
|
806
951
|
|
|
807
952
|
createDOM() {
|
|
808
|
-
const figure = createElement(
|
|
809
|
-
const hr = createElement("hr");
|
|
953
|
+
const figure = createElement(this.tagName, { "content-type": this.contentType, "data-lexxy-decorator": true });
|
|
810
954
|
|
|
811
|
-
figure.
|
|
955
|
+
figure.insertAdjacentHTML("beforeend", sanitize(this.innerHtml));
|
|
812
956
|
|
|
813
957
|
const deleteButton = createElement("lexxy-node-delete-button");
|
|
814
958
|
figure.appendChild(deleteButton);
|
|
@@ -817,26 +961,40 @@ class HorizontalDividerNode extends DecoratorNode {
|
|
|
817
961
|
}
|
|
818
962
|
|
|
819
963
|
updateDOM() {
|
|
820
|
-
return
|
|
964
|
+
return false
|
|
821
965
|
}
|
|
822
966
|
|
|
823
967
|
getTextContent() {
|
|
824
|
-
return "
|
|
968
|
+
return "\ufeff"
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
getReadableTextContent() {
|
|
972
|
+
return this.plainText || `[${this.contentType}]`
|
|
825
973
|
}
|
|
826
974
|
|
|
827
975
|
isInline() {
|
|
828
|
-
return
|
|
976
|
+
return true
|
|
829
977
|
}
|
|
830
978
|
|
|
831
979
|
exportDOM() {
|
|
832
|
-
const
|
|
833
|
-
|
|
980
|
+
const attachment = createElement(this.tagName, {
|
|
981
|
+
sgid: this.sgid,
|
|
982
|
+
content: this.innerHtml,
|
|
983
|
+
"content-type": this.contentType
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
return { element: attachment }
|
|
834
987
|
}
|
|
835
988
|
|
|
836
989
|
exportJSON() {
|
|
837
990
|
return {
|
|
838
|
-
type: "
|
|
839
|
-
version: 1
|
|
991
|
+
type: "custom_action_text_attachment",
|
|
992
|
+
version: 1,
|
|
993
|
+
tagName: this.tagName,
|
|
994
|
+
sgid: this.sgid,
|
|
995
|
+
contentType: this.contentType,
|
|
996
|
+
innerHtml: this.innerHtml,
|
|
997
|
+
plainText: this.plainText
|
|
840
998
|
}
|
|
841
999
|
}
|
|
842
1000
|
|
|
@@ -845,407 +1003,439 @@ class HorizontalDividerNode extends DecoratorNode {
|
|
|
845
1003
|
}
|
|
846
1004
|
}
|
|
847
1005
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
return node instanceof HorizontalDividerNode ? "---" : null
|
|
852
|
-
},
|
|
853
|
-
regExpStart: /^-{3,}\s?$/,
|
|
854
|
-
replace: (parentNode, children, match, endMatch, linesInBetween, isImport) => {
|
|
855
|
-
const hrNode = new HorizontalDividerNode();
|
|
856
|
-
parentNode.replace(hrNode);
|
|
1006
|
+
function dasherize(value) {
|
|
1007
|
+
return value.replace(/([A-Z])/g, (_, char) => `-${char.toLowerCase()}`)
|
|
1008
|
+
}
|
|
857
1009
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
}
|
|
1010
|
+
function isUrl(string) {
|
|
1011
|
+
try {
|
|
1012
|
+
new URL(string);
|
|
1013
|
+
return true
|
|
1014
|
+
} catch {
|
|
1015
|
+
return false
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
866
1018
|
|
|
867
|
-
|
|
1019
|
+
function normalizeFilteredText(string) {
|
|
1020
|
+
return string
|
|
1021
|
+
.toLowerCase()
|
|
1022
|
+
.normalize("NFD").replace(/[\u0300-\u036f]/g, "") // Remove diacritics
|
|
1023
|
+
}
|
|
868
1024
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
//
|
|
873
|
-
// Lexical's markdown shortcut handler only triggers format transformations when
|
|
874
|
-
// the closing tag is the character just typed. When the opening tag is typed
|
|
875
|
-
// instead (e.g. typing ` before `hello`` to form ``hello``), the built-in
|
|
876
|
-
// handler doesn't match because it looks backward from the cursor for an
|
|
877
|
-
// opening tag, but the cursor is right after it.
|
|
878
|
-
//
|
|
879
|
-
// This listener detects that scenario for ALL text format transformers
|
|
880
|
-
// (backtick, bold, italic, strikethrough, etc.) and applies the appropriate
|
|
881
|
-
// format.
|
|
882
|
-
function registerMarkdownLeadingTagHandler(editor, transformers) {
|
|
883
|
-
const textFormatTransformers = transformers
|
|
884
|
-
.filter(t => t.type === "text-format")
|
|
885
|
-
.sort((a, b) => b.tag.length - a.tag.length); // Longer tags first
|
|
1025
|
+
function filterMatchPosition(text, potentialMatch) {
|
|
1026
|
+
const normalizedText = normalizeFilteredText(text);
|
|
1027
|
+
const normalizedMatch = normalizeFilteredText(potentialMatch);
|
|
886
1028
|
|
|
887
|
-
|
|
888
|
-
if (tags.has("historic") || tags.has("collaboration")) return
|
|
889
|
-
if (editor.isComposing()) return
|
|
1029
|
+
if (!normalizedMatch) return 0
|
|
890
1030
|
|
|
891
|
-
|
|
892
|
-
|
|
1031
|
+
const match = normalizedText.match(new RegExp(`(?:^|\\b)${escapeForRegExp(normalizedMatch)}`));
|
|
1032
|
+
return match ? match.index : -1
|
|
1033
|
+
}
|
|
893
1034
|
|
|
894
|
-
|
|
1035
|
+
function upcaseFirst(string) {
|
|
1036
|
+
return string.charAt(0).toUpperCase() + string.slice(1)
|
|
1037
|
+
}
|
|
895
1038
|
|
|
896
|
-
|
|
897
|
-
|
|
1039
|
+
function escapeForRegExp(string) {
|
|
1040
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
1041
|
+
}
|
|
898
1042
|
|
|
899
|
-
|
|
1043
|
+
// Parses a value that may arrive as a boolean or as a string (e.g. from DOM
|
|
1044
|
+
// getAttribute) into a proper boolean. Ensures "false" doesn't evaluate as truthy.
|
|
1045
|
+
function parseBoolean(value) {
|
|
1046
|
+
if (typeof value === "string") return value === "true"
|
|
1047
|
+
return Boolean(value)
|
|
1048
|
+
}
|
|
900
1049
|
|
|
901
|
-
|
|
902
|
-
|
|
1050
|
+
class LexxyExtension {
|
|
1051
|
+
#editorElement
|
|
903
1052
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1053
|
+
constructor(editorElement) {
|
|
1054
|
+
this.#editorElement = editorElement;
|
|
1055
|
+
}
|
|
907
1056
|
|
|
908
|
-
|
|
1057
|
+
get editorElement() {
|
|
1058
|
+
return this.#editorElement
|
|
1059
|
+
}
|
|
909
1060
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
const tagLen = tag.length;
|
|
1061
|
+
get editorConfig() {
|
|
1062
|
+
return this.#editorElement.config
|
|
1063
|
+
}
|
|
914
1064
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1065
|
+
// optional: defaults to true
|
|
1066
|
+
get enabled() {
|
|
1067
|
+
return true
|
|
1068
|
+
}
|
|
918
1069
|
|
|
919
|
-
|
|
920
|
-
|
|
1070
|
+
get lexicalExtension() {
|
|
1071
|
+
return null
|
|
1072
|
+
}
|
|
921
1073
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
const tagChar = tag[0];
|
|
926
|
-
if (openTagStart > 0 && textContent[openTagStart - 1] === tagChar) continue
|
|
1074
|
+
get allowedElements() {
|
|
1075
|
+
return []
|
|
1076
|
+
}
|
|
927
1077
|
|
|
928
|
-
|
|
929
|
-
// the opening tag must be a space, punctuation, or the start of the text
|
|
930
|
-
if (transformer.intraword === false && openTagStart > 0) {
|
|
931
|
-
const beforeChar = textContent[openTagStart - 1];
|
|
932
|
-
if (beforeChar && !PUNCTUATION_OR_SPACE.test(beforeChar)) continue
|
|
933
|
-
}
|
|
1078
|
+
initializeToolbar(_lexxyToolbar) {
|
|
934
1079
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
const closeTagIndex = textContent.indexOf(tag, searchStart);
|
|
938
|
-
if (closeTagIndex < 0) continue
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
939
1082
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1083
|
+
function $createNodeSelectionWith(...nodes) {
|
|
1084
|
+
const selection = $createNodeSelection();
|
|
1085
|
+
nodes.forEach(node => selection.add(node.getKey()));
|
|
1086
|
+
return selection
|
|
1087
|
+
}
|
|
944
1088
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1089
|
+
function $isShadowRoot(node) {
|
|
1090
|
+
return $isElementNode(node) && $isRootOrShadowRoot(node) && !$isRootNode(node)
|
|
1091
|
+
}
|
|
948
1092
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1093
|
+
function $makeSafeForRoot(node) {
|
|
1094
|
+
if ($isTextNode(node)) {
|
|
1095
|
+
return $wrapNodeInElement(node, $createParagraphNode)
|
|
1096
|
+
} else if (node.isParentRequired()) {
|
|
1097
|
+
const parent = node.createRequiredParent();
|
|
1098
|
+
return $wrapNodeInElement(node, parent)
|
|
1099
|
+
} else {
|
|
1100
|
+
return node
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
953
1103
|
|
|
954
|
-
|
|
955
|
-
|
|
1104
|
+
function getListType(node) {
|
|
1105
|
+
const list = $getNearestNodeOfType(node, ListNode);
|
|
1106
|
+
return list?.getListType() ?? null
|
|
1107
|
+
}
|
|
956
1108
|
|
|
957
|
-
|
|
958
|
-
|
|
1109
|
+
function isEditorFocused(editor) {
|
|
1110
|
+
const rootElement = editor.getRootElement();
|
|
1111
|
+
return rootElement !== null && rootElement.contains(document.activeElement)
|
|
1112
|
+
}
|
|
959
1113
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1114
|
+
function $isAtNodeEdge(point, atStart = null) {
|
|
1115
|
+
if (atStart === null) {
|
|
1116
|
+
return $isAtNodeEdge(point, true) || $isAtNodeEdge(point, false)
|
|
1117
|
+
} else {
|
|
1118
|
+
return atStart ? $isAtNodeStart(point) : $isAtNodeEnd(point)
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
965
1121
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1122
|
+
function $isAtNodeStart(point) {
|
|
1123
|
+
return point.offset === 0
|
|
1124
|
+
}
|
|
969
1125
|
|
|
970
|
-
|
|
971
|
-
|
|
1126
|
+
function extendTextNodeConversion(conversionName, ...callbacks) {
|
|
1127
|
+
return extendConversion(TextNode, conversionName, (conversionOutput, element) => ({
|
|
1128
|
+
...conversionOutput,
|
|
1129
|
+
forChild: (lexicalNode, parentNode) => {
|
|
1130
|
+
const originalForChild = conversionOutput?.forChild ?? (x => x);
|
|
1131
|
+
let childNode = originalForChild(lexicalNode, parentNode);
|
|
972
1132
|
|
|
973
|
-
$applyFormatFromLeadingTag(node, openTagStart, transformer);
|
|
974
|
-
});
|
|
975
1133
|
|
|
976
|
-
|
|
1134
|
+
if ($isTextNode(childNode)) {
|
|
1135
|
+
childNode = callbacks.reduce(
|
|
1136
|
+
(childNode, callback) => callback(childNode, element) ?? childNode,
|
|
1137
|
+
childNode
|
|
1138
|
+
);
|
|
1139
|
+
return childNode
|
|
1140
|
+
}
|
|
977
1141
|
}
|
|
978
|
-
})
|
|
1142
|
+
}))
|
|
979
1143
|
}
|
|
980
1144
|
|
|
981
|
-
function
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1145
|
+
function extendConversion(nodeKlass, conversionName, callback = (output => output)) {
|
|
1146
|
+
return (element) => {
|
|
1147
|
+
const converter = nodeKlass.importDOM()?.[conversionName]?.(element);
|
|
1148
|
+
if (!converter) return null
|
|
985
1149
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
if (closeTagIndex < 0) return
|
|
1150
|
+
const conversionOutput = converter.conversion(element);
|
|
1151
|
+
if (!conversionOutput) return conversionOutput
|
|
989
1152
|
|
|
990
|
-
|
|
991
|
-
|
|
1153
|
+
return callback(conversionOutput, element) ?? conversionOutput
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
992
1156
|
|
|
993
|
-
|
|
994
|
-
const
|
|
995
|
-
const
|
|
1157
|
+
function $isCursorOnLastLine(selection) {
|
|
1158
|
+
const anchorNode = selection.anchor.getNode();
|
|
1159
|
+
const elementNode = $isElementNode(anchorNode) ? anchorNode : anchorNode.getParentOrThrow();
|
|
1160
|
+
const children = elementNode.getChildren();
|
|
1161
|
+
if (children.length === 0) return true
|
|
996
1162
|
|
|
997
|
-
|
|
1163
|
+
const lastChild = children[children.length - 1];
|
|
998
1164
|
|
|
999
|
-
|
|
1000
|
-
|
|
1165
|
+
if (anchorNode === elementNode.getLatest() && selection.anchor.offset === children.length) return true
|
|
1166
|
+
if (anchorNode === lastChild) return true
|
|
1001
1167
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
nextSelection.focus.set(anchorNode.getKey(), openTagStart + inner.length, "text");
|
|
1168
|
+
const lastLineBreakIndex = children.findLastIndex(child => $isLineBreakNode(child));
|
|
1169
|
+
if (lastLineBreakIndex === -1) return true
|
|
1005
1170
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1171
|
+
const anchorIndex = children.indexOf(anchorNode);
|
|
1172
|
+
return anchorIndex > lastLineBreakIndex
|
|
1173
|
+
}
|
|
1011
1174
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
nextSelection.anchor.set(nextSelection.focus.key, nextSelection.focus.offset, nextSelection.focus.type);
|
|
1175
|
+
function $isBlankNode(node) {
|
|
1176
|
+
if (node.getTextContent().trim() !== "") return false
|
|
1015
1177
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1178
|
+
const children = node.getChildren?.();
|
|
1179
|
+
if (!children || children.length === 0) return true
|
|
1180
|
+
|
|
1181
|
+
return children.every(child => {
|
|
1182
|
+
if ($isLineBreakNode(child)) return true
|
|
1183
|
+
return $isBlankNode(child)
|
|
1184
|
+
})
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function $trimTrailingBlankNodes(parent) {
|
|
1188
|
+
for (const child of $lastToFirstIterator(parent)) {
|
|
1189
|
+
if ($isBlankNode(child)) {
|
|
1190
|
+
child.remove();
|
|
1191
|
+
} else {
|
|
1192
|
+
break
|
|
1019
1193
|
}
|
|
1020
1194
|
}
|
|
1021
1195
|
}
|
|
1022
1196
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
tableCellFocus: "lexxy-content__table-cell--focus",
|
|
1037
|
-
list: {
|
|
1038
|
-
nested: {
|
|
1039
|
-
listitem: "lexxy-nested-listitem",
|
|
1040
|
-
}
|
|
1041
|
-
},
|
|
1042
|
-
codeHighlight: {
|
|
1043
|
-
addition: "code-token__selector",
|
|
1044
|
-
atrule: "code-token__attr",
|
|
1045
|
-
attr: "code-token__attr",
|
|
1046
|
-
"attr-name": "code-token__attr",
|
|
1047
|
-
"attr-value": "code-token__selector",
|
|
1048
|
-
boolean: "code-token__property",
|
|
1049
|
-
bold: "code-token__variable",
|
|
1050
|
-
builtin: "code-token__selector",
|
|
1051
|
-
cdata: "code-token__comment",
|
|
1052
|
-
char: "code-token__selector",
|
|
1053
|
-
class: "code-token__function",
|
|
1054
|
-
"class-name": "code-token__function",
|
|
1055
|
-
color: "code-token__property",
|
|
1056
|
-
comment: "code-token__comment",
|
|
1057
|
-
constant: "code-token__property",
|
|
1058
|
-
coord: "code-token__comment",
|
|
1059
|
-
decorator: "code-token__function",
|
|
1060
|
-
deleted: "code-token__operator",
|
|
1061
|
-
deletion: "code-token__operator",
|
|
1062
|
-
directive: "code-token__attr",
|
|
1063
|
-
"directive-hash": "code-token__property",
|
|
1064
|
-
doctype: "code-token__comment",
|
|
1065
|
-
entity: "code-token__operator",
|
|
1066
|
-
function: "code-token__function",
|
|
1067
|
-
hexcode: "code-token__property",
|
|
1068
|
-
important: "code-token__function",
|
|
1069
|
-
inserted: "code-token__selector",
|
|
1070
|
-
italic: "code-token__comment",
|
|
1071
|
-
keyword: "code-token__attr",
|
|
1072
|
-
line: "code-token__selector",
|
|
1073
|
-
namespace: "code-token__variable",
|
|
1074
|
-
number: "code-token__property",
|
|
1075
|
-
macro: "code-token__function",
|
|
1076
|
-
operator: "code-token__operator",
|
|
1077
|
-
parameter: "code-token__variable",
|
|
1078
|
-
prolog: "code-token__comment",
|
|
1079
|
-
property: "code-token__property",
|
|
1080
|
-
punctuation: "code-token__punctuation",
|
|
1081
|
-
"raw-string": "code-token__operator",
|
|
1082
|
-
regex: "code-token__variable",
|
|
1083
|
-
script: "code-token__function",
|
|
1084
|
-
selector: "code-token__selector",
|
|
1085
|
-
string: "code-token__selector",
|
|
1086
|
-
style: "code-token__function",
|
|
1087
|
-
symbol: "code-token__property",
|
|
1088
|
-
tag: "code-token__property",
|
|
1089
|
-
title: "code-token__function",
|
|
1090
|
-
"type-definition": "code-token__function",
|
|
1091
|
-
url: "code-token__operator",
|
|
1092
|
-
variable: "code-token__variable",
|
|
1093
|
-
}
|
|
1094
|
-
};
|
|
1095
|
-
|
|
1096
|
-
function deepMerge(target, source) {
|
|
1097
|
-
const result = { ...target, ...source };
|
|
1098
|
-
for (const [ key, value ] of Object.entries(source)) {
|
|
1099
|
-
if (arePlainHashes(target[key], value)) {
|
|
1100
|
-
result[key] = deepMerge(target[key], value);
|
|
1197
|
+
// A list item is structurally empty if it contains no meaningful content.
|
|
1198
|
+
// Unlike getTextContent().trim() === "", this walks descendants to ensure
|
|
1199
|
+
// decorator nodes (mentions, attachments whose getTextContent() may return
|
|
1200
|
+
// invisible characters like \ufeff) are treated as non-empty content.
|
|
1201
|
+
function $isListItemStructurallyEmpty(listItem) {
|
|
1202
|
+
const children = listItem.getChildren();
|
|
1203
|
+
for (const child of children) {
|
|
1204
|
+
if ($isDecoratorNode(child)) return false
|
|
1205
|
+
if ($isLineBreakNode(child)) continue
|
|
1206
|
+
if ($isTextNode(child)) {
|
|
1207
|
+
if (child.getTextContent().trim() !== "") return false
|
|
1208
|
+
} else if ($isElementNode(child)) {
|
|
1209
|
+
if (child.getTextContent().trim() !== "") return false
|
|
1101
1210
|
}
|
|
1102
1211
|
}
|
|
1103
|
-
|
|
1104
|
-
return result
|
|
1212
|
+
return true
|
|
1105
1213
|
}
|
|
1106
1214
|
|
|
1107
|
-
function
|
|
1108
|
-
return
|
|
1215
|
+
function isAttachmentSpacerTextNode(node, previousNode, index, childCount) {
|
|
1216
|
+
return $isTextNode(node)
|
|
1217
|
+
&& node.getTextContent() === " "
|
|
1218
|
+
&& index === childCount - 1
|
|
1219
|
+
&& previousNode instanceof CustomActionTextAttachmentNode
|
|
1109
1220
|
}
|
|
1110
1221
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
constructor(...configs) {
|
|
1115
|
-
this.merge(...configs);
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
merge(...configs) {
|
|
1119
|
-
return this.#tree = configs.reduce(deepMerge, this.#tree)
|
|
1120
|
-
}
|
|
1222
|
+
function $splitParagraphsAtLineBreakBoundaries(selection) {
|
|
1223
|
+
$ensureForwardRangeSelection(selection);
|
|
1121
1224
|
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
}
|
|
1225
|
+
// Split focus first so the anchor split position stays valid.
|
|
1226
|
+
$splitAtNearestLineBreak(selection.focus, "next");
|
|
1227
|
+
$splitAtNearestLineBreak(selection.anchor, "previous");
|
|
1126
1228
|
}
|
|
1127
1229
|
|
|
1128
|
-
function
|
|
1129
|
-
|
|
1130
|
-
|
|
1230
|
+
function $splitAtNearestLineBreak(point, direction) {
|
|
1231
|
+
const paragraph = point.getNode().getTopLevelElement();
|
|
1232
|
+
if (!paragraph || !$isParagraphNode(paragraph)) return
|
|
1131
1233
|
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
extensions: []
|
|
1137
|
-
});
|
|
1234
|
+
const pointNode = point.getNode();
|
|
1235
|
+
const selectionChild = pointNode.getParent().is(paragraph) ? pointNode : pointNode.getParentOrThrow();
|
|
1236
|
+
const lineBreakCaret = $caretAtNearestNodeOfType(selectionChild, LineBreakNode, direction);
|
|
1237
|
+
if (!lineBreakCaret) return
|
|
1138
1238
|
|
|
1139
|
-
const
|
|
1140
|
-
|
|
1141
|
-
attachments: true,
|
|
1142
|
-
markdown: true,
|
|
1143
|
-
multiLine: true,
|
|
1144
|
-
richText: true,
|
|
1145
|
-
toolbar: {
|
|
1146
|
-
upload: "both"
|
|
1147
|
-
},
|
|
1148
|
-
highlight: {
|
|
1149
|
-
buttons: {
|
|
1150
|
-
color: range(1, 9).map(n => `var(--highlight-${n})`),
|
|
1151
|
-
"background-color": range(1, 9).map(n => `var(--highlight-bg-${n})`),
|
|
1152
|
-
},
|
|
1153
|
-
permit: {
|
|
1154
|
-
color: [],
|
|
1155
|
-
"background-color": []
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
});
|
|
1239
|
+
const lineBreak = lineBreakCaret.origin;
|
|
1240
|
+
const isEdge = lineBreakCaret.getNodeAtCaret() === null;
|
|
1160
1241
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
presets,
|
|
1164
|
-
configure({ global: newGlobal, ...newPresets }) {
|
|
1165
|
-
if (newGlobal) {
|
|
1166
|
-
global.merge(newGlobal);
|
|
1167
|
-
}
|
|
1168
|
-
presets.merge(newPresets);
|
|
1242
|
+
if (!isEdge) {
|
|
1243
|
+
$splitNode(paragraph, lineBreak.getIndexWithinParent());
|
|
1169
1244
|
}
|
|
1170
|
-
};
|
|
1171
1245
|
|
|
1172
|
-
|
|
1173
|
-
DOMPurify.clearConfig();
|
|
1174
|
-
DOMPurify.setConfig(buildConfig(allowedTags));
|
|
1246
|
+
lineBreak.remove();
|
|
1175
1247
|
}
|
|
1176
1248
|
|
|
1177
|
-
function
|
|
1178
|
-
|
|
1249
|
+
function $caretAtNearestNodeOfType(node, klass, direction) {
|
|
1250
|
+
for (const caret of $getSiblingCaret(node, direction)) {
|
|
1251
|
+
if (caret.origin instanceof klass) return caret
|
|
1252
|
+
}
|
|
1253
|
+
return null
|
|
1179
1254
|
}
|
|
1180
1255
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
const value = bytes / Math.pow(1024, i);
|
|
1186
|
-
return `${ value.toFixed(2) } ${ sizes[i] }`
|
|
1187
|
-
}
|
|
1256
|
+
// Payload: Record<nodeKey, { patch?, replace? }>
|
|
1257
|
+
// - patch: plain object, shallow-merged into the existing node's properties
|
|
1258
|
+
// - replace: a LexicalNode instance that replaces the node
|
|
1259
|
+
const REWRITE_HISTORY_COMMAND = createCommand("REWRITE_HISTORY_COMMAND");
|
|
1188
1260
|
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
}
|
|
1261
|
+
class RewritableHistoryExtension extends LexxyExtension {
|
|
1262
|
+
#historyState = null
|
|
1192
1263
|
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1264
|
+
get lexicalExtension() {
|
|
1265
|
+
return defineExtension({
|
|
1266
|
+
name: "lexxy/rewritable-history",
|
|
1267
|
+
dependencies: [ HistoryExtension ],
|
|
1268
|
+
register: (editor, _config, state) => {
|
|
1269
|
+
const historyOutput = state.getDependency(HistoryExtension).output;
|
|
1270
|
+
this.#historyState = historyOutput.historyState.value;
|
|
1271
|
+
|
|
1272
|
+
return editor.registerCommand(
|
|
1273
|
+
REWRITE_HISTORY_COMMAND,
|
|
1274
|
+
(rewrites) => this.#rewriteHistory(rewrites),
|
|
1275
|
+
COMMAND_PRIORITY_EDITOR
|
|
1276
|
+
)
|
|
1277
|
+
}
|
|
1278
|
+
})
|
|
1200
1279
|
}
|
|
1201
|
-
}
|
|
1202
1280
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
return "custom_action_text_attachment"
|
|
1281
|
+
get historyState() {
|
|
1282
|
+
return this.#historyState
|
|
1206
1283
|
}
|
|
1207
1284
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1285
|
+
get #allHistoryEntries() {
|
|
1286
|
+
const entries = Array.from(this.#historyState.undoStack);
|
|
1287
|
+
if (this.#historyState.current) entries.push(this.#historyState.current);
|
|
1288
|
+
return entries.concat(this.#historyState.redoStack)
|
|
1210
1289
|
}
|
|
1211
1290
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1291
|
+
#rewriteHistory(rewrites) {
|
|
1292
|
+
this.#applyRewritesImmediatelyToCurrentState(rewrites);
|
|
1293
|
+
this.#applyRewritesToHistory(rewrites);
|
|
1294
|
+
|
|
1295
|
+
return true
|
|
1214
1296
|
}
|
|
1215
1297
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
[
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
}
|
|
1298
|
+
#applyRewritesImmediatelyToCurrentState(rewrites) {
|
|
1299
|
+
$getEditor().update(() => {
|
|
1300
|
+
for (const [ nodeKey, { patch, replace } ] of Object.entries(rewrites)) {
|
|
1301
|
+
const node = $getNodeByKey(nodeKey);
|
|
1302
|
+
if (!node) continue
|
|
1222
1303
|
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
if (previousSibling && previousSibling.nodeType === Node.TEXT_NODE && /\s$/.test(previousSibling.textContent)) {
|
|
1229
|
-
nodes.push($createTextNode(" "));
|
|
1230
|
-
}
|
|
1304
|
+
if (patch) Object.assign(node.getWritable(), patch);
|
|
1305
|
+
if (replace) node.replace(replace);
|
|
1306
|
+
}
|
|
1307
|
+
}, { discrete: true, tag: this.#getBackgroundUpdateTags() });
|
|
1308
|
+
}
|
|
1231
1309
|
|
|
1232
|
-
|
|
1310
|
+
#applyRewritesToHistory(rewrites) {
|
|
1311
|
+
const nodeKeys = Object.keys(rewrites);
|
|
1233
1312
|
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
innerHtml,
|
|
1237
|
-
plainText: attachment.textContent.trim() || extractPlainTextFromHtml(innerHtml),
|
|
1238
|
-
contentType: attachment.getAttribute("content-type")
|
|
1239
|
-
}));
|
|
1313
|
+
for (const entry of this.#allHistoryEntries) {
|
|
1314
|
+
if (!this.#entryHasSomeKeys(entry, nodeKeys)) continue
|
|
1240
1315
|
|
|
1241
|
-
|
|
1242
|
-
if (nextSibling && nextSibling.nodeType === Node.TEXT_NODE && /^\s/.test(nextSibling.textContent)) {
|
|
1243
|
-
nodes.push($createTextNode(" "));
|
|
1244
|
-
}
|
|
1316
|
+
const editorState = entry.editorState = safeCloneEditorState(entry.editorState);
|
|
1245
1317
|
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1318
|
+
for (const [ nodeKey, { patch, replace } ] of Object.entries(rewrites)) {
|
|
1319
|
+
const node = editorState._nodeMap.get(nodeKey);
|
|
1320
|
+
if (!node) continue
|
|
1321
|
+
|
|
1322
|
+
if (patch) {
|
|
1323
|
+
this.#patchNodeInEditorState(editorState, node, patch);
|
|
1324
|
+
} else if (replace) {
|
|
1325
|
+
this.#replaceNodeInEditorState(editorState, node, replace);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
#entryHasSomeKeys(entry, nodeKeys) {
|
|
1332
|
+
return nodeKeys.some(key => entry.editorState._nodeMap.has(key))
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
#getBackgroundUpdateTags() {
|
|
1336
|
+
const tags = [ HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG ];
|
|
1337
|
+
if (!isEditorFocused(this.editorElement.editor)) { tags.push(SKIP_DOM_SELECTION_TAG); }
|
|
1338
|
+
return tags
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
#patchNodeInEditorState(editorState, node, patch) {
|
|
1342
|
+
editorState._nodeMap.set(node.__key, $cloneNodeWithPatch(node, patch));
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
#replaceNodeInEditorState(editorState, node, replaceWith) {
|
|
1346
|
+
editorState._nodeMap.set(node.__key, $cloneNodeAdoptingKeys(replaceWith, node));
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function $cloneNodeWithPatch(node, patch) {
|
|
1351
|
+
const clone = $cloneWithProperties(node);
|
|
1352
|
+
Object.assign(clone, patch);
|
|
1353
|
+
return clone
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function $cloneNodeAdoptingKeys(node, previousNode) {
|
|
1357
|
+
const clone = $cloneWithProperties(node);
|
|
1358
|
+
clone.__key = previousNode.__key;
|
|
1359
|
+
clone.__parent = previousNode.__parent;
|
|
1360
|
+
clone.__prev = previousNode.__prev;
|
|
1361
|
+
clone.__next = previousNode.__next;
|
|
1362
|
+
return clone
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// EditorState#clone() keeps the same map reference.
|
|
1366
|
+
// A new Map is needed to prevent editing Lexical's internal map
|
|
1367
|
+
// Warning: this bypasses DEV's safety map freezing
|
|
1368
|
+
function safeCloneEditorState(editorState) {
|
|
1369
|
+
const clone = editorState.clone();
|
|
1370
|
+
clone._nodeMap = new Map(editorState._nodeMap);
|
|
1371
|
+
return clone
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
class ActionTextAttachmentNode extends DecoratorNode {
|
|
1375
|
+
static getType() {
|
|
1376
|
+
return "action_text_attachment"
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
static clone(node) {
|
|
1380
|
+
return new ActionTextAttachmentNode({ ...node }, node.__key)
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
static importJSON(serializedNode) {
|
|
1384
|
+
return new ActionTextAttachmentNode({ ...serializedNode })
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
static importDOM() {
|
|
1388
|
+
return {
|
|
1389
|
+
[this.TAG_NAME]: () => {
|
|
1390
|
+
return {
|
|
1391
|
+
conversion: (attachment) => ({
|
|
1392
|
+
node: new ActionTextAttachmentNode({
|
|
1393
|
+
sgid: attachment.getAttribute("sgid"),
|
|
1394
|
+
src: attachment.getAttribute("url"),
|
|
1395
|
+
previewable: attachment.getAttribute("previewable"),
|
|
1396
|
+
altText: attachment.getAttribute("alt"),
|
|
1397
|
+
caption: attachment.getAttribute("caption"),
|
|
1398
|
+
contentType: attachment.getAttribute("content-type"),
|
|
1399
|
+
fileName: attachment.getAttribute("filename"),
|
|
1400
|
+
fileSize: attachment.getAttribute("filesize"),
|
|
1401
|
+
width: attachment.getAttribute("width"),
|
|
1402
|
+
height: attachment.getAttribute("height")
|
|
1403
|
+
})
|
|
1404
|
+
}), priority: 1
|
|
1405
|
+
}
|
|
1406
|
+
},
|
|
1407
|
+
"img": () => {
|
|
1408
|
+
return {
|
|
1409
|
+
conversion: (img) => {
|
|
1410
|
+
const fileName = extractFileName(img.getAttribute("src") ?? "");
|
|
1411
|
+
return {
|
|
1412
|
+
node: new ActionTextAttachmentNode({
|
|
1413
|
+
src: img.getAttribute("src"),
|
|
1414
|
+
fileName: fileName,
|
|
1415
|
+
caption: img.getAttribute("alt") || "",
|
|
1416
|
+
contentType: "image/*",
|
|
1417
|
+
width: img.getAttribute("width"),
|
|
1418
|
+
height: img.getAttribute("height")
|
|
1419
|
+
})
|
|
1420
|
+
}
|
|
1421
|
+
}, priority: 1
|
|
1422
|
+
}
|
|
1423
|
+
},
|
|
1424
|
+
"video": () => {
|
|
1425
|
+
return {
|
|
1426
|
+
conversion: (video) => {
|
|
1427
|
+
const videoSource = video.getAttribute("src") || video.querySelector("source")?.src;
|
|
1428
|
+
const fileName = videoSource?.split("/")?.pop();
|
|
1429
|
+
const contentType = video.querySelector("source")?.getAttribute("content-type") || "video/*";
|
|
1430
|
+
|
|
1431
|
+
return {
|
|
1432
|
+
node: new ActionTextAttachmentNode({
|
|
1433
|
+
src: videoSource,
|
|
1434
|
+
fileName: fileName,
|
|
1435
|
+
contentType: contentType
|
|
1436
|
+
})
|
|
1437
|
+
}
|
|
1438
|
+
}, priority: 1
|
|
1249
1439
|
}
|
|
1250
1440
|
}
|
|
1251
1441
|
}
|
|
@@ -1255,50 +1445,79 @@ class CustomActionTextAttachmentNode extends DecoratorNode {
|
|
|
1255
1445
|
return Lexxy.global.get("attachmentTagName")
|
|
1256
1446
|
}
|
|
1257
1447
|
|
|
1258
|
-
constructor({ tagName, sgid, contentType,
|
|
1448
|
+
constructor({ tagName, sgid, src, previewSrc, previewable, pendingPreview, altText, caption, contentType, fileName, fileSize, width, height, uploadError }, key) {
|
|
1259
1449
|
super(key);
|
|
1260
1450
|
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
this.tagName = tagName || CustomActionTextAttachmentNode.TAG_NAME;
|
|
1451
|
+
this.tagName = tagName || ActionTextAttachmentNode.TAG_NAME;
|
|
1264
1452
|
this.sgid = sgid;
|
|
1265
|
-
this.
|
|
1266
|
-
this.
|
|
1267
|
-
this.
|
|
1453
|
+
this.src = src;
|
|
1454
|
+
this.previewSrc = previewSrc;
|
|
1455
|
+
this.previewable = parseBoolean(previewable);
|
|
1456
|
+
this.pendingPreview = pendingPreview;
|
|
1457
|
+
this.altText = altText || "";
|
|
1458
|
+
this.caption = caption || "";
|
|
1459
|
+
this.contentType = contentType || "";
|
|
1460
|
+
this.fileName = fileName || "";
|
|
1461
|
+
this.fileSize = fileSize;
|
|
1462
|
+
this.width = width;
|
|
1463
|
+
this.height = height;
|
|
1464
|
+
this.uploadError = uploadError;
|
|
1465
|
+
|
|
1466
|
+
this.editor = $getEditor();
|
|
1268
1467
|
}
|
|
1269
1468
|
|
|
1270
1469
|
createDOM() {
|
|
1271
|
-
|
|
1470
|
+
if (this.uploadError) return this.createDOMForError()
|
|
1471
|
+
if (this.pendingPreview) return this.#createDOMForPendingPreview()
|
|
1272
1472
|
|
|
1273
|
-
figure
|
|
1473
|
+
const figure = this.createAttachmentFigure();
|
|
1274
1474
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1475
|
+
if (this.isPreviewableAttachment) {
|
|
1476
|
+
figure.appendChild(this.#createDOMForImage());
|
|
1477
|
+
figure.appendChild(this.#createEditableCaption());
|
|
1478
|
+
} else if (this.isVideo) {
|
|
1479
|
+
figure.appendChild(this.#createDOMForFile());
|
|
1480
|
+
figure.appendChild(this.#createEditableCaption());
|
|
1481
|
+
} else {
|
|
1482
|
+
figure.appendChild(this.#createDOMForFile());
|
|
1483
|
+
figure.appendChild(this.#createDOMForNotImage());
|
|
1484
|
+
}
|
|
1277
1485
|
|
|
1278
1486
|
return figure
|
|
1279
1487
|
}
|
|
1280
1488
|
|
|
1281
|
-
updateDOM() {
|
|
1489
|
+
updateDOM(prevNode, dom) {
|
|
1490
|
+
if (this.uploadError !== prevNode.uploadError) return true
|
|
1491
|
+
|
|
1492
|
+
const caption = dom.querySelector("figcaption textarea");
|
|
1493
|
+
if (caption && this.caption) {
|
|
1494
|
+
caption.value = this.caption;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1282
1497
|
return false
|
|
1283
1498
|
}
|
|
1284
1499
|
|
|
1285
1500
|
getTextContent() {
|
|
1286
|
-
return
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
getReadableTextContent() {
|
|
1290
|
-
return this.plainText || `[${this.contentType}]`
|
|
1501
|
+
return `[${this.caption || this.fileName}]\n\n`
|
|
1291
1502
|
}
|
|
1292
1503
|
|
|
1293
1504
|
isInline() {
|
|
1294
|
-
return
|
|
1505
|
+
return this.isAttached() && !this.getParent().is($getNearestRootOrShadowRoot(this))
|
|
1295
1506
|
}
|
|
1296
1507
|
|
|
1297
1508
|
exportDOM() {
|
|
1298
1509
|
const attachment = createElement(this.tagName, {
|
|
1299
1510
|
sgid: this.sgid,
|
|
1300
|
-
|
|
1301
|
-
|
|
1511
|
+
previewable: this.previewable || null,
|
|
1512
|
+
url: this.src,
|
|
1513
|
+
alt: this.altText,
|
|
1514
|
+
caption: this.caption,
|
|
1515
|
+
"content-type": this.contentType,
|
|
1516
|
+
filename: this.fileName,
|
|
1517
|
+
filesize: this.fileSize,
|
|
1518
|
+
width: this.width,
|
|
1519
|
+
height: this.height,
|
|
1520
|
+
presentation: "gallery"
|
|
1302
1521
|
});
|
|
1303
1522
|
|
|
1304
1523
|
return { element: attachment }
|
|
@@ -1306,1894 +1525,1699 @@ class CustomActionTextAttachmentNode extends DecoratorNode {
|
|
|
1306
1525
|
|
|
1307
1526
|
exportJSON() {
|
|
1308
1527
|
return {
|
|
1309
|
-
type: "
|
|
1528
|
+
type: "action_text_attachment",
|
|
1310
1529
|
version: 1,
|
|
1311
1530
|
tagName: this.tagName,
|
|
1312
1531
|
sgid: this.sgid,
|
|
1532
|
+
src: this.src,
|
|
1533
|
+
previewable: this.previewable,
|
|
1534
|
+
altText: this.altText,
|
|
1535
|
+
caption: this.caption,
|
|
1313
1536
|
contentType: this.contentType,
|
|
1314
|
-
|
|
1315
|
-
|
|
1537
|
+
fileName: this.fileName,
|
|
1538
|
+
fileSize: this.fileSize,
|
|
1539
|
+
width: this.width,
|
|
1540
|
+
height: this.height
|
|
1316
1541
|
}
|
|
1317
1542
|
}
|
|
1318
1543
|
|
|
1319
1544
|
decorate() {
|
|
1320
1545
|
return null
|
|
1321
1546
|
}
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
function $createNodeSelectionWith(...nodes) {
|
|
1325
|
-
const selection = $createNodeSelection();
|
|
1326
|
-
nodes.forEach(node => selection.add(node.getKey()));
|
|
1327
|
-
return selection
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
function $isShadowRoot(node) {
|
|
1331
|
-
return $isElementNode(node) && $isRootOrShadowRoot(node) && !$isRootNode(node)
|
|
1332
|
-
}
|
|
1333
1547
|
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
return $wrapNodeInElement(node, parent)
|
|
1340
|
-
} else {
|
|
1341
|
-
return node
|
|
1548
|
+
createDOMForError() {
|
|
1549
|
+
const figure = this.createAttachmentFigure();
|
|
1550
|
+
figure.classList.add("attachment--error");
|
|
1551
|
+
figure.appendChild(createElement("div", { innerText: `Error uploading ${this.fileName || "file"}` }));
|
|
1552
|
+
return figure
|
|
1342
1553
|
}
|
|
1343
|
-
}
|
|
1344
1554
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1555
|
+
createAttachmentFigure(previewable = this.isPreviewableAttachment) {
|
|
1556
|
+
const figure = createAttachmentFigure(this.contentType, previewable, this.fileName);
|
|
1557
|
+
figure.draggable = true;
|
|
1558
|
+
figure.dataset.lexicalNodeKey = this.__key;
|
|
1349
1559
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
return rootElement !== null && rootElement.contains(document.activeElement)
|
|
1353
|
-
}
|
|
1560
|
+
const deleteButton = createElement("lexxy-node-delete-button");
|
|
1561
|
+
figure.appendChild(deleteButton);
|
|
1354
1562
|
|
|
1355
|
-
|
|
1356
|
-
if (atStart === null) {
|
|
1357
|
-
return $isAtNodeEdge(point, true) || $isAtNodeEdge(point, false)
|
|
1358
|
-
} else {
|
|
1359
|
-
return atStart ? $isAtNodeStart(point) : $isAtNodeEnd(point)
|
|
1563
|
+
return figure
|
|
1360
1564
|
}
|
|
1361
|
-
}
|
|
1362
1565
|
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
function extendTextNodeConversion(conversionName, ...callbacks) {
|
|
1368
|
-
return extendConversion(TextNode, conversionName, (conversionOutput, element) => ({
|
|
1369
|
-
...conversionOutput,
|
|
1370
|
-
forChild: (lexicalNode, parentNode) => {
|
|
1371
|
-
const originalForChild = conversionOutput?.forChild ?? (x => x);
|
|
1372
|
-
let childNode = originalForChild(lexicalNode, parentNode);
|
|
1566
|
+
get isPreviewableAttachment() {
|
|
1567
|
+
return this.isPreviewableImage || this.previewable
|
|
1568
|
+
}
|
|
1373
1569
|
|
|
1570
|
+
get isPreviewableImage() {
|
|
1571
|
+
return isPreviewableImage(this.contentType)
|
|
1572
|
+
}
|
|
1374
1573
|
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
childNode
|
|
1379
|
-
);
|
|
1380
|
-
return childNode
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
}))
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
function extendConversion(nodeKlass, conversionName, callback = (output => output)) {
|
|
1387
|
-
return (element) => {
|
|
1388
|
-
const converter = nodeKlass.importDOM()?.[conversionName]?.(element);
|
|
1389
|
-
if (!converter) return null
|
|
1574
|
+
get isVideo() {
|
|
1575
|
+
return this.contentType.startsWith("video/")
|
|
1576
|
+
}
|
|
1390
1577
|
|
|
1391
|
-
|
|
1392
|
-
|
|
1578
|
+
#createDOMForPendingPreview() {
|
|
1579
|
+
const figure = this.createAttachmentFigure(false);
|
|
1580
|
+
figure.appendChild(this.#createDOMForFile());
|
|
1581
|
+
figure.appendChild(this.#createDOMForNotImage());
|
|
1582
|
+
this.#pollForPreview(figure);
|
|
1583
|
+
return figure
|
|
1584
|
+
}
|
|
1393
1585
|
|
|
1394
|
-
|
|
1586
|
+
patchAndRewriteHistory(patch) {
|
|
1587
|
+
this.editor.dispatchCommand(REWRITE_HISTORY_COMMAND, {
|
|
1588
|
+
[this.getKey()]: { patch }
|
|
1589
|
+
});
|
|
1395
1590
|
}
|
|
1396
|
-
}
|
|
1397
1591
|
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1592
|
+
replaceAndRewriteHistory(node) {
|
|
1593
|
+
this.editor.dispatchCommand(REWRITE_HISTORY_COMMAND, {
|
|
1594
|
+
[this.getKey()]: { replace: node }
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1403
1597
|
|
|
1404
|
-
|
|
1598
|
+
#createDOMForImage(options = {}) {
|
|
1599
|
+
const initialSrc = this.previewSrc || this.src;
|
|
1600
|
+
const img = createElement("img", { src: initialSrc, draggable: false, alt: this.altText, ...this.#imageDimensions, ...options });
|
|
1405
1601
|
|
|
1406
|
-
|
|
1407
|
-
|
|
1602
|
+
if (this.previewable && !this.isPreviewableImage) {
|
|
1603
|
+
img.onerror = () => this.#swapPreviewToFileDOM(img);
|
|
1604
|
+
}
|
|
1408
1605
|
|
|
1409
|
-
|
|
1410
|
-
|
|
1606
|
+
if (this.previewSrc) {
|
|
1607
|
+
this.#preloadAndSwapSrc(img);
|
|
1608
|
+
}
|
|
1411
1609
|
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1610
|
+
const container = createElement("div", { className: "attachment__container" });
|
|
1611
|
+
container.appendChild(img);
|
|
1612
|
+
return container
|
|
1613
|
+
}
|
|
1415
1614
|
|
|
1416
|
-
|
|
1417
|
-
|
|
1615
|
+
#preloadAndSwapSrc(img) {
|
|
1616
|
+
const previewSrc = this.previewSrc;
|
|
1617
|
+
const serverImage = new Image();
|
|
1418
1618
|
|
|
1419
|
-
|
|
1420
|
-
|
|
1619
|
+
serverImage.onload = () => this.#handleImageLoaded(img, previewSrc);
|
|
1620
|
+
serverImage.onerror = () => this.#handleImageLoadError(previewSrc);
|
|
1621
|
+
serverImage.src = this.src;
|
|
1622
|
+
}
|
|
1421
1623
|
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
}
|
|
1624
|
+
#handleImageLoaded(img, previewSrc) {
|
|
1625
|
+
img.src = this.src;
|
|
1626
|
+
this.patchAndRewriteHistory({ previewSrc: null });
|
|
1627
|
+
this.#revokePreviewSrc(previewSrc);
|
|
1628
|
+
}
|
|
1427
1629
|
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
}
|
|
1630
|
+
#handleImageLoadError(previewSrc) {
|
|
1631
|
+
this.patchAndRewriteHistory({
|
|
1632
|
+
previewSrc: null,
|
|
1633
|
+
uploadError: true
|
|
1634
|
+
});
|
|
1635
|
+
this.#revokePreviewSrc(previewSrc);
|
|
1435
1636
|
}
|
|
1436
|
-
}
|
|
1437
1637
|
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
// decorator nodes (mentions, attachments whose getTextContent() may return
|
|
1441
|
-
// invisible characters like \ufeff) are treated as non-empty content.
|
|
1442
|
-
function $isListItemStructurallyEmpty(listItem) {
|
|
1443
|
-
const children = listItem.getChildren();
|
|
1444
|
-
for (const child of children) {
|
|
1445
|
-
if ($isDecoratorNode(child)) return false
|
|
1446
|
-
if ($isLineBreakNode(child)) continue
|
|
1447
|
-
if ($isTextNode(child)) {
|
|
1448
|
-
if (child.getTextContent().trim() !== "") return false
|
|
1449
|
-
} else if ($isElementNode(child)) {
|
|
1450
|
-
if (child.getTextContent().trim() !== "") return false
|
|
1451
|
-
}
|
|
1638
|
+
#revokePreviewSrc(previewSrc) {
|
|
1639
|
+
if (previewSrc?.startsWith("blob:")) URL.revokeObjectURL(previewSrc);
|
|
1452
1640
|
}
|
|
1453
|
-
return true
|
|
1454
|
-
}
|
|
1455
1641
|
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
&& index === childCount - 1
|
|
1460
|
-
&& previousNode instanceof CustomActionTextAttachmentNode
|
|
1461
|
-
}
|
|
1642
|
+
#swapPreviewToFileDOM(img) {
|
|
1643
|
+
const figure = img.closest("figure.attachment");
|
|
1644
|
+
if (!figure) return
|
|
1462
1645
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1646
|
+
this.#swapFigureContent(figure, "attachment--preview", "attachment--file", () => {
|
|
1647
|
+
figure.appendChild(this.#createDOMForFile());
|
|
1648
|
+
figure.appendChild(this.#createDOMForNotImage());
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1465
1651
|
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
}
|
|
1652
|
+
#pollForPreview(figure) {
|
|
1653
|
+
let attempt = 0;
|
|
1654
|
+
const maxAttempts = 10;
|
|
1470
1655
|
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
if (!paragraph || !$isParagraphNode(paragraph)) return
|
|
1656
|
+
const tryLoad = () => {
|
|
1657
|
+
if (!this.editor.read(() => this.isAttached())) return
|
|
1474
1658
|
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
const lineBreakCaret = $caretAtNearestNodeOfType(selectionChild, LineBreakNode, direction);
|
|
1478
|
-
if (!lineBreakCaret) return
|
|
1659
|
+
const img = new Image();
|
|
1660
|
+
const cacheBustedSrc = `${this.src}${this.src.includes("?") ? "&" : "?"}_=${Date.now()}`;
|
|
1479
1661
|
|
|
1480
|
-
|
|
1481
|
-
|
|
1662
|
+
img.onload = () => {
|
|
1663
|
+
if (!this.editor.read(() => this.isAttached())) return
|
|
1482
1664
|
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1665
|
+
// The placeholder is a file-type icon SVG (86×100). A real thumbnail
|
|
1666
|
+
// generated from PDF/video content is significantly larger.
|
|
1667
|
+
if (img.naturalWidth > 150 && img.naturalHeight > 150) {
|
|
1668
|
+
this.#swapToPreviewDOM(figure, cacheBustedSrc);
|
|
1669
|
+
} else {
|
|
1670
|
+
retry();
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1673
|
+
img.onerror = () => retry();
|
|
1674
|
+
img.src = cacheBustedSrc;
|
|
1675
|
+
};
|
|
1486
1676
|
|
|
1487
|
-
|
|
1488
|
-
|
|
1677
|
+
const retry = () => {
|
|
1678
|
+
attempt++;
|
|
1679
|
+
if (attempt < maxAttempts && this.editor.read(() => this.isAttached())) {
|
|
1680
|
+
const delay = Math.min(2000 * Math.pow(1.5, attempt), 15000);
|
|
1681
|
+
setTimeout(tryLoad, delay);
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1489
1684
|
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
if (caret.origin instanceof klass) return caret
|
|
1685
|
+
// Give the server time to start processing before the first attempt
|
|
1686
|
+
setTimeout(tryLoad, 3000);
|
|
1493
1687
|
}
|
|
1494
|
-
return null
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
// Shared, strictly-contained element used to attach ephemeral nodes when we
|
|
1498
|
-
// need to read computed styles (e.g. canonicalizing style values, resolving
|
|
1499
|
-
// CSS custom properties). The container is created once and attached to
|
|
1500
|
-
// `document.body` once; subsequent child mutations happen *inside* the
|
|
1501
|
-
// contained subtree so they do not invalidate style on the rest of the page.
|
|
1502
|
-
//
|
|
1503
|
-
// Without this, `document.body.appendChild(...)` / `element.remove()` calls
|
|
1504
|
-
// forced the browser to re-evaluate every ancestor-dependent selector (`:has()`,
|
|
1505
|
-
// descendant combinators, universal sibling rules) across the document on each
|
|
1506
|
-
// invocation — a 13,000+ element style recalc per call on a typical Basecamp
|
|
1507
|
-
// page.
|
|
1508
1688
|
|
|
1509
|
-
|
|
1689
|
+
#swapToPreviewDOM(figure, previewSrc) {
|
|
1690
|
+
this.#swapFigureContent(figure, "attachment--file", "attachment--preview", () => {
|
|
1691
|
+
const img = createElement("img", { src: previewSrc, draggable: false, alt: this.altText });
|
|
1692
|
+
img.onerror = () => this.#swapPreviewToFileDOM(img);
|
|
1693
|
+
const container = createElement("div", { className: "attachment__container" });
|
|
1694
|
+
container.appendChild(img);
|
|
1695
|
+
figure.appendChild(container);
|
|
1696
|
+
figure.appendChild(this.#createEditableCaption());
|
|
1697
|
+
});
|
|
1510
1698
|
|
|
1511
|
-
|
|
1512
|
-
|
|
1699
|
+
this.patchAndRewriteHistory({ pendingPreview: false });
|
|
1700
|
+
}
|
|
1513
1701
|
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
resolverRoot.setAttribute("data-lexxy-style-resolver", "");
|
|
1517
|
-
// `contain: strict` (size, layout, paint, style) isolates everything.
|
|
1518
|
-
// The root itself paints nothing (visibility hidden), has zero
|
|
1519
|
-
// geometric impact (position fixed, intrinsic size via contain), and
|
|
1520
|
-
// never leaks style invalidation to its ancestors.
|
|
1521
|
-
resolverRoot.style.cssText = "contain: strict; position: fixed; top: 0; left: 0; visibility: hidden; pointer-events: none; width: 0; height: 0;";
|
|
1522
|
-
document.body.appendChild(resolverRoot);
|
|
1523
|
-
return resolverRoot
|
|
1524
|
-
}
|
|
1702
|
+
#swapFigureContent(figure, fromClass, toClass, renderContent) {
|
|
1703
|
+
figure.className = figure.className.replace(fromClass, toClass);
|
|
1525
1704
|
|
|
1526
|
-
|
|
1527
|
-
|
|
1705
|
+
for (const child of [ ...figure.querySelectorAll(".attachment__container, .attachment__icon, figcaption") ]) {
|
|
1706
|
+
child.remove();
|
|
1707
|
+
}
|
|
1528
1708
|
|
|
1529
|
-
|
|
1530
|
-
return hasHighlightStyles(selection.style)
|
|
1531
|
-
} else {
|
|
1532
|
-
return selection.hasFormat("highlight")
|
|
1709
|
+
renderContent();
|
|
1533
1710
|
}
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
function getHighlightStyles(selection) {
|
|
1537
|
-
if (!$isRangeSelection(selection)) return null
|
|
1538
1711
|
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1712
|
+
get #imageDimensions() {
|
|
1713
|
+
if (this.width && this.height) {
|
|
1714
|
+
return { width: this.width, height: this.height }
|
|
1715
|
+
} else {
|
|
1716
|
+
return {}
|
|
1544
1717
|
}
|
|
1545
1718
|
}
|
|
1546
1719
|
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1720
|
+
#createDOMForFile() {
|
|
1721
|
+
const extension = this.fileName ? this.fileName.split(".").pop().toLowerCase() : "unknown";
|
|
1722
|
+
return createElement("span", { className: "attachment__icon", textContent: `${extension}` })
|
|
1723
|
+
}
|
|
1550
1724
|
|
|
1551
|
-
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
function hasHighlightStyles(cssOrStyles) {
|
|
1555
|
-
const styles = typeof cssOrStyles === "string" ? getStyleObjectFromCSS(cssOrStyles) : cssOrStyles;
|
|
1556
|
-
return !!(styles.color || styles["background-color"])
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
function applyCanonicalizers(styles, canonicalizers = []) {
|
|
1560
|
-
return canonicalizers.reduce((css, canonicalizer) => {
|
|
1561
|
-
return canonicalizer.applyCanonicalization(css)
|
|
1562
|
-
}, styles)
|
|
1563
|
-
}
|
|
1725
|
+
#createDOMForNotImage() {
|
|
1726
|
+
const figcaption = createElement("figcaption", { className: "attachment__caption" });
|
|
1564
1727
|
|
|
1565
|
-
|
|
1566
|
-
constructor(property, allowedValues= []) {
|
|
1567
|
-
this._property = property;
|
|
1568
|
-
this._allowedValues = allowedValues;
|
|
1569
|
-
this._canonicalValues = this.#allowedValuesIdentityObject;
|
|
1570
|
-
}
|
|
1728
|
+
const nameTag = createElement("strong", { className: "attachment__name", textContent: this.caption || this.fileName });
|
|
1571
1729
|
|
|
1572
|
-
|
|
1573
|
-
const styles = { ...getStyleObjectFromCSS(css) };
|
|
1730
|
+
figcaption.appendChild(nameTag);
|
|
1574
1731
|
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1732
|
+
if (this.fileSize) {
|
|
1733
|
+
const sizeSpan = createElement("span", { className: "attachment__size", textContent: bytesToHumanSize(this.fileSize) });
|
|
1734
|
+
figcaption.appendChild(sizeSpan);
|
|
1578
1735
|
}
|
|
1579
1736
|
|
|
1580
|
-
return
|
|
1737
|
+
return figcaption
|
|
1581
1738
|
}
|
|
1582
1739
|
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1740
|
+
#createEditableCaption() {
|
|
1741
|
+
const caption = createElement("figcaption", { className: "attachment__caption" });
|
|
1742
|
+
const input = createElement("textarea", {
|
|
1743
|
+
value: this.caption,
|
|
1744
|
+
placeholder: this.fileName,
|
|
1745
|
+
rows: "1"
|
|
1746
|
+
});
|
|
1586
1747
|
|
|
1587
|
-
|
|
1748
|
+
input.addEventListener("focusin", () => input.placeholder = "Add caption...");
|
|
1749
|
+
input.addEventListener("blur", (event) => this.#handleCaptionInputBlurred(event));
|
|
1750
|
+
input.addEventListener("keydown", (event) => this.#handleCaptionInputKeydown(event));
|
|
1751
|
+
input.addEventListener("copy", (event) => event.stopPropagation());
|
|
1752
|
+
input.addEventListener("cut", (event) => event.stopPropagation());
|
|
1753
|
+
input.addEventListener("paste", (event) => event.stopPropagation());
|
|
1588
1754
|
|
|
1589
|
-
|
|
1590
|
-
|
|
1755
|
+
caption.appendChild(input);
|
|
1756
|
+
|
|
1757
|
+
return caption
|
|
1591
1758
|
}
|
|
1592
1759
|
|
|
1593
|
-
#
|
|
1594
|
-
|
|
1595
|
-
if (index === -1) {
|
|
1596
|
-
index = this.#computedAllowedValues.indexOf(computeStyleValues(this._property, [ value ])[0]);
|
|
1597
|
-
}
|
|
1598
|
-
return index === -1 ? null : this._allowedValues[index]
|
|
1760
|
+
#handleCaptionInputBlurred(event) {
|
|
1761
|
+
this.#updateCaptionValueFromInput(event.target);
|
|
1599
1762
|
}
|
|
1600
1763
|
|
|
1601
|
-
|
|
1602
|
-
|
|
1764
|
+
#updateCaptionValueFromInput(input) {
|
|
1765
|
+
input.placeholder = this.fileName;
|
|
1766
|
+
this.editor.update(() => {
|
|
1767
|
+
this.getWritable().caption = input.value;
|
|
1768
|
+
});
|
|
1603
1769
|
}
|
|
1604
|
-
}
|
|
1605
1770
|
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
// page-wide style recalc on every canonicalization pass.
|
|
1611
|
-
function computeStyleValues(property, values) {
|
|
1612
|
-
const fragment = document.createDocumentFragment();
|
|
1771
|
+
#handleCaptionInputKeydown(event) {
|
|
1772
|
+
if (event.key === "Enter") {
|
|
1773
|
+
event.preventDefault();
|
|
1774
|
+
event.target.blur();
|
|
1613
1775
|
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1776
|
+
this.editor.update(() => {
|
|
1777
|
+
// Place the cursor after the current image
|
|
1778
|
+
this.selectNext(0, 0);
|
|
1779
|
+
}, {
|
|
1780
|
+
tag: HISTORY_MERGE_TAG
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1619
1783
|
|
|
1620
|
-
|
|
1784
|
+
// Stop all keydown events from bubbling to the Lexical root element.
|
|
1785
|
+
// The caption textarea is outside Lexical's content model and should
|
|
1786
|
+
// handle its own keyboard events natively (Ctrl+A, Ctrl+C, Ctrl+X, etc.).
|
|
1787
|
+
event.stopPropagation();
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1621
1790
|
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1791
|
+
function $createActionTextAttachmentNode(...args) {
|
|
1792
|
+
return new ActionTextAttachmentNode(...args)
|
|
1793
|
+
}
|
|
1625
1794
|
|
|
1626
|
-
|
|
1627
|
-
return
|
|
1795
|
+
function $isActionTextAttachmentNode(node) {
|
|
1796
|
+
return node instanceof ActionTextAttachmentNode
|
|
1628
1797
|
}
|
|
1629
1798
|
|
|
1630
|
-
|
|
1631
|
-
|
|
1799
|
+
function $generateFilteredNodesFromDOM(editorElement, doc) {
|
|
1800
|
+
const nodes = $generateNodesFromDOM(editorElement.editor, doc);
|
|
1801
|
+
return filterDisallowedAttachmentNodes(nodes, editorElement)
|
|
1802
|
+
}
|
|
1632
1803
|
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1804
|
+
function filterDisallowedAttachmentNodes(nodes, editorElement) {
|
|
1805
|
+
return nodes
|
|
1806
|
+
.filter(node => !isDisallowedAttachment(node, editorElement))
|
|
1807
|
+
.map(node => {
|
|
1808
|
+
$descendantsMatching([ node ], descendant => isDisallowedAttachment(descendant, editorElement))
|
|
1809
|
+
.forEach(descendant => descendant.remove());
|
|
1810
|
+
return node
|
|
1811
|
+
})
|
|
1812
|
+
}
|
|
1636
1813
|
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1814
|
+
function isDisallowedAttachment(node, editorElement) {
|
|
1815
|
+
const isAttachmentNode =
|
|
1816
|
+
node instanceof CustomActionTextAttachmentNode ||
|
|
1817
|
+
node instanceof ActionTextAttachmentNode;
|
|
1818
|
+
return isAttachmentNode &&
|
|
1819
|
+
!editorElement.permitsAttachmentContentType(node.contentType)
|
|
1820
|
+
}
|
|
1640
1821
|
|
|
1641
|
-
|
|
1642
|
-
|
|
1822
|
+
class HorizontalDividerNode extends DecoratorNode {
|
|
1823
|
+
static getType() {
|
|
1824
|
+
return "horizontal_divider"
|
|
1643
1825
|
}
|
|
1644
1826
|
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
return true
|
|
1827
|
+
static clone(node) {
|
|
1828
|
+
return new HorizontalDividerNode(node.__key)
|
|
1648
1829
|
}
|
|
1649
1830
|
|
|
1650
|
-
|
|
1651
|
-
return
|
|
1831
|
+
static importJSON(serializedNode) {
|
|
1832
|
+
return new HorizontalDividerNode()
|
|
1652
1833
|
}
|
|
1653
1834
|
|
|
1654
|
-
|
|
1655
|
-
return
|
|
1835
|
+
static importDOM() {
|
|
1836
|
+
return {
|
|
1837
|
+
"hr": (hr) => {
|
|
1838
|
+
return {
|
|
1839
|
+
conversion: () => ({
|
|
1840
|
+
node: new HorizontalDividerNode()
|
|
1841
|
+
}),
|
|
1842
|
+
priority: 1
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1656
1846
|
}
|
|
1657
1847
|
|
|
1658
|
-
|
|
1659
|
-
|
|
1848
|
+
constructor(key) {
|
|
1849
|
+
super(key);
|
|
1660
1850
|
}
|
|
1661
|
-
}
|
|
1662
1851
|
|
|
1663
|
-
|
|
1664
|
-
const
|
|
1665
|
-
const
|
|
1852
|
+
createDOM() {
|
|
1853
|
+
const figure = createElement("figure", { className: "horizontal-divider" });
|
|
1854
|
+
const hr = createElement("hr");
|
|
1666
1855
|
|
|
1667
|
-
|
|
1668
|
-
parse: (value) => value || false
|
|
1669
|
-
});
|
|
1856
|
+
figure.appendChild(hr);
|
|
1670
1857
|
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
// reads this map and re-applies the highlight styles. Scoped per editor instance
|
|
1674
|
-
// so entries don't leak across editors or outlive a torn-down editor.
|
|
1675
|
-
const pendingCodeHighlights = new WeakMap();
|
|
1858
|
+
const deleteButton = createElement("lexxy-node-delete-button");
|
|
1859
|
+
figure.appendChild(deleteButton);
|
|
1676
1860
|
|
|
1677
|
-
|
|
1678
|
-
get enabled() {
|
|
1679
|
-
return this.editorElement.supportsRichText
|
|
1861
|
+
return figure
|
|
1680
1862
|
}
|
|
1681
1863
|
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
name: "lexxy/highlight",
|
|
1686
|
-
config: {
|
|
1687
|
-
color: { buttons: [], permit: [] },
|
|
1688
|
-
"background-color": { buttons: [], permit: [] }
|
|
1689
|
-
},
|
|
1690
|
-
html: {
|
|
1691
|
-
import: {
|
|
1692
|
-
mark: $markConversion
|
|
1693
|
-
}
|
|
1694
|
-
},
|
|
1695
|
-
register(editor, config) {
|
|
1696
|
-
// keep the ref to the canonicalizers for optimized css conversion
|
|
1697
|
-
const canonicalizers = buildCanonicalizers(config);
|
|
1698
|
-
|
|
1699
|
-
// Register the <pre> converter directly in the conversion cache so it
|
|
1700
|
-
// coexists with other extensions' "pre" converters (the extension-level
|
|
1701
|
-
// html.import uses Object.assign, which means only one "pre" per key).
|
|
1702
|
-
$registerPreConversion(editor);
|
|
1703
|
-
|
|
1704
|
-
return mergeRegister(
|
|
1705
|
-
editor.registerCommand(TOGGLE_HIGHLIGHT_COMMAND, (styles) => $toggleSelectionStyles(editor, styles), COMMAND_PRIORITY_NORMAL),
|
|
1706
|
-
editor.registerCommand(REMOVE_HIGHLIGHT_COMMAND, () => $toggleSelectionStyles(editor, BLANK_STYLES), COMMAND_PRIORITY_NORMAL),
|
|
1707
|
-
editor.registerNodeTransform(TextNode, $syncHighlightWithStyle),
|
|
1708
|
-
editor.registerNodeTransform(CodeHighlightNode, $syncHighlightWithCodeHighlightNode),
|
|
1709
|
-
editor.registerNodeTransform(TextNode, (textNode) => $canonicalizePastedStyles(textNode, canonicalizers)),
|
|
1710
|
-
editor.registerMutationListener(CodeNode, (mutations) => {
|
|
1711
|
-
$applyPendingCodeHighlights(editor, mutations);
|
|
1712
|
-
}, { skipInitialization: true })
|
|
1713
|
-
)
|
|
1714
|
-
}
|
|
1715
|
-
});
|
|
1864
|
+
updateDOM() {
|
|
1865
|
+
return true
|
|
1866
|
+
}
|
|
1716
1867
|
|
|
1717
|
-
|
|
1868
|
+
getTextContent() {
|
|
1869
|
+
return "┄\n\n"
|
|
1718
1870
|
}
|
|
1719
|
-
}
|
|
1720
1871
|
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
"background-color": element.style?.backgroundColor
|
|
1725
|
-
};
|
|
1872
|
+
isInline() {
|
|
1873
|
+
return false
|
|
1874
|
+
}
|
|
1726
1875
|
|
|
1727
|
-
|
|
1728
|
-
|
|
1876
|
+
exportDOM() {
|
|
1877
|
+
const hr = createElement("hr");
|
|
1878
|
+
return { element: hr }
|
|
1879
|
+
}
|
|
1729
1880
|
|
|
1730
|
-
|
|
1731
|
-
return
|
|
1881
|
+
exportJSON() {
|
|
1882
|
+
return {
|
|
1883
|
+
type: "horizontal_divider",
|
|
1884
|
+
version: 1
|
|
1885
|
+
}
|
|
1732
1886
|
}
|
|
1733
|
-
}
|
|
1734
1887
|
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
conversion: extendTextNodeConversion("mark", $applyHighlightStyle),
|
|
1738
|
-
priority: 1
|
|
1888
|
+
decorate() {
|
|
1889
|
+
return null
|
|
1739
1890
|
}
|
|
1740
1891
|
}
|
|
1741
1892
|
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
preEntries = [];
|
|
1752
|
-
editor._htmlConversions.set("pre", preEntries);
|
|
1753
|
-
}
|
|
1754
|
-
preEntries.push($preConversionWithHighlightsFactory(editor));
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
// Returns a <pre> converter factory scoped to a specific editor instance.
|
|
1758
|
-
// The factory extracts highlight ranges from <mark> elements before the code
|
|
1759
|
-
// retokenizer can destroy them. The ranges are stored in pendingCodeHighlights
|
|
1760
|
-
// and applied after retokenization via a mutation listener.
|
|
1761
|
-
function $preConversionWithHighlightsFactory(editor) {
|
|
1762
|
-
return function $preConversionWithHighlights(domNode) {
|
|
1763
|
-
const highlights = extractHighlightRanges(domNode);
|
|
1764
|
-
if (highlights.length === 0) return null
|
|
1765
|
-
|
|
1766
|
-
return {
|
|
1767
|
-
conversion: (domNode) => {
|
|
1768
|
-
const language = domNode.getAttribute("data-language");
|
|
1769
|
-
const codeNode = $createCodeNode(language);
|
|
1770
|
-
$getPendingHighlights(editor).set(codeNode.getKey(), highlights);
|
|
1771
|
-
return { node: codeNode }
|
|
1772
|
-
},
|
|
1773
|
-
priority: 2
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
// Walk the DOM tree inside a <pre> element and build a list of
|
|
1779
|
-
// { start, end, style } ranges for every <mark> element found.
|
|
1780
|
-
function extractHighlightRanges(preElement) {
|
|
1781
|
-
const ranges = [];
|
|
1782
|
-
const codeElement = preElement.querySelector("code") || preElement;
|
|
1783
|
-
|
|
1784
|
-
let offset = 0;
|
|
1785
|
-
|
|
1786
|
-
function walk(node) {
|
|
1787
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
1788
|
-
offset += node.textContent.length;
|
|
1789
|
-
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
1790
|
-
// <br> maps to a LineBreakNode (1 character) in Lexical
|
|
1791
|
-
if (node.tagName === "BR") {
|
|
1792
|
-
offset += 1;
|
|
1793
|
-
return
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
const isMark = node.tagName === "MARK";
|
|
1797
|
-
const start = offset;
|
|
1798
|
-
|
|
1799
|
-
for (const child of node.childNodes) {
|
|
1800
|
-
walk(child);
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
if (isMark) {
|
|
1804
|
-
const style = extractHighlightStyleFromElement(node);
|
|
1805
|
-
if (style) {
|
|
1806
|
-
ranges.push({ start, end: offset, style });
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
for (const child of codeElement.childNodes) {
|
|
1813
|
-
walk(child);
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
return ranges
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
function $getPendingHighlights(editor) {
|
|
1820
|
-
let map = pendingCodeHighlights.get(editor);
|
|
1821
|
-
if (!map) {
|
|
1822
|
-
map = new Map();
|
|
1823
|
-
pendingCodeHighlights.set(editor, map);
|
|
1824
|
-
}
|
|
1825
|
-
return map
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
function extractHighlightStyleFromElement(element) {
|
|
1829
|
-
const styles = {};
|
|
1830
|
-
if (element.style?.color) styles.color = element.style.color;
|
|
1831
|
-
if (element.style?.backgroundColor) styles["background-color"] = element.style.backgroundColor;
|
|
1832
|
-
const css = getCSSFromStyleObject(styles);
|
|
1833
|
-
return css.length > 0 ? css : null
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
// Called from the CodeNode mutation listener after the retokenizer has
|
|
1837
|
-
// replaced TextNodes with fresh CodeHighlightNodes.
|
|
1838
|
-
function $applyPendingCodeHighlights(editor, mutations) {
|
|
1839
|
-
const pending = $getPendingHighlights(editor);
|
|
1840
|
-
const keysToProcess = [];
|
|
1893
|
+
const HORIZONTAL_DIVIDER = {
|
|
1894
|
+
dependencies: [ HorizontalDividerNode ],
|
|
1895
|
+
export: (node) => {
|
|
1896
|
+
return node instanceof HorizontalDividerNode ? "---" : null
|
|
1897
|
+
},
|
|
1898
|
+
regExpStart: /^-{3,}\s?$/,
|
|
1899
|
+
replace: (parentNode, children, match, endMatch, linesInBetween, isImport) => {
|
|
1900
|
+
const hrNode = new HorizontalDividerNode();
|
|
1901
|
+
parentNode.replace(hrNode);
|
|
1841
1902
|
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1903
|
+
if (!isImport) {
|
|
1904
|
+
const paragraph = $createParagraphNode();
|
|
1905
|
+
hrNode.insertAfter(paragraph);
|
|
1906
|
+
paragraph.select();
|
|
1845
1907
|
}
|
|
1846
|
-
}
|
|
1908
|
+
},
|
|
1909
|
+
type: "multiline-element"
|
|
1910
|
+
};
|
|
1847
1911
|
|
|
1848
|
-
|
|
1912
|
+
const PUNCTUATION_OR_SPACE = /[^\w]/;
|
|
1849
1913
|
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1914
|
+
// Supplements Lexical's built-in registerMarkdownShortcuts to handle the case
|
|
1915
|
+
// where a user types a leading tag before text that already ends with a
|
|
1916
|
+
// trailing tag (e.g. typing ` before `hello`` or ** before **hello**).
|
|
1917
|
+
//
|
|
1918
|
+
// Lexical's markdown shortcut handler only triggers format transformations when
|
|
1919
|
+
// the closing tag is the character just typed. When the opening tag is typed
|
|
1920
|
+
// instead (e.g. typing ` before `hello`` to form ``hello``), the built-in
|
|
1921
|
+
// handler doesn't match because it looks backward from the cursor for an
|
|
1922
|
+
// opening tag, but the cursor is right after it.
|
|
1923
|
+
//
|
|
1924
|
+
// This listener detects that scenario for ALL text format transformers
|
|
1925
|
+
// (backtick, bold, italic, strikethrough, etc.) and applies the appropriate
|
|
1926
|
+
// format.
|
|
1927
|
+
function registerMarkdownLeadingTagHandler(editor, transformers) {
|
|
1928
|
+
const textFormatTransformers = transformers
|
|
1929
|
+
.filter(t => t.type === "text-format")
|
|
1930
|
+
.sort((a, b) => b.tag.length - a.tag.length); // Longer tags first
|
|
1857
1931
|
|
|
1858
|
-
|
|
1859
|
-
|
|
1932
|
+
return editor.registerUpdateListener(({ tags, dirtyLeaves, editorState, prevEditorState }) => {
|
|
1933
|
+
if (tags.has("historic") || tags.has("collaboration")) return
|
|
1934
|
+
if (editor.isComposing()) return
|
|
1860
1935
|
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
}, { skipTransforms: true, discrete: true });
|
|
1864
|
-
}
|
|
1936
|
+
const selection = editorState.read($getSelection);
|
|
1937
|
+
const prevSelection = prevEditorState.read($getSelection);
|
|
1865
1938
|
|
|
1866
|
-
|
|
1867
|
-
// of a CodeNode, splitting nodes at range boundaries as needed.
|
|
1868
|
-
// We can't use TextNode.splitText() because it creates TextNode
|
|
1869
|
-
// instances (not CodeHighlightNodes) for the split parts. Instead,
|
|
1870
|
-
// we manually create CodeHighlightNode replacements.
|
|
1871
|
-
function $applyHighlightRangesToCodeNode(codeNode, highlights) {
|
|
1872
|
-
if (highlights.length === 0) return
|
|
1939
|
+
if (!$isRangeSelection(prevSelection) || !$isRangeSelection(selection) || !selection.isCollapsed()) return
|
|
1873
1940
|
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
// earlier ranges may have split nodes, invalidating previous mappings.
|
|
1877
|
-
const childRanges = $buildChildRanges(codeNode);
|
|
1941
|
+
const anchorKey = selection.anchor.key;
|
|
1942
|
+
const anchorOffset = selection.anchor.offset;
|
|
1878
1943
|
|
|
1879
|
-
|
|
1880
|
-
// Skip plain TextNodes: only CodeHighlightNodes can be split into
|
|
1881
|
-
// styled replacements here. The retokenizer normally converts any
|
|
1882
|
-
// TextNode children back to CodeHighlightNodes before this runs,
|
|
1883
|
-
// but the iteration over $buildChildRanges has to keep counting
|
|
1884
|
-
// them so character offsets stay aligned with the saved ranges.
|
|
1885
|
-
if (!$isCodeHighlightNode(node)) continue
|
|
1944
|
+
if (!dirtyLeaves.has(anchorKey)) return
|
|
1886
1945
|
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
const overlapEnd = Math.min(hlEnd, nodeEnd);
|
|
1946
|
+
const anchorNode = editorState.read(() => $getNodeByKey(anchorKey));
|
|
1947
|
+
if (!$isTextNode(anchorNode)) return
|
|
1890
1948
|
|
|
1891
|
-
|
|
1949
|
+
// Only trigger when cursor moved forward (typing)
|
|
1950
|
+
const prevOffset = prevSelection.anchor.key === anchorKey ? prevSelection.anchor.offset : 0;
|
|
1951
|
+
if (anchorOffset <= prevOffset) return
|
|
1892
1952
|
|
|
1893
|
-
|
|
1894
|
-
const relStart = overlapStart - nodeStart;
|
|
1895
|
-
const relEnd = overlapEnd - nodeStart;
|
|
1896
|
-
const nodeLength = nodeEnd - nodeStart;
|
|
1953
|
+
const textContent = editorState.read(() => anchorNode.getTextContent());
|
|
1897
1954
|
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
} else {
|
|
1903
|
-
// Need to split: replace the node with 2 or 3 CodeHighlightNodes
|
|
1904
|
-
const text = node.getTextContent();
|
|
1905
|
-
const highlightType = node.getHighlightType();
|
|
1906
|
-
const replacements = [];
|
|
1955
|
+
// Try each transformer, longest tags first
|
|
1956
|
+
for (const transformer of textFormatTransformers) {
|
|
1957
|
+
const tag = transformer.tag;
|
|
1958
|
+
const tagLen = tag.length;
|
|
1907
1959
|
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1960
|
+
// The typed characters must end at the cursor position and form the opening tag
|
|
1961
|
+
const openTagStart = anchorOffset - tagLen;
|
|
1962
|
+
if (openTagStart < 0) continue
|
|
1911
1963
|
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
$setCodeHighlightFormat(styledNode, true);
|
|
1915
|
-
replacements.push(styledNode);
|
|
1964
|
+
const candidateOpenTag = textContent.slice(openTagStart, anchorOffset);
|
|
1965
|
+
if (candidateOpenTag !== tag) continue
|
|
1916
1966
|
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1967
|
+
// Disambiguate from longer tags: if the character before the opening tag
|
|
1968
|
+
// is the same as the tag character, this might be part of a longer tag
|
|
1969
|
+
// (e.g. seeing `*` when the user is actually typing `**`)
|
|
1970
|
+
const tagChar = tag[0];
|
|
1971
|
+
if (openTagStart > 0 && textContent[openTagStart - 1] === tagChar) continue
|
|
1920
1972
|
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1973
|
+
// Check intraword constraint: if intraword is false, the character before
|
|
1974
|
+
// the opening tag must be a space, punctuation, or the start of the text
|
|
1975
|
+
if (transformer.intraword === false && openTagStart > 0) {
|
|
1976
|
+
const beforeChar = textContent[openTagStart - 1];
|
|
1977
|
+
if (beforeChar && !PUNCTUATION_OR_SPACE.test(beforeChar)) continue
|
|
1925
1978
|
}
|
|
1926
|
-
}
|
|
1927
|
-
}
|
|
1928
|
-
}
|
|
1929
1979
|
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
for (const child of codeNode.getChildren()) {
|
|
1935
|
-
if ($isCodeHighlightNode(child) || $isTextNode(child)) {
|
|
1936
|
-
const text = child.getTextContent();
|
|
1937
|
-
childRanges.push({ node: child, start: charOffset, end: charOffset + text.length });
|
|
1938
|
-
charOffset += text.length;
|
|
1939
|
-
} else {
|
|
1940
|
-
// LineBreakNode, TabNode - count as 1 character each (\n, \t)
|
|
1941
|
-
charOffset += 1;
|
|
1942
|
-
}
|
|
1943
|
-
}
|
|
1944
|
-
|
|
1945
|
-
return childRanges
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
// Extract highlight ranges from the Lexical node tree of a CodeNode.
|
|
1949
|
-
// This mirrors extractHighlightRanges (which works on DOM elements during
|
|
1950
|
-
// HTML import) but reads from live CodeHighlightNode children instead.
|
|
1951
|
-
function $extractHighlightRangesFromCodeNode(codeNode) {
|
|
1952
|
-
const ranges = [];
|
|
1953
|
-
const childRanges = $buildChildRanges(codeNode);
|
|
1954
|
-
|
|
1955
|
-
for (const { node, start, end } of childRanges) {
|
|
1956
|
-
const style = node.getStyle();
|
|
1957
|
-
if (style && hasHighlightStyles(style)) {
|
|
1958
|
-
ranges.push({ start, end, style });
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
return ranges
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
function buildCanonicalizers(config) {
|
|
1966
|
-
return [
|
|
1967
|
-
new StyleCanonicalizer("color", [ ...config.buttons.color, ...config.permit.color ]),
|
|
1968
|
-
new StyleCanonicalizer("background-color", [ ...config.buttons["background-color"], ...config.permit["background-color"] ])
|
|
1969
|
-
]
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
function $toggleSelectionStyles(editor, styles) {
|
|
1973
|
-
const selection = $getSelection();
|
|
1974
|
-
if (!$isRangeSelection(selection)) return
|
|
1980
|
+
// Search forward for a closing tag in the same text node
|
|
1981
|
+
const searchStart = anchorOffset;
|
|
1982
|
+
const closeTagIndex = textContent.indexOf(tag, searchStart);
|
|
1983
|
+
if (closeTagIndex < 0) continue
|
|
1975
1984
|
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
}
|
|
1985
|
+
// Disambiguate closing tag from longer tags: if the character right after
|
|
1986
|
+
// the closing tag is the same as the tag character, skip
|
|
1987
|
+
// (e.g. `*hello**` — the first `*` at index 6 is part of `**`)
|
|
1988
|
+
if (textContent[closeTagIndex + tagLen] === tagChar) continue
|
|
1981
1989
|
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
$patchStyleText(selection, patch);
|
|
1986
|
-
}
|
|
1987
|
-
}
|
|
1990
|
+
// Also check if the character before the closing tag start is the same
|
|
1991
|
+
// tag character (e.g. the closing tag might be a suffix of a longer sequence)
|
|
1992
|
+
if (closeTagIndex > 0 && textContent[closeTagIndex - 1] === tagChar) continue
|
|
1988
1993
|
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
// (after retokenization) or a plain TextNode (after splitText or before
|
|
1994
|
-
// the retokenizer has run). Check the parent in both cases.
|
|
1995
|
-
if ($isCodeHighlightNode(node) || $isTextNode(node)) {
|
|
1996
|
-
return $isCodeNode(node.getParent())
|
|
1997
|
-
}
|
|
1998
|
-
return $isCodeNode(node)
|
|
1999
|
-
})
|
|
2000
|
-
}
|
|
1994
|
+
// There must be content between the tags (not just empty or whitespace-adjacent)
|
|
1995
|
+
const innerStart = anchorOffset;
|
|
1996
|
+
const innerEnd = closeTagIndex;
|
|
1997
|
+
if (innerEnd <= innerStart) continue
|
|
2001
1998
|
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
// Accept both CodeHighlightNode and TextNode children of a CodeNode
|
|
2005
|
-
// because splitText creates TextNode instances and the retokenizer
|
|
2006
|
-
// may not have converted them back to CodeHighlightNodes yet.
|
|
2007
|
-
const nodeKeys = selection.getNodes()
|
|
2008
|
-
.filter((node) => ($isCodeHighlightNode(node) || $isTextNode(node)) && $isCodeNode(node.getParent()))
|
|
2009
|
-
.map((node) => ({
|
|
2010
|
-
key: node.getKey(),
|
|
2011
|
-
startOffset: $getNodeSelectionOffsets(node, selection)[0],
|
|
2012
|
-
endOffset: $getNodeSelectionOffsets(node, selection)[1],
|
|
2013
|
-
textSize: node.getTextContentSize()
|
|
2014
|
-
}));
|
|
1999
|
+
// No space immediately after opening tag
|
|
2000
|
+
if (textContent[innerStart] === " ") continue
|
|
2015
2001
|
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
// Use discrete to force a synchronous commit, ensuring the changes
|
|
2019
|
-
// are committed before editor.focus() triggers a second update cycle
|
|
2020
|
-
// that would re-run transforms and wipe out the styles.
|
|
2021
|
-
editor.update(() => {
|
|
2022
|
-
const affectedCodeNodes = new Set();
|
|
2002
|
+
// No space immediately before closing tag
|
|
2003
|
+
if (textContent[innerEnd - 1] === " ") continue
|
|
2023
2004
|
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2005
|
+
// Check intraword constraint for closing tag
|
|
2006
|
+
if (transformer.intraword === false) {
|
|
2007
|
+
const afterCloseChar = textContent[closeTagIndex + tagLen];
|
|
2008
|
+
if (afterCloseChar && !PUNCTUATION_OR_SPACE.test(afterCloseChar)) continue
|
|
2009
|
+
}
|
|
2027
2010
|
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2011
|
+
editor.update(() => {
|
|
2012
|
+
const node = $getNodeByKey(anchorKey);
|
|
2013
|
+
if (!node || !$isTextNode(node)) return
|
|
2031
2014
|
|
|
2032
|
-
|
|
2015
|
+
const parent = node.getParent();
|
|
2016
|
+
if (parent === null || $isCodeNode(parent)) return
|
|
2033
2017
|
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
} else {
|
|
2037
|
-
const splitNodes = node.splitText(startOffset, endOffset);
|
|
2038
|
-
const targetNode = splitNodes[startOffset === 0 ? 0 : 1];
|
|
2039
|
-
$applyStylePatchToNode(targetNode, patch);
|
|
2040
|
-
}
|
|
2041
|
-
}
|
|
2018
|
+
$applyFormatFromLeadingTag(node, openTagStart, transformer);
|
|
2019
|
+
});
|
|
2042
2020
|
|
|
2043
|
-
|
|
2044
|
-
// The code retokenizer will replace the styled nodes with fresh unstyled
|
|
2045
|
-
// tokens when transforms run. The pending highlights are picked up by the
|
|
2046
|
-
// CodeNode mutation listener and reapplied after retokenization.
|
|
2047
|
-
for (const codeNode of affectedCodeNodes) {
|
|
2048
|
-
const ranges = $extractHighlightRangesFromCodeNode(codeNode);
|
|
2049
|
-
if (ranges.length > 0) {
|
|
2050
|
-
$getPendingHighlights(editor).set(codeNode.getKey(), ranges);
|
|
2051
|
-
}
|
|
2021
|
+
break // Only apply the first (longest) matching transformer
|
|
2052
2022
|
}
|
|
2053
|
-
}
|
|
2023
|
+
})
|
|
2054
2024
|
}
|
|
2055
2025
|
|
|
2056
|
-
function $
|
|
2057
|
-
const
|
|
2058
|
-
const
|
|
2059
|
-
const
|
|
2060
|
-
const textSize = node.getTextContentSize();
|
|
2026
|
+
function $applyFormatFromLeadingTag(anchorNode, openTagStart, transformer) {
|
|
2027
|
+
const tag = transformer.tag;
|
|
2028
|
+
const tagLen = tag.length;
|
|
2029
|
+
const textContent = anchorNode.getTextContent();
|
|
2061
2030
|
|
|
2062
|
-
const
|
|
2063
|
-
const
|
|
2031
|
+
const innerStart = openTagStart + tagLen;
|
|
2032
|
+
const closeTagIndex = textContent.indexOf(tag, innerStart);
|
|
2033
|
+
if (closeTagIndex < 0) return
|
|
2064
2034
|
|
|
2065
|
-
|
|
2066
|
-
|
|
2035
|
+
const inner = textContent.slice(innerStart, closeTagIndex);
|
|
2036
|
+
if (inner.length === 0) return
|
|
2067
2037
|
|
|
2068
|
-
|
|
2069
|
-
|
|
2038
|
+
// Remove both tags and apply format
|
|
2039
|
+
const before = textContent.slice(0, openTagStart);
|
|
2040
|
+
const after = textContent.slice(closeTagIndex + tagLen);
|
|
2070
2041
|
|
|
2071
|
-
|
|
2072
|
-
if (isAnchor) start = selection.anchor.offset;
|
|
2073
|
-
if (isFocus) end = selection.focus.offset;
|
|
2074
|
-
} else {
|
|
2075
|
-
if (isFocus) start = selection.focus.offset;
|
|
2076
|
-
if (isAnchor) end = selection.anchor.offset;
|
|
2077
|
-
}
|
|
2042
|
+
anchorNode.setTextContent(before + inner + after);
|
|
2078
2043
|
|
|
2079
|
-
|
|
2080
|
-
|
|
2044
|
+
const nextSelection = $createRangeSelection();
|
|
2045
|
+
$setSelection(nextSelection);
|
|
2081
2046
|
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2047
|
+
// Select the inner text to apply formatting
|
|
2048
|
+
nextSelection.anchor.set(anchorNode.getKey(), openTagStart, "text");
|
|
2049
|
+
nextSelection.focus.set(anchorNode.getKey(), openTagStart + inner.length, "text");
|
|
2085
2050
|
|
|
2086
|
-
for (const
|
|
2087
|
-
if (
|
|
2088
|
-
|
|
2089
|
-
} else {
|
|
2090
|
-
newStyles[key] = value;
|
|
2051
|
+
for (const format of transformer.format) {
|
|
2052
|
+
if (!nextSelection.hasFormat(format)) {
|
|
2053
|
+
nextSelection.formatText(format);
|
|
2091
2054
|
}
|
|
2092
2055
|
}
|
|
2093
2056
|
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
// Sync the highlight format using TextNode's setFormat to bypass
|
|
2098
|
-
// CodeHighlightNode's no-op override
|
|
2099
|
-
const shouldHaveHighlight = hasHighlightStyles(newCSSText);
|
|
2100
|
-
const hasHighlight = node.hasFormat("highlight");
|
|
2057
|
+
// Collapse selection to end of formatted text and clear the format
|
|
2058
|
+
// so subsequent typing is plain text
|
|
2059
|
+
nextSelection.anchor.set(nextSelection.focus.key, nextSelection.focus.offset, nextSelection.focus.type);
|
|
2101
2060
|
|
|
2102
|
-
|
|
2103
|
-
|
|
2061
|
+
for (const format of transformer.format) {
|
|
2062
|
+
if (nextSelection.hasFormat(format)) {
|
|
2063
|
+
nextSelection.toggleFormat(format);
|
|
2064
|
+
}
|
|
2104
2065
|
}
|
|
2105
2066
|
}
|
|
2106
2067
|
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2068
|
+
var theme = {
|
|
2069
|
+
text: {
|
|
2070
|
+
bold: "lexxy-content__bold",
|
|
2071
|
+
italic: "lexxy-content__italic",
|
|
2072
|
+
strikethrough: "lexxy-content__strikethrough",
|
|
2073
|
+
underline: "lexxy-content__underline",
|
|
2074
|
+
highlight: "lexxy-content__highlight"
|
|
2075
|
+
},
|
|
2076
|
+
tableCellHeader: "lexxy-content__table-cell--header",
|
|
2077
|
+
tableCellSelected: "lexxy-content__table-cell--selected",
|
|
2078
|
+
tableSelection: "lexxy-content__table--selection",
|
|
2079
|
+
tableScrollableWrapper: "lexxy-content__table-wrapper",
|
|
2080
|
+
tableCellHighlight: "lexxy-content__table-cell--highlight",
|
|
2081
|
+
tableCellFocus: "lexxy-content__table-cell--focus",
|
|
2082
|
+
list: {
|
|
2083
|
+
nested: {
|
|
2084
|
+
listitem: "lexxy-nested-listitem",
|
|
2085
|
+
}
|
|
2086
|
+
},
|
|
2087
|
+
codeHighlight: {
|
|
2088
|
+
addition: "code-token__selector",
|
|
2089
|
+
atrule: "code-token__attr",
|
|
2090
|
+
attr: "code-token__attr",
|
|
2091
|
+
"attr-name": "code-token__attr",
|
|
2092
|
+
"attr-value": "code-token__selector",
|
|
2093
|
+
boolean: "code-token__property",
|
|
2094
|
+
bold: "code-token__variable",
|
|
2095
|
+
builtin: "code-token__selector",
|
|
2096
|
+
cdata: "code-token__comment",
|
|
2097
|
+
char: "code-token__selector",
|
|
2098
|
+
class: "code-token__function",
|
|
2099
|
+
"class-name": "code-token__function",
|
|
2100
|
+
color: "code-token__property",
|
|
2101
|
+
comment: "code-token__comment",
|
|
2102
|
+
constant: "code-token__property",
|
|
2103
|
+
coord: "code-token__comment",
|
|
2104
|
+
decorator: "code-token__function",
|
|
2105
|
+
deleted: "code-token__operator",
|
|
2106
|
+
deletion: "code-token__operator",
|
|
2107
|
+
directive: "code-token__attr",
|
|
2108
|
+
"directive-hash": "code-token__property",
|
|
2109
|
+
doctype: "code-token__comment",
|
|
2110
|
+
entity: "code-token__operator",
|
|
2111
|
+
function: "code-token__function",
|
|
2112
|
+
hexcode: "code-token__property",
|
|
2113
|
+
important: "code-token__function",
|
|
2114
|
+
inserted: "code-token__selector",
|
|
2115
|
+
italic: "code-token__comment",
|
|
2116
|
+
keyword: "code-token__attr",
|
|
2117
|
+
line: "code-token__selector",
|
|
2118
|
+
namespace: "code-token__variable",
|
|
2119
|
+
number: "code-token__property",
|
|
2120
|
+
macro: "code-token__function",
|
|
2121
|
+
operator: "code-token__operator",
|
|
2122
|
+
parameter: "code-token__variable",
|
|
2123
|
+
prolog: "code-token__comment",
|
|
2124
|
+
property: "code-token__property",
|
|
2125
|
+
punctuation: "code-token__punctuation",
|
|
2126
|
+
"raw-string": "code-token__operator",
|
|
2127
|
+
regex: "code-token__variable",
|
|
2128
|
+
script: "code-token__function",
|
|
2129
|
+
selector: "code-token__selector",
|
|
2130
|
+
string: "code-token__selector",
|
|
2131
|
+
style: "code-token__function",
|
|
2132
|
+
symbol: "code-token__property",
|
|
2133
|
+
tag: "code-token__property",
|
|
2134
|
+
title: "code-token__function",
|
|
2135
|
+
"type-definition": "code-token__function",
|
|
2136
|
+
url: "code-token__operator",
|
|
2137
|
+
variable: "code-token__variable",
|
|
2115
2138
|
}
|
|
2116
|
-
}
|
|
2139
|
+
};
|
|
2117
2140
|
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2141
|
+
// Shared, strictly-contained element used to attach ephemeral nodes when we
|
|
2142
|
+
// need to read computed styles (e.g. canonicalizing style values, resolving
|
|
2143
|
+
// CSS custom properties). The container is created once and attached to
|
|
2144
|
+
// `document.body` once; subsequent child mutations happen *inside* the
|
|
2145
|
+
// contained subtree so they do not invalidate style on the rest of the page.
|
|
2146
|
+
//
|
|
2147
|
+
// Without this, `document.body.appendChild(...)` / `element.remove()` calls
|
|
2148
|
+
// forced the browser to re-evaluate every ancestor-dependent selector (`:has()`,
|
|
2149
|
+
// descendant combinators, universal sibling rules) across the document on each
|
|
2150
|
+
// invocation — a 13,000+ element style recalc per call on a typical Basecamp
|
|
2151
|
+
// page.
|
|
2121
2152
|
|
|
2122
|
-
|
|
2123
|
-
if (hasHighlightStyles(textNode.getStyle()) !== textNode.hasFormat("highlight")) {
|
|
2124
|
-
textNode.toggleFormat("highlight");
|
|
2125
|
-
}
|
|
2126
|
-
}
|
|
2153
|
+
let resolverRoot = null;
|
|
2127
2154
|
|
|
2128
|
-
function
|
|
2129
|
-
|
|
2130
|
-
if (!$isCodeNode(parent)) return
|
|
2155
|
+
function styleResolverRoot() {
|
|
2156
|
+
if (resolverRoot && resolverRoot.isConnected) return resolverRoot
|
|
2131
2157
|
|
|
2132
|
-
|
|
2133
|
-
|
|
2158
|
+
resolverRoot = document.createElement("div");
|
|
2159
|
+
resolverRoot.setAttribute("aria-hidden", "true");
|
|
2160
|
+
resolverRoot.setAttribute("data-lexxy-style-resolver", "");
|
|
2161
|
+
// `contain: strict` (size, layout, paint, style) isolates everything.
|
|
2162
|
+
// The root itself paints nothing (visibility hidden), has zero
|
|
2163
|
+
// geometric impact (position fixed, intrinsic size via contain), and
|
|
2164
|
+
// never leaks style invalidation to its ancestors.
|
|
2165
|
+
resolverRoot.style.cssText = "contain: strict; position: fixed; top: 0; left: 0; visibility: hidden; pointer-events: none; width: 0; height: 0;";
|
|
2166
|
+
document.body.appendChild(resolverRoot);
|
|
2167
|
+
return resolverRoot
|
|
2168
|
+
}
|
|
2134
2169
|
|
|
2135
|
-
|
|
2136
|
-
|
|
2170
|
+
function isSelectionHighlighted(selection) {
|
|
2171
|
+
if (!$isRangeSelection(selection)) return false
|
|
2172
|
+
|
|
2173
|
+
if (selection.isCollapsed()) {
|
|
2174
|
+
return hasHighlightStyles(selection.style)
|
|
2175
|
+
} else {
|
|
2176
|
+
return selection.hasFormat("highlight")
|
|
2137
2177
|
}
|
|
2138
2178
|
}
|
|
2139
2179
|
|
|
2140
|
-
function
|
|
2141
|
-
if (
|
|
2142
|
-
$setPastedStyles(textNode, false);
|
|
2143
|
-
|
|
2144
|
-
const canonicalizedCSS = applyCanonicalizers(textNode.getStyle(), canonicalizers);
|
|
2145
|
-
textNode.setStyle(canonicalizedCSS);
|
|
2180
|
+
function getHighlightStyles(selection) {
|
|
2181
|
+
if (!$isRangeSelection(selection)) return null
|
|
2146
2182
|
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2183
|
+
let styles = getStyleObjectFromCSS(selection.style);
|
|
2184
|
+
if (!styles.color && !styles["background-color"]) {
|
|
2185
|
+
const anchorNode = selection.anchor.getNode();
|
|
2186
|
+
if ($isTextNode(anchorNode)) {
|
|
2187
|
+
styles = getStyleObjectFromCSS(anchorNode.getStyle());
|
|
2151
2188
|
}
|
|
2152
2189
|
}
|
|
2153
|
-
}
|
|
2154
2190
|
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2191
|
+
const color = styles.color || null;
|
|
2192
|
+
const backgroundColor = styles["background-color"] || null;
|
|
2193
|
+
if (!color && !backgroundColor) return null
|
|
2158
2194
|
|
|
2159
|
-
|
|
2160
|
-
return $getState(textNode, hasPastedStylesState)
|
|
2195
|
+
return { color, backgroundColor }
|
|
2161
2196
|
}
|
|
2162
2197
|
|
|
2163
|
-
|
|
2164
|
-
"
|
|
2165
|
-
"
|
|
2166
|
-
|
|
2167
|
-
"underline",
|
|
2168
|
-
"link",
|
|
2169
|
-
"unlink",
|
|
2170
|
-
"toggleHighlight",
|
|
2171
|
-
"removeHighlight",
|
|
2172
|
-
"setFormatHeadingLarge",
|
|
2173
|
-
"setFormatHeadingMedium",
|
|
2174
|
-
"setFormatHeadingSmall",
|
|
2175
|
-
"setFormatParagraph",
|
|
2176
|
-
"clearFormatting",
|
|
2177
|
-
"insertUnorderedList",
|
|
2178
|
-
"insertOrderedList",
|
|
2179
|
-
"insertQuoteBlock",
|
|
2180
|
-
"insertCodeBlock",
|
|
2181
|
-
"setCodeLanguage",
|
|
2182
|
-
"insertHorizontalDivider",
|
|
2183
|
-
"uploadImage",
|
|
2184
|
-
"uploadFile",
|
|
2185
|
-
|
|
2186
|
-
"insertTable",
|
|
2187
|
-
|
|
2188
|
-
"undo",
|
|
2189
|
-
"redo"
|
|
2190
|
-
];
|
|
2191
|
-
|
|
2192
|
-
class CommandDispatcher {
|
|
2193
|
-
#selectionBeforeDrag = null
|
|
2194
|
-
#listeners = new ListenerBin()
|
|
2198
|
+
function hasHighlightStyles(cssOrStyles) {
|
|
2199
|
+
const styles = typeof cssOrStyles === "string" ? getStyleObjectFromCSS(cssOrStyles) : cssOrStyles;
|
|
2200
|
+
return !!(styles.color || styles["background-color"])
|
|
2201
|
+
}
|
|
2195
2202
|
|
|
2196
|
-
|
|
2197
|
-
|
|
2203
|
+
function applyCanonicalizers(styles, canonicalizers = []) {
|
|
2204
|
+
return canonicalizers.reduce((css, canonicalizer) => {
|
|
2205
|
+
return canonicalizer.applyCanonicalization(css)
|
|
2206
|
+
}, styles)
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
class StyleCanonicalizer {
|
|
2210
|
+
constructor(property, allowedValues= []) {
|
|
2211
|
+
this._property = property;
|
|
2212
|
+
this._allowedValues = allowedValues;
|
|
2213
|
+
this._canonicalValues = this.#allowedValuesIdentityObject;
|
|
2198
2214
|
}
|
|
2199
2215
|
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
this.editor = editorElement.editor;
|
|
2203
|
-
this.selection = editorElement.selection;
|
|
2204
|
-
this.contents = editorElement.contents;
|
|
2205
|
-
this.clipboard = editorElement.clipboard;
|
|
2216
|
+
applyCanonicalization(css) {
|
|
2217
|
+
const styles = { ...getStyleObjectFromCSS(css) };
|
|
2206
2218
|
|
|
2207
|
-
this
|
|
2208
|
-
this
|
|
2209
|
-
|
|
2210
|
-
|
|
2219
|
+
styles[this._property] = this.getCanonicalAllowedValue(styles[this._property]);
|
|
2220
|
+
if (!styles[this._property]) {
|
|
2221
|
+
delete styles[this._property];
|
|
2222
|
+
}
|
|
2211
2223
|
|
|
2212
|
-
|
|
2213
|
-
return this.clipboard.paste(event)
|
|
2224
|
+
return getCSSFromStyleObject(styles)
|
|
2214
2225
|
}
|
|
2215
2226
|
|
|
2216
|
-
|
|
2217
|
-
this.
|
|
2227
|
+
getCanonicalAllowedValue(value) {
|
|
2228
|
+
return this._canonicalValues[value] ||= this.#resolveCannonicalValue(value)
|
|
2218
2229
|
}
|
|
2219
2230
|
|
|
2220
|
-
|
|
2221
|
-
this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic");
|
|
2222
|
-
}
|
|
2231
|
+
// Private
|
|
2223
2232
|
|
|
2224
|
-
|
|
2225
|
-
this.
|
|
2233
|
+
get #allowedValuesIdentityObject() {
|
|
2234
|
+
return this._allowedValues.reduce((object, value) => ({ ...object, [value]: value }), {})
|
|
2226
2235
|
}
|
|
2227
2236
|
|
|
2228
|
-
|
|
2229
|
-
this.
|
|
2237
|
+
#resolveCannonicalValue(value) {
|
|
2238
|
+
let index = this.#computedAllowedValues.indexOf(value);
|
|
2239
|
+
if (index === -1) {
|
|
2240
|
+
index = this.#computedAllowedValues.indexOf(computeStyleValues(this._property, [ value ])[0]);
|
|
2241
|
+
}
|
|
2242
|
+
return index === -1 ? null : this._allowedValues[index]
|
|
2230
2243
|
}
|
|
2231
2244
|
|
|
2232
|
-
|
|
2233
|
-
this.
|
|
2245
|
+
get #computedAllowedValues() {
|
|
2246
|
+
return this._computedAllowedValues ||= computeStyleValues(this._property, this._allowedValues)
|
|
2234
2247
|
}
|
|
2248
|
+
}
|
|
2235
2249
|
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2250
|
+
// Separates DOM writes from layout reads to avoid forced reflows, and attaches
|
|
2251
|
+
// resolver elements to a strictly-contained root (outside the normal document
|
|
2252
|
+
// flow) so neither the attach nor the detach invalidate styles on the rest of
|
|
2253
|
+
// the page. Without containment, appending to `document.body` triggered a
|
|
2254
|
+
// page-wide style recalc on every canonicalization pass.
|
|
2255
|
+
function computeStyleValues(property, values) {
|
|
2256
|
+
const fragment = document.createDocumentFragment();
|
|
2239
2257
|
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2258
|
+
const elements = values.map(value => {
|
|
2259
|
+
const element = createElement("span", { style: `display: none; ${property}: ${value};` });
|
|
2260
|
+
fragment.appendChild(element);
|
|
2261
|
+
return element
|
|
2262
|
+
});
|
|
2244
2263
|
|
|
2245
|
-
|
|
2264
|
+
styleResolverRoot().appendChild(fragment);
|
|
2246
2265
|
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
autoLinkNode.append(textNode);
|
|
2251
|
-
selection.insertNodes([ autoLinkNode ]);
|
|
2252
|
-
} else {
|
|
2253
|
-
$toggleLink(url);
|
|
2254
|
-
}
|
|
2255
|
-
});
|
|
2256
|
-
}
|
|
2266
|
+
const computed = elements.map(element =>
|
|
2267
|
+
window.getComputedStyle(element).getPropertyValue(property)
|
|
2268
|
+
);
|
|
2257
2269
|
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
if (this.editorElement.adapter.unlinkFrozenNode?.()) {
|
|
2262
|
-
return
|
|
2263
|
-
}
|
|
2270
|
+
elements.forEach(element => element.remove());
|
|
2271
|
+
return computed
|
|
2272
|
+
}
|
|
2264
2273
|
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2274
|
+
const TOGGLE_HIGHLIGHT_COMMAND = createCommand();
|
|
2275
|
+
const REMOVE_HIGHLIGHT_COMMAND = createCommand();
|
|
2276
|
+
const BLANK_STYLES = { "color": null, "background-color": null };
|
|
2268
2277
|
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2278
|
+
const hasPastedStylesState = createState("hasPastedStyles", {
|
|
2279
|
+
parse: (value) => value || false
|
|
2280
|
+
});
|
|
2272
2281
|
|
|
2273
|
-
|
|
2282
|
+
// Stores pending highlight ranges extracted during HTML import, keyed by CodeNode key.
|
|
2283
|
+
// After the code retokenizer creates fresh CodeHighlightNodes, a mutation listener
|
|
2284
|
+
// reads this map and re-applies the highlight styles. Scoped per editor instance
|
|
2285
|
+
// so entries don't leak across editors or outlive a torn-down editor.
|
|
2286
|
+
const pendingCodeHighlights = new WeakMap();
|
|
2274
2287
|
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
this.contents.applyUnorderedListFormat();
|
|
2279
|
-
}
|
|
2288
|
+
class HighlightExtension extends LexxyExtension {
|
|
2289
|
+
get enabled() {
|
|
2290
|
+
return this.editorElement.supportsRichText
|
|
2280
2291
|
}
|
|
2281
2292
|
|
|
2282
|
-
|
|
2283
|
-
const
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2293
|
+
get lexicalExtension() {
|
|
2294
|
+
const extension = defineExtension({
|
|
2295
|
+
dependencies: [ RichTextExtension ],
|
|
2296
|
+
name: "lexxy/highlight",
|
|
2297
|
+
config: {
|
|
2298
|
+
color: { buttons: [], permit: [] },
|
|
2299
|
+
"background-color": { buttons: [], permit: [] }
|
|
2300
|
+
},
|
|
2301
|
+
html: {
|
|
2302
|
+
import: {
|
|
2303
|
+
mark: $markConversion
|
|
2304
|
+
}
|
|
2305
|
+
},
|
|
2306
|
+
register(editor, config) {
|
|
2307
|
+
// keep the ref to the canonicalizers for optimized css conversion
|
|
2308
|
+
const canonicalizers = buildCanonicalizers(config);
|
|
2287
2309
|
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
}
|
|
2293
|
-
}
|
|
2310
|
+
// Register the <pre> converter directly in the conversion cache so it
|
|
2311
|
+
// coexists with other extensions' "pre" converters (the extension-level
|
|
2312
|
+
// html.import uses Object.assign, which means only one "pre" per key).
|
|
2313
|
+
$registerPreConversion(editor);
|
|
2294
2314
|
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2315
|
+
return mergeRegister(
|
|
2316
|
+
editor.registerCommand(TOGGLE_HIGHLIGHT_COMMAND, (styles) => $toggleSelectionStyles(editor, styles), COMMAND_PRIORITY_NORMAL),
|
|
2317
|
+
editor.registerCommand(REMOVE_HIGHLIGHT_COMMAND, () => $toggleSelectionStyles(editor, BLANK_STYLES), COMMAND_PRIORITY_NORMAL),
|
|
2318
|
+
editor.registerNodeTransform(TextNode, $syncHighlightWithStyle),
|
|
2319
|
+
editor.registerNodeTransform(CodeHighlightNode, $syncHighlightWithCodeHighlightNode),
|
|
2320
|
+
editor.registerNodeTransform(TextNode, (textNode) => $canonicalizePastedStyles(textNode, canonicalizers)),
|
|
2321
|
+
editor.registerMutationListener(CodeNode, (mutations) => {
|
|
2322
|
+
$applyPendingCodeHighlights(editor, mutations);
|
|
2323
|
+
}, { skipInitialization: true })
|
|
2324
|
+
)
|
|
2325
|
+
}
|
|
2326
|
+
});
|
|
2298
2327
|
|
|
2299
|
-
|
|
2300
|
-
if (this.selection.hasSelectedWordsInSingleLine) {
|
|
2301
|
-
this.#toggleInlineCode();
|
|
2302
|
-
} else {
|
|
2303
|
-
this.contents.toggleCodeBlock();
|
|
2304
|
-
}
|
|
2328
|
+
return [ extension, this.editorConfig.get("highlight") ]
|
|
2305
2329
|
}
|
|
2330
|
+
}
|
|
2306
2331
|
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
const textNodes = selection.getNodes().filter($isTextNode);
|
|
2313
|
-
const applyingCode = !textNodes.every((node) => node.hasFormat("code"));
|
|
2332
|
+
function $applyHighlightStyle(textNode, element) {
|
|
2333
|
+
const elementStyles = {
|
|
2334
|
+
color: element.style?.color,
|
|
2335
|
+
"background-color": element.style?.backgroundColor
|
|
2336
|
+
};
|
|
2314
2337
|
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
}
|
|
2318
|
-
}
|
|
2338
|
+
if ($hasUpdateTag(PASTE_TAG)) { $setPastedStyles(textNode); }
|
|
2339
|
+
const highlightStyle = getCSSFromStyleObject(elementStyles);
|
|
2319
2340
|
|
|
2320
|
-
|
|
2341
|
+
if (highlightStyle.length) {
|
|
2342
|
+
return textNode.setStyle(textNode.getStyle() + highlightStyle)
|
|
2321
2343
|
}
|
|
2344
|
+
}
|
|
2322
2345
|
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
const endPoint = isBackward ? selection.anchor : selection.focus;
|
|
2330
|
-
|
|
2331
|
-
for (let i = 0; i < textNodes.length; i++) {
|
|
2332
|
-
const node = textNodes[i];
|
|
2333
|
-
if (node.getFormat() === 0) continue
|
|
2346
|
+
function $markConversion() {
|
|
2347
|
+
return {
|
|
2348
|
+
conversion: extendTextNodeConversion("mark", $applyHighlightStyle),
|
|
2349
|
+
priority: 1
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2334
2352
|
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2353
|
+
// Register a custom <pre> converter directly in the editor's HTML conversion
|
|
2354
|
+
// cache. We can't use the extension-level html.import because Object.assign
|
|
2355
|
+
// merges all extensions' converters by tag, and a later extension (e.g.
|
|
2356
|
+
// TrixContentExtension) would overwrite ours.
|
|
2357
|
+
function $registerPreConversion(editor) {
|
|
2358
|
+
if (!editor._htmlConversions) return
|
|
2339
2359
|
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2360
|
+
let preEntries = editor._htmlConversions.get("pre");
|
|
2361
|
+
if (!preEntries) {
|
|
2362
|
+
preEntries = [];
|
|
2363
|
+
editor._htmlConversions.set("pre", preEntries);
|
|
2364
|
+
}
|
|
2365
|
+
preEntries.push($preConversionWithHighlightsFactory(editor));
|
|
2366
|
+
}
|
|
2346
2367
|
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2368
|
+
// Returns a <pre> converter factory scoped to a specific editor instance.
|
|
2369
|
+
// The factory extracts highlight ranges from <mark> elements before the code
|
|
2370
|
+
// retokenizer can destroy them. The ranges are stored in pendingCodeHighlights
|
|
2371
|
+
// and applied after retokenization via a mutation listener.
|
|
2372
|
+
function $preConversionWithHighlightsFactory(editor) {
|
|
2373
|
+
return function $preConversionWithHighlights(domNode) {
|
|
2374
|
+
const highlights = extractHighlightRanges(domNode);
|
|
2375
|
+
if (highlights.length === 0) return null
|
|
2376
|
+
|
|
2377
|
+
return {
|
|
2378
|
+
conversion: (domNode) => {
|
|
2379
|
+
const language = domNode.getAttribute("data-language");
|
|
2380
|
+
const codeNode = $createCodeNode(language);
|
|
2381
|
+
$getPendingHighlights(editor).set(codeNode.getKey(), highlights);
|
|
2382
|
+
return { node: codeNode }
|
|
2383
|
+
},
|
|
2384
|
+
priority: 2
|
|
2354
2385
|
}
|
|
2355
2386
|
}
|
|
2387
|
+
}
|
|
2356
2388
|
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2389
|
+
// Walk the DOM tree inside a <pre> element and build a list of
|
|
2390
|
+
// { start, end, style } ranges for every <mark> element found.
|
|
2391
|
+
function extractHighlightRanges(preElement) {
|
|
2392
|
+
const ranges = [];
|
|
2393
|
+
const codeElement = preElement.querySelector("code") || preElement;
|
|
2360
2394
|
|
|
2361
|
-
|
|
2362
|
-
if (!codeNode) return
|
|
2395
|
+
let offset = 0;
|
|
2363
2396
|
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2397
|
+
function walk(node) {
|
|
2398
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
2399
|
+
offset += node.textContent.length;
|
|
2400
|
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
2401
|
+
// <br> maps to a LineBreakNode (1 character) in Lexical
|
|
2402
|
+
if (node.tagName === "BR") {
|
|
2403
|
+
offset += 1;
|
|
2404
|
+
return
|
|
2405
|
+
}
|
|
2367
2406
|
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
this.editor.focus();
|
|
2371
|
-
}
|
|
2407
|
+
const isMark = node.tagName === "MARK";
|
|
2408
|
+
const start = offset;
|
|
2372
2409
|
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2410
|
+
for (const child of node.childNodes) {
|
|
2411
|
+
walk(child);
|
|
2412
|
+
}
|
|
2376
2413
|
|
|
2377
|
-
|
|
2378
|
-
|
|
2414
|
+
if (isMark) {
|
|
2415
|
+
const style = extractHighlightStyleFromElement(node);
|
|
2416
|
+
if (style) {
|
|
2417
|
+
ranges.push({ start, end: offset, style });
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2379
2421
|
}
|
|
2380
2422
|
|
|
2381
|
-
|
|
2382
|
-
|
|
2423
|
+
for (const child of codeElement.childNodes) {
|
|
2424
|
+
walk(child);
|
|
2383
2425
|
}
|
|
2384
2426
|
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
}
|
|
2427
|
+
return ranges
|
|
2428
|
+
}
|
|
2388
2429
|
|
|
2389
|
-
|
|
2390
|
-
|
|
2430
|
+
function $getPendingHighlights(editor) {
|
|
2431
|
+
let map = pendingCodeHighlights.get(editor);
|
|
2432
|
+
if (!map) {
|
|
2433
|
+
map = new Map();
|
|
2434
|
+
pendingCodeHighlights.set(editor, map);
|
|
2391
2435
|
}
|
|
2436
|
+
return map
|
|
2437
|
+
}
|
|
2392
2438
|
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2439
|
+
function extractHighlightStyleFromElement(element) {
|
|
2440
|
+
const styles = {};
|
|
2441
|
+
if (element.style?.color) styles.color = element.style.color;
|
|
2442
|
+
if (element.style?.backgroundColor) styles["background-color"] = element.style.backgroundColor;
|
|
2443
|
+
const css = getCSSFromStyleObject(styles);
|
|
2444
|
+
return css.length > 0 ? css : null
|
|
2445
|
+
}
|
|
2396
2446
|
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2447
|
+
// Called from the CodeNode mutation listener after the retokenizer has
|
|
2448
|
+
// replaced TextNodes with fresh CodeHighlightNodes.
|
|
2449
|
+
function $applyPendingCodeHighlights(editor, mutations) {
|
|
2450
|
+
const pending = $getPendingHighlights(editor);
|
|
2451
|
+
const keysToProcess = [];
|
|
2400
2452
|
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
onchange: ({ target: { files } }) => {
|
|
2407
|
-
this.contents.uploadFiles(files, { selectLast: true });
|
|
2408
|
-
}
|
|
2409
|
-
};
|
|
2453
|
+
for (const [ key, type ] of mutations) {
|
|
2454
|
+
if (type !== "destroyed" && pending.has(key)) {
|
|
2455
|
+
keysToProcess.push(key);
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2410
2458
|
|
|
2411
|
-
|
|
2459
|
+
if (keysToProcess.length === 0) return
|
|
2412
2460
|
|
|
2413
|
-
|
|
2461
|
+
// Use a deferred update so the retokenizer has finished its
|
|
2462
|
+
// skipTransforms update before we touch the nodes.
|
|
2463
|
+
editor.update(() => {
|
|
2464
|
+
for (const key of keysToProcess) {
|
|
2465
|
+
const highlights = pending.get(key);
|
|
2466
|
+
pending.delete(key);
|
|
2467
|
+
if (!highlights) continue
|
|
2414
2468
|
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
input.click();
|
|
2418
|
-
setTimeout(() => input.remove(), 1000);
|
|
2419
|
-
}
|
|
2469
|
+
const codeNode = $getNodeByKey(key);
|
|
2470
|
+
if (!codeNode || !$isCodeNode(codeNode)) continue
|
|
2420
2471
|
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
}
|
|
2472
|
+
$applyHighlightRangesToCodeNode(codeNode, highlights);
|
|
2473
|
+
}
|
|
2474
|
+
}, { skipTransforms: true, discrete: true });
|
|
2475
|
+
}
|
|
2424
2476
|
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2477
|
+
// Apply saved highlight ranges to the CodeHighlightNode children
|
|
2478
|
+
// of a CodeNode, splitting nodes at range boundaries as needed.
|
|
2479
|
+
// We can't use TextNode.splitText() because it creates TextNode
|
|
2480
|
+
// instances (not CodeHighlightNodes) for the split parts. Instead,
|
|
2481
|
+
// we manually create CodeHighlightNode replacements.
|
|
2482
|
+
function $applyHighlightRangesToCodeNode(codeNode, highlights) {
|
|
2483
|
+
if (highlights.length === 0) return
|
|
2428
2484
|
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2485
|
+
for (const { start: hlStart, end: hlEnd, style } of highlights) {
|
|
2486
|
+
// Rebuild the child-to-offset mapping for each highlight range because
|
|
2487
|
+
// earlier ranges may have split nodes, invalidating previous mappings.
|
|
2488
|
+
const childRanges = $buildChildRanges(codeNode);
|
|
2432
2489
|
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2490
|
+
for (const { node, start: nodeStart, end: nodeEnd } of childRanges) {
|
|
2491
|
+
// Skip plain TextNodes: only CodeHighlightNodes can be split into
|
|
2492
|
+
// styled replacements here. The retokenizer normally converts any
|
|
2493
|
+
// TextNode children back to CodeHighlightNodes before this runs,
|
|
2494
|
+
// but the iteration over $buildChildRanges has to keep counting
|
|
2495
|
+
// them so character offsets stay aligned with the saved ranges.
|
|
2496
|
+
if (!$isCodeHighlightNode(node)) continue
|
|
2436
2497
|
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
const
|
|
2440
|
-
this.#registerCommandHandler(command, 0, this[methodName].bind(this));
|
|
2441
|
-
}
|
|
2498
|
+
// Check if this child overlaps with the highlight range
|
|
2499
|
+
const overlapStart = Math.max(hlStart, nodeStart);
|
|
2500
|
+
const overlapEnd = Math.min(hlEnd, nodeEnd);
|
|
2442
2501
|
|
|
2443
|
-
|
|
2444
|
-
}
|
|
2502
|
+
if (overlapStart >= overlapEnd) continue
|
|
2445
2503
|
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2504
|
+
// Calculate offsets relative to this node
|
|
2505
|
+
const relStart = overlapStart - nodeStart;
|
|
2506
|
+
const relEnd = overlapEnd - nodeStart;
|
|
2507
|
+
const nodeLength = nodeEnd - nodeStart;
|
|
2449
2508
|
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2509
|
+
if (relStart === 0 && relEnd === nodeLength) {
|
|
2510
|
+
// Entire node is highlighted - apply style directly
|
|
2511
|
+
node.setStyle(style);
|
|
2512
|
+
$setCodeHighlightFormat(node, true);
|
|
2513
|
+
} else {
|
|
2514
|
+
// Need to split: replace the node with 2 or 3 CodeHighlightNodes
|
|
2515
|
+
const text = node.getTextContent();
|
|
2516
|
+
const highlightType = node.getHighlightType();
|
|
2517
|
+
const replacements = [];
|
|
2454
2518
|
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
if (this.selection.isInsideCodeBlock || !selection.hasFormat("code")) return false
|
|
2519
|
+
if (relStart > 0) {
|
|
2520
|
+
replacements.push($createCodeHighlightNode(text.slice(0, relStart), highlightType));
|
|
2521
|
+
}
|
|
2459
2522
|
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2523
|
+
const styledNode = $createCodeHighlightNode(text.slice(relStart, relEnd), highlightType);
|
|
2524
|
+
styledNode.setStyle(style);
|
|
2525
|
+
$setCodeHighlightFormat(styledNode, true);
|
|
2526
|
+
replacements.push(styledNode);
|
|
2463
2527
|
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
}
|
|
2528
|
+
if (relEnd < nodeLength) {
|
|
2529
|
+
replacements.push($createCodeHighlightNode(text.slice(relEnd), highlightType));
|
|
2530
|
+
}
|
|
2468
2531
|
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
registerEventListener(root, "dragover", this.#handleDragOver.bind(this)),
|
|
2475
|
-
registerEventListener(root, "drop", this.#handleDrop.bind(this)),
|
|
2476
|
-
registerEventListener(root, "dragenter", this.#handleDragEnter.bind(this)),
|
|
2477
|
-
registerEventListener(root, "dragleave", this.#handleDragLeave.bind(this))
|
|
2478
|
-
);
|
|
2532
|
+
for (const replacement of replacements) {
|
|
2533
|
+
node.insertBefore(replacement);
|
|
2534
|
+
}
|
|
2535
|
+
node.remove();
|
|
2536
|
+
}
|
|
2479
2537
|
}
|
|
2480
2538
|
}
|
|
2539
|
+
}
|
|
2481
2540
|
|
|
2482
|
-
|
|
2483
|
-
|
|
2541
|
+
function $buildChildRanges(codeNode) {
|
|
2542
|
+
const childRanges = [];
|
|
2543
|
+
let charOffset = 0;
|
|
2484
2544
|
|
|
2485
|
-
|
|
2486
|
-
if (
|
|
2487
|
-
|
|
2488
|
-
|
|
2545
|
+
for (const child of codeNode.getChildren()) {
|
|
2546
|
+
if ($isCodeHighlightNode(child) || $isTextNode(child)) {
|
|
2547
|
+
const text = child.getTextContent();
|
|
2548
|
+
childRanges.push({ node: child, start: charOffset, end: charOffset + text.length });
|
|
2549
|
+
charOffset += text.length;
|
|
2550
|
+
} else {
|
|
2551
|
+
// LineBreakNode, TabNode - count as 1 character each (\n, \t)
|
|
2552
|
+
charOffset += 1;
|
|
2489
2553
|
}
|
|
2490
2554
|
}
|
|
2491
2555
|
|
|
2492
|
-
|
|
2493
|
-
|
|
2556
|
+
return childRanges
|
|
2557
|
+
}
|
|
2494
2558
|
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2559
|
+
// Extract highlight ranges from the Lexical node tree of a CodeNode.
|
|
2560
|
+
// This mirrors extractHighlightRanges (which works on DOM elements during
|
|
2561
|
+
// HTML import) but reads from live CodeHighlightNode children instead.
|
|
2562
|
+
function $extractHighlightRangesFromCodeNode(codeNode) {
|
|
2563
|
+
const ranges = [];
|
|
2564
|
+
const childRanges = $buildChildRanges(codeNode);
|
|
2565
|
+
|
|
2566
|
+
for (const { node, start, end } of childRanges) {
|
|
2567
|
+
const style = node.getStyle();
|
|
2568
|
+
if (style && hasHighlightStyles(style)) {
|
|
2569
|
+
ranges.push({ start, end, style });
|
|
2499
2570
|
}
|
|
2500
2571
|
}
|
|
2501
2572
|
|
|
2502
|
-
|
|
2503
|
-
|
|
2573
|
+
return ranges
|
|
2574
|
+
}
|
|
2504
2575
|
|
|
2505
|
-
|
|
2506
|
-
|
|
2576
|
+
function buildCanonicalizers(config) {
|
|
2577
|
+
return [
|
|
2578
|
+
new StyleCanonicalizer("color", [ ...config.buttons.color, ...config.permit.color ]),
|
|
2579
|
+
new StyleCanonicalizer("background-color", [ ...config.buttons["background-color"], ...config.permit["background-color"] ])
|
|
2580
|
+
]
|
|
2581
|
+
}
|
|
2507
2582
|
|
|
2508
|
-
|
|
2509
|
-
|
|
2583
|
+
function $toggleSelectionStyles(editor, styles) {
|
|
2584
|
+
const selection = $getSelection();
|
|
2585
|
+
if (!$isRangeSelection(selection)) return
|
|
2510
2586
|
|
|
2511
|
-
|
|
2587
|
+
const patch = {};
|
|
2588
|
+
for (const property in styles) {
|
|
2589
|
+
const oldValue = $getSelectionStyleValueForProperty(selection, property);
|
|
2590
|
+
patch[property] = toggleOrReplace(oldValue, styles[property]);
|
|
2591
|
+
}
|
|
2512
2592
|
|
|
2513
|
-
|
|
2514
|
-
|
|
2593
|
+
if ($selectionIsInCodeBlock(selection)) {
|
|
2594
|
+
$patchCodeHighlightStyles(editor, selection, patch);
|
|
2595
|
+
} else {
|
|
2596
|
+
$patchStyleText(selection, patch);
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2515
2599
|
|
|
2516
|
-
|
|
2517
|
-
|
|
2600
|
+
function $selectionIsInCodeBlock(selection) {
|
|
2601
|
+
const nodes = selection.getNodes();
|
|
2602
|
+
return nodes.some((node) => {
|
|
2603
|
+
// A text node inside a code block may be either a CodeHighlightNode
|
|
2604
|
+
// (after retokenization) or a plain TextNode (after splitText or before
|
|
2605
|
+
// the retokenizer has run). Check the parent in both cases.
|
|
2606
|
+
if ($isCodeHighlightNode(node) || $isTextNode(node)) {
|
|
2607
|
+
return $isCodeNode(node.getParent())
|
|
2608
|
+
}
|
|
2609
|
+
return $isCodeNode(node)
|
|
2610
|
+
})
|
|
2611
|
+
}
|
|
2518
2612
|
|
|
2519
|
-
|
|
2520
|
-
|
|
2613
|
+
function $patchCodeHighlightStyles(editor, selection, patch) {
|
|
2614
|
+
// Capture selection state and node keys before the nested update.
|
|
2615
|
+
// Accept both CodeHighlightNode and TextNode children of a CodeNode
|
|
2616
|
+
// because splitText creates TextNode instances and the retokenizer
|
|
2617
|
+
// may not have converted them back to CodeHighlightNodes yet.
|
|
2618
|
+
const nodeKeys = selection.getNodes()
|
|
2619
|
+
.filter((node) => ($isCodeHighlightNode(node) || $isTextNode(node)) && $isCodeNode(node.getParent()))
|
|
2620
|
+
.map((node) => ({
|
|
2621
|
+
key: node.getKey(),
|
|
2622
|
+
startOffset: $getNodeSelectionOffsets(node, selection)[0],
|
|
2623
|
+
endOffset: $getNodeSelectionOffsets(node, selection)[1],
|
|
2624
|
+
textSize: node.getTextContentSize()
|
|
2625
|
+
}));
|
|
2521
2626
|
|
|
2522
|
-
|
|
2523
|
-
|
|
2627
|
+
// Use skipTransforms to prevent the code highlighting system from
|
|
2628
|
+
// re-tokenizing and wiping out the style changes we apply.
|
|
2629
|
+
// Use discrete to force a synchronous commit, ensuring the changes
|
|
2630
|
+
// are committed before editor.focus() triggers a second update cycle
|
|
2631
|
+
// that would re-run transforms and wipe out the styles.
|
|
2632
|
+
editor.update(() => {
|
|
2633
|
+
const affectedCodeNodes = new Set();
|
|
2524
2634
|
|
|
2525
|
-
|
|
2526
|
-
|
|
2635
|
+
for (const { key, startOffset, endOffset, textSize } of nodeKeys) {
|
|
2636
|
+
const node = $getNodeByKey(key);
|
|
2637
|
+
if (!node) continue
|
|
2527
2638
|
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
});
|
|
2532
|
-
}
|
|
2639
|
+
const parent = node.getParent();
|
|
2640
|
+
if (!$isCodeNode(parent)) continue
|
|
2641
|
+
if (startOffset === endOffset) continue
|
|
2533
2642
|
|
|
2534
|
-
|
|
2535
|
-
if (!this.#selectionBeforeDrag) return
|
|
2643
|
+
affectedCodeNodes.add(parent);
|
|
2536
2644
|
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2645
|
+
if (startOffset === 0 && endOffset === textSize) {
|
|
2646
|
+
$applyStylePatchToNode(node, patch);
|
|
2647
|
+
} else {
|
|
2648
|
+
const splitNodes = node.splitText(startOffset, endOffset);
|
|
2649
|
+
const targetNode = splitNodes[startOffset === 0 ? 0 : 1];
|
|
2650
|
+
$applyStylePatchToNode(targetNode, patch);
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2540
2653
|
|
|
2541
|
-
|
|
2542
|
-
|
|
2654
|
+
// After applying styles, save highlight ranges for each affected CodeNode.
|
|
2655
|
+
// The code retokenizer will replace the styled nodes with fresh unstyled
|
|
2656
|
+
// tokens when transforms run. The pending highlights are picked up by the
|
|
2657
|
+
// CodeNode mutation listener and reapplied after retokenization.
|
|
2658
|
+
for (const codeNode of affectedCodeNodes) {
|
|
2659
|
+
const ranges = $extractHighlightRangesFromCodeNode(codeNode);
|
|
2660
|
+
if (ranges.length > 0) {
|
|
2661
|
+
$getPendingHighlights(editor).set(codeNode.getKey(), ranges);
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
}, { skipTransforms: true, discrete: true });
|
|
2665
|
+
}
|
|
2543
2666
|
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2667
|
+
function $getNodeSelectionOffsets(node, selection) {
|
|
2668
|
+
const nodeKey = node.getKey();
|
|
2669
|
+
const anchorKey = selection.anchor.key;
|
|
2670
|
+
const focusKey = selection.focus.key;
|
|
2671
|
+
const textSize = node.getTextContentSize();
|
|
2547
2672
|
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
return this.#handleTabForList(event)
|
|
2551
|
-
} else if (this.selection.isInsideCodeBlock) {
|
|
2552
|
-
return this.#handleTabForCode()
|
|
2553
|
-
}
|
|
2554
|
-
return false
|
|
2555
|
-
}
|
|
2673
|
+
const isAnchor = nodeKey === anchorKey;
|
|
2674
|
+
const isFocus = nodeKey === focusKey;
|
|
2556
2675
|
|
|
2557
|
-
|
|
2558
|
-
|
|
2676
|
+
// Determine if selection is forward or backward
|
|
2677
|
+
const isForward = selection.isBackward() === false;
|
|
2559
2678
|
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
return this.editor.dispatchCommand(command)
|
|
2563
|
-
}
|
|
2679
|
+
let start = 0;
|
|
2680
|
+
let end = textSize;
|
|
2564
2681
|
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2682
|
+
if (isForward) {
|
|
2683
|
+
if (isAnchor) start = selection.anchor.offset;
|
|
2684
|
+
if (isFocus) end = selection.focus.offset;
|
|
2685
|
+
} else {
|
|
2686
|
+
if (isFocus) start = selection.focus.offset;
|
|
2687
|
+
if (isAnchor) end = selection.anchor.offset;
|
|
2568
2688
|
}
|
|
2569
2689
|
|
|
2690
|
+
return [ start, end ]
|
|
2570
2691
|
}
|
|
2571
2692
|
|
|
2572
|
-
function
|
|
2573
|
-
|
|
2574
|
-
}
|
|
2575
|
-
|
|
2576
|
-
function debounce(fn, wait) {
|
|
2577
|
-
let timeout;
|
|
2693
|
+
function $applyStylePatchToNode(node, patch) {
|
|
2694
|
+
const prevStyles = getStyleObjectFromCSS(node.getStyle());
|
|
2695
|
+
const newStyles = { ...prevStyles };
|
|
2578
2696
|
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2697
|
+
for (const [ key, value ] of Object.entries(patch)) {
|
|
2698
|
+
if (value === null) {
|
|
2699
|
+
delete newStyles[key];
|
|
2700
|
+
} else {
|
|
2701
|
+
newStyles[key] = value;
|
|
2702
|
+
}
|
|
2582
2703
|
}
|
|
2583
|
-
}
|
|
2584
2704
|
|
|
2585
|
-
|
|
2586
|
-
|
|
2705
|
+
const newCSSText = getCSSFromStyleObject(newStyles);
|
|
2706
|
+
node.setStyle(newCSSText);
|
|
2587
2707
|
|
|
2588
|
-
|
|
2589
|
-
|
|
2708
|
+
// Sync the highlight format using TextNode's setFormat to bypass
|
|
2709
|
+
// CodeHighlightNode's no-op override
|
|
2710
|
+
const shouldHaveHighlight = hasHighlightStyles(newCSSText);
|
|
2711
|
+
const hasHighlight = node.hasFormat("highlight");
|
|
2590
2712
|
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
try {
|
|
2594
|
-
const result = await fn(...args);
|
|
2595
|
-
resolve(result);
|
|
2596
|
-
} catch (err) {
|
|
2597
|
-
reject(err);
|
|
2598
|
-
}
|
|
2599
|
-
}, wait);
|
|
2600
|
-
})
|
|
2713
|
+
if (shouldHaveHighlight !== hasHighlight) {
|
|
2714
|
+
$setCodeHighlightFormat(node, shouldHaveHighlight);
|
|
2601
2715
|
}
|
|
2602
2716
|
}
|
|
2603
2717
|
|
|
2604
|
-
function
|
|
2605
|
-
|
|
2606
|
-
|
|
2718
|
+
function $setCodeHighlightFormat(node, shouldHaveHighlight) {
|
|
2719
|
+
const writable = node.getWritable();
|
|
2720
|
+
const IS_HIGHLIGHT = 1 << 7;
|
|
2607
2721
|
|
|
2608
|
-
|
|
2609
|
-
|
|
2722
|
+
if (shouldHaveHighlight) {
|
|
2723
|
+
writable.__format |= IS_HIGHLIGHT;
|
|
2724
|
+
} else {
|
|
2725
|
+
writable.__format &= ~IS_HIGHLIGHT;
|
|
2726
|
+
}
|
|
2610
2727
|
}
|
|
2611
2728
|
|
|
2612
|
-
function
|
|
2613
|
-
return
|
|
2729
|
+
function toggleOrReplace(oldValue, newValue) {
|
|
2730
|
+
return oldValue === newValue ? null : newValue
|
|
2614
2731
|
}
|
|
2615
2732
|
|
|
2616
|
-
function
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
return true
|
|
2620
|
-
} catch {
|
|
2621
|
-
return false
|
|
2733
|
+
function $syncHighlightWithStyle(textNode) {
|
|
2734
|
+
if (hasHighlightStyles(textNode.getStyle()) !== textNode.hasFormat("highlight")) {
|
|
2735
|
+
textNode.toggleFormat("highlight");
|
|
2622
2736
|
}
|
|
2623
2737
|
}
|
|
2624
2738
|
|
|
2625
|
-
function
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
.normalize("NFD").replace(/[\u0300-\u036f]/g, "") // Remove diacritics
|
|
2629
|
-
}
|
|
2630
|
-
|
|
2631
|
-
function filterMatchPosition(text, potentialMatch) {
|
|
2632
|
-
const normalizedText = normalizeFilteredText(text);
|
|
2633
|
-
const normalizedMatch = normalizeFilteredText(potentialMatch);
|
|
2739
|
+
function $syncHighlightWithCodeHighlightNode(node) {
|
|
2740
|
+
const parent = node.getParent();
|
|
2741
|
+
if (!$isCodeNode(parent)) return
|
|
2634
2742
|
|
|
2635
|
-
|
|
2743
|
+
const shouldHaveHighlight = hasHighlightStyles(node.getStyle());
|
|
2744
|
+
const hasHighlight = node.hasFormat("highlight");
|
|
2636
2745
|
|
|
2637
|
-
|
|
2638
|
-
|
|
2746
|
+
if (shouldHaveHighlight !== hasHighlight) {
|
|
2747
|
+
$setCodeHighlightFormat(node, shouldHaveHighlight);
|
|
2748
|
+
}
|
|
2639
2749
|
}
|
|
2640
2750
|
|
|
2641
|
-
function
|
|
2642
|
-
|
|
2643
|
-
|
|
2751
|
+
function $canonicalizePastedStyles(textNode, canonicalizers = []) {
|
|
2752
|
+
if ($hasPastedStyles(textNode)) {
|
|
2753
|
+
$setPastedStyles(textNode, false);
|
|
2644
2754
|
|
|
2645
|
-
|
|
2646
|
-
|
|
2755
|
+
const canonicalizedCSS = applyCanonicalizers(textNode.getStyle(), canonicalizers);
|
|
2756
|
+
textNode.setStyle(canonicalizedCSS);
|
|
2757
|
+
|
|
2758
|
+
const selection = $getSelection();
|
|
2759
|
+
if (textNode.isSelected(selection)) {
|
|
2760
|
+
selection.setStyle(textNode.getStyle());
|
|
2761
|
+
selection.setFormat(textNode.getFormat());
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2647
2764
|
}
|
|
2648
2765
|
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
function parseBoolean(value) {
|
|
2652
|
-
if (typeof value === "string") return value === "true"
|
|
2653
|
-
return Boolean(value)
|
|
2766
|
+
function $setPastedStyles(textNode, value = true) {
|
|
2767
|
+
$setState(textNode, hasPastedStylesState, value);
|
|
2654
2768
|
}
|
|
2655
2769
|
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
const REWRITE_HISTORY_COMMAND = createCommand("REWRITE_HISTORY_COMMAND");
|
|
2770
|
+
function $hasPastedStyles(textNode) {
|
|
2771
|
+
return $getState(textNode, hasPastedStylesState)
|
|
2772
|
+
}
|
|
2660
2773
|
|
|
2661
|
-
|
|
2662
|
-
|
|
2774
|
+
const COMMANDS = [
|
|
2775
|
+
"bold",
|
|
2776
|
+
"italic",
|
|
2777
|
+
"strikethrough",
|
|
2778
|
+
"underline",
|
|
2779
|
+
"link",
|
|
2780
|
+
"unlink",
|
|
2781
|
+
"toggleHighlight",
|
|
2782
|
+
"removeHighlight",
|
|
2783
|
+
"setFormatHeadingLarge",
|
|
2784
|
+
"setFormatHeadingMedium",
|
|
2785
|
+
"setFormatHeadingSmall",
|
|
2786
|
+
"setFormatParagraph",
|
|
2787
|
+
"clearFormatting",
|
|
2788
|
+
"insertUnorderedList",
|
|
2789
|
+
"insertOrderedList",
|
|
2790
|
+
"insertQuoteBlock",
|
|
2791
|
+
"insertCodeBlock",
|
|
2792
|
+
"setCodeLanguage",
|
|
2793
|
+
"insertHorizontalDivider",
|
|
2794
|
+
"uploadImage",
|
|
2795
|
+
"uploadFile",
|
|
2663
2796
|
|
|
2664
|
-
|
|
2665
|
-
return defineExtension({
|
|
2666
|
-
name: "lexxy/rewritable-history",
|
|
2667
|
-
dependencies: [ HistoryExtension ],
|
|
2668
|
-
register: (editor, _config, state) => {
|
|
2669
|
-
const historyOutput = state.getDependency(HistoryExtension).output;
|
|
2670
|
-
this.#historyState = historyOutput.historyState.value;
|
|
2797
|
+
"insertTable",
|
|
2671
2798
|
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
COMMAND_PRIORITY_EDITOR
|
|
2676
|
-
)
|
|
2677
|
-
}
|
|
2678
|
-
})
|
|
2679
|
-
}
|
|
2799
|
+
"undo",
|
|
2800
|
+
"redo"
|
|
2801
|
+
];
|
|
2680
2802
|
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2803
|
+
class CommandDispatcher {
|
|
2804
|
+
#selectionBeforeDrag = null
|
|
2805
|
+
#listeners = new ListenerBin()
|
|
2684
2806
|
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
if (this.#historyState.current) entries.push(this.#historyState.current);
|
|
2688
|
-
return entries.concat(this.#historyState.redoStack)
|
|
2807
|
+
static configureFor(editorElement) {
|
|
2808
|
+
return new CommandDispatcher(editorElement)
|
|
2689
2809
|
}
|
|
2690
2810
|
|
|
2691
|
-
|
|
2692
|
-
this
|
|
2693
|
-
this
|
|
2811
|
+
constructor(editorElement) {
|
|
2812
|
+
this.editorElement = editorElement;
|
|
2813
|
+
this.editor = editorElement.editor;
|
|
2814
|
+
this.selection = editorElement.selection;
|
|
2815
|
+
this.contents = editorElement.contents;
|
|
2816
|
+
this.clipboard = editorElement.clipboard;
|
|
2694
2817
|
|
|
2695
|
-
|
|
2818
|
+
this.#registerCommands();
|
|
2819
|
+
this.#registerKeyboardCommands();
|
|
2820
|
+
this.#registerDragAndDropHandlers();
|
|
2696
2821
|
}
|
|
2697
2822
|
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
for (const [ nodeKey, { patch, replace } ] of Object.entries(rewrites)) {
|
|
2701
|
-
const node = $getNodeByKey(nodeKey);
|
|
2702
|
-
if (!node) continue
|
|
2703
|
-
|
|
2704
|
-
if (patch) Object.assign(node.getWritable(), patch);
|
|
2705
|
-
if (replace) node.replace(replace);
|
|
2706
|
-
}
|
|
2707
|
-
}, { discrete: true, tag: this.#getBackgroundUpdateTags() });
|
|
2823
|
+
dispatchPaste(event) {
|
|
2824
|
+
return this.clipboard.paste(event)
|
|
2708
2825
|
}
|
|
2709
2826
|
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
for (const entry of this.#allHistoryEntries) {
|
|
2714
|
-
if (!this.#entryHasSomeKeys(entry, nodeKeys)) continue
|
|
2715
|
-
|
|
2716
|
-
const editorState = entry.editorState = safeCloneEditorState(entry.editorState);
|
|
2717
|
-
|
|
2718
|
-
for (const [ nodeKey, { patch, replace } ] of Object.entries(rewrites)) {
|
|
2719
|
-
const node = editorState._nodeMap.get(nodeKey);
|
|
2720
|
-
if (!node) continue
|
|
2721
|
-
|
|
2722
|
-
if (patch) {
|
|
2723
|
-
this.#patchNodeInEditorState(editorState, node, patch);
|
|
2724
|
-
} else if (replace) {
|
|
2725
|
-
this.#replaceNodeInEditorState(editorState, node, replace);
|
|
2726
|
-
}
|
|
2727
|
-
}
|
|
2728
|
-
}
|
|
2827
|
+
dispatchBold() {
|
|
2828
|
+
this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold");
|
|
2729
2829
|
}
|
|
2730
2830
|
|
|
2731
|
-
|
|
2732
|
-
|
|
2831
|
+
dispatchItalic() {
|
|
2832
|
+
this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic");
|
|
2733
2833
|
}
|
|
2734
2834
|
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
if (!isEditorFocused(this.editorElement.editor)) { tags.push(SKIP_DOM_SELECTION_TAG); }
|
|
2738
|
-
return tags
|
|
2835
|
+
dispatchStrikethrough() {
|
|
2836
|
+
this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough");
|
|
2739
2837
|
}
|
|
2740
2838
|
|
|
2741
|
-
|
|
2742
|
-
|
|
2839
|
+
dispatchUnderline() {
|
|
2840
|
+
this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline");
|
|
2743
2841
|
}
|
|
2744
2842
|
|
|
2745
|
-
|
|
2746
|
-
|
|
2843
|
+
dispatchToggleHighlight(styles) {
|
|
2844
|
+
this.editor.dispatchCommand(TOGGLE_HIGHLIGHT_COMMAND, styles);
|
|
2747
2845
|
}
|
|
2748
|
-
}
|
|
2749
2846
|
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
return clone
|
|
2754
|
-
}
|
|
2847
|
+
dispatchRemoveHighlight() {
|
|
2848
|
+
this.editor.dispatchCommand(REMOVE_HIGHLIGHT_COMMAND);
|
|
2849
|
+
}
|
|
2755
2850
|
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
clone.__prev = previousNode.__prev;
|
|
2761
|
-
clone.__next = previousNode.__next;
|
|
2762
|
-
return clone
|
|
2763
|
-
}
|
|
2851
|
+
dispatchLink(url) {
|
|
2852
|
+
this.editor.update(() => {
|
|
2853
|
+
const selection = $getSelection();
|
|
2854
|
+
if (!$isRangeSelection(selection)) return
|
|
2764
2855
|
|
|
2765
|
-
|
|
2766
|
-
// A new Map is needed to prevent editing Lexical's internal map
|
|
2767
|
-
// Warning: this bypasses DEV's safety map freezing
|
|
2768
|
-
function safeCloneEditorState(editorState) {
|
|
2769
|
-
const clone = editorState.clone();
|
|
2770
|
-
clone._nodeMap = new Map(editorState._nodeMap);
|
|
2771
|
-
return clone
|
|
2772
|
-
}
|
|
2856
|
+
const anchorNode = selection.anchor.getNode();
|
|
2773
2857
|
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2858
|
+
if (selection.isCollapsed() && !$getNearestNodeOfType(anchorNode, LinkNode)) {
|
|
2859
|
+
const autoLinkNode = $createAutoLinkNode(url);
|
|
2860
|
+
const textNode = $createTextNode(url);
|
|
2861
|
+
autoLinkNode.append(textNode);
|
|
2862
|
+
selection.insertNodes([ autoLinkNode ]);
|
|
2863
|
+
} else {
|
|
2864
|
+
$toggleLink(url);
|
|
2865
|
+
}
|
|
2866
|
+
});
|
|
2777
2867
|
}
|
|
2778
2868
|
|
|
2779
|
-
|
|
2780
|
-
|
|
2869
|
+
dispatchUnlink() {
|
|
2870
|
+
this.editor.update(() => {
|
|
2871
|
+
// Let adapters signal whether unlink should target a frozen link key.
|
|
2872
|
+
if (this.editorElement.adapter.unlinkFrozenNode?.()) {
|
|
2873
|
+
return
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
$toggleLink(null);
|
|
2877
|
+
});
|
|
2781
2878
|
}
|
|
2782
2879
|
|
|
2783
|
-
|
|
2784
|
-
|
|
2880
|
+
dispatchInsertUnorderedList() {
|
|
2881
|
+
const selection = $getSelection();
|
|
2882
|
+
if (!$isRangeSelection(selection)) return
|
|
2883
|
+
|
|
2884
|
+
const anchorNode = selection.anchor.getNode();
|
|
2885
|
+
|
|
2886
|
+
if (this.selection.isInsideList && anchorNode && getListType(anchorNode) === "bullet") {
|
|
2887
|
+
this.contents.applyParagraphFormat();
|
|
2888
|
+
} else {
|
|
2889
|
+
this.contents.applyUnorderedListFormat();
|
|
2890
|
+
}
|
|
2785
2891
|
}
|
|
2786
2892
|
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
return {
|
|
2791
|
-
conversion: (attachment) => ({
|
|
2792
|
-
node: new ActionTextAttachmentNode({
|
|
2793
|
-
sgid: attachment.getAttribute("sgid"),
|
|
2794
|
-
src: attachment.getAttribute("url"),
|
|
2795
|
-
previewable: attachment.getAttribute("previewable"),
|
|
2796
|
-
altText: attachment.getAttribute("alt"),
|
|
2797
|
-
caption: attachment.getAttribute("caption"),
|
|
2798
|
-
contentType: attachment.getAttribute("content-type"),
|
|
2799
|
-
fileName: attachment.getAttribute("filename"),
|
|
2800
|
-
fileSize: attachment.getAttribute("filesize"),
|
|
2801
|
-
width: attachment.getAttribute("width"),
|
|
2802
|
-
height: attachment.getAttribute("height")
|
|
2803
|
-
})
|
|
2804
|
-
}), priority: 1
|
|
2805
|
-
}
|
|
2806
|
-
},
|
|
2807
|
-
"img": () => {
|
|
2808
|
-
return {
|
|
2809
|
-
conversion: (img) => {
|
|
2810
|
-
const fileName = extractFileName(img.getAttribute("src") ?? "");
|
|
2811
|
-
return {
|
|
2812
|
-
node: new ActionTextAttachmentNode({
|
|
2813
|
-
src: img.getAttribute("src"),
|
|
2814
|
-
fileName: fileName,
|
|
2815
|
-
caption: img.getAttribute("alt") || "",
|
|
2816
|
-
contentType: "image/*",
|
|
2817
|
-
width: img.getAttribute("width"),
|
|
2818
|
-
height: img.getAttribute("height")
|
|
2819
|
-
})
|
|
2820
|
-
}
|
|
2821
|
-
}, priority: 1
|
|
2822
|
-
}
|
|
2823
|
-
},
|
|
2824
|
-
"video": () => {
|
|
2825
|
-
return {
|
|
2826
|
-
conversion: (video) => {
|
|
2827
|
-
const videoSource = video.getAttribute("src") || video.querySelector("source")?.src;
|
|
2828
|
-
const fileName = videoSource?.split("/")?.pop();
|
|
2829
|
-
const contentType = video.querySelector("source")?.getAttribute("content-type") || "video/*";
|
|
2893
|
+
dispatchInsertOrderedList() {
|
|
2894
|
+
const selection = $getSelection();
|
|
2895
|
+
if (!$isRangeSelection(selection)) return
|
|
2830
2896
|
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
}
|
|
2838
|
-
}, priority: 1
|
|
2839
|
-
}
|
|
2840
|
-
}
|
|
2897
|
+
const anchorNode = selection.anchor.getNode();
|
|
2898
|
+
|
|
2899
|
+
if (this.selection.isInsideList && anchorNode && getListType(anchorNode) === "number") {
|
|
2900
|
+
this.contents.applyParagraphFormat();
|
|
2901
|
+
} else {
|
|
2902
|
+
this.contents.applyOrderedListFormat();
|
|
2841
2903
|
}
|
|
2842
2904
|
}
|
|
2843
2905
|
|
|
2844
|
-
|
|
2845
|
-
|
|
2906
|
+
dispatchInsertQuoteBlock() {
|
|
2907
|
+
this.contents.toggleBlockquote();
|
|
2846
2908
|
}
|
|
2847
2909
|
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
this.previewSrc = previewSrc;
|
|
2855
|
-
this.previewable = parseBoolean(previewable);
|
|
2856
|
-
this.pendingPreview = pendingPreview;
|
|
2857
|
-
this.altText = altText || "";
|
|
2858
|
-
this.caption = caption || "";
|
|
2859
|
-
this.contentType = contentType || "";
|
|
2860
|
-
this.fileName = fileName || "";
|
|
2861
|
-
this.fileSize = fileSize;
|
|
2862
|
-
this.width = width;
|
|
2863
|
-
this.height = height;
|
|
2864
|
-
this.uploadError = uploadError;
|
|
2865
|
-
|
|
2866
|
-
this.editor = $getEditor();
|
|
2910
|
+
dispatchInsertCodeBlock() {
|
|
2911
|
+
if (this.selection.hasSelectedWordsInSingleLine) {
|
|
2912
|
+
this.#toggleInlineCode();
|
|
2913
|
+
} else {
|
|
2914
|
+
this.contents.toggleCodeBlock();
|
|
2915
|
+
}
|
|
2867
2916
|
}
|
|
2868
2917
|
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
if (
|
|
2918
|
+
#toggleInlineCode() {
|
|
2919
|
+
const selection = $getSelection();
|
|
2920
|
+
if (!$isRangeSelection(selection)) return
|
|
2872
2921
|
|
|
2873
|
-
|
|
2922
|
+
if (!selection.isCollapsed()) {
|
|
2923
|
+
const textNodes = selection.getNodes().filter($isTextNode);
|
|
2924
|
+
const applyingCode = !textNodes.every((node) => node.hasFormat("code"));
|
|
2874
2925
|
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
} else if (this.isVideo) {
|
|
2879
|
-
figure.appendChild(this.#createDOMForFile());
|
|
2880
|
-
figure.appendChild(this.#createEditableCaption());
|
|
2881
|
-
} else {
|
|
2882
|
-
figure.appendChild(this.#createDOMForFile());
|
|
2883
|
-
figure.appendChild(this.#createDOMForNotImage());
|
|
2926
|
+
if (applyingCode) {
|
|
2927
|
+
this.#stripInlineFormattingFromSelection(selection, textNodes);
|
|
2928
|
+
}
|
|
2884
2929
|
}
|
|
2885
2930
|
|
|
2886
|
-
|
|
2931
|
+
this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code");
|
|
2887
2932
|
}
|
|
2888
2933
|
|
|
2889
|
-
|
|
2890
|
-
|
|
2934
|
+
// Strip all inline formatting (bold, italic, etc.) from the selected text
|
|
2935
|
+
// nodes so that applying code produces a single merged <code> element instead
|
|
2936
|
+
// of one per differently-formatted span.
|
|
2937
|
+
#stripInlineFormattingFromSelection(selection, textNodes) {
|
|
2938
|
+
const isBackward = selection.isBackward();
|
|
2939
|
+
const startPoint = isBackward ? selection.focus : selection.anchor;
|
|
2940
|
+
const endPoint = isBackward ? selection.anchor : selection.focus;
|
|
2891
2941
|
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2942
|
+
for (let i = 0; i < textNodes.length; i++) {
|
|
2943
|
+
const node = textNodes[i];
|
|
2944
|
+
if (node.getFormat() === 0) continue
|
|
2945
|
+
|
|
2946
|
+
const isFirst = i === 0;
|
|
2947
|
+
const isLast = i === textNodes.length - 1;
|
|
2948
|
+
const startOffset = isFirst && startPoint.type === "text" ? startPoint.offset : 0;
|
|
2949
|
+
const endOffset = isLast && endPoint.type === "text" ? endPoint.offset : node.getTextContentSize();
|
|
2950
|
+
|
|
2951
|
+
if (startOffset === 0 && endOffset === node.getTextContentSize()) {
|
|
2952
|
+
node.setFormat(0);
|
|
2953
|
+
} else {
|
|
2954
|
+
const splits = node.splitText(startOffset, endOffset);
|
|
2955
|
+
const target = startOffset === 0 ? splits[0] : splits[1];
|
|
2956
|
+
target.setFormat(0);
|
|
2957
|
+
|
|
2958
|
+
if (isFirst && startPoint.type === "text") {
|
|
2959
|
+
startPoint.set(target.getKey(), 0, "text");
|
|
2960
|
+
}
|
|
2961
|
+
if (isLast && endPoint.type === "text") {
|
|
2962
|
+
endPoint.set(target.getKey(), endOffset - startOffset, "text");
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2895
2965
|
}
|
|
2966
|
+
}
|
|
2896
2967
|
|
|
2897
|
-
|
|
2968
|
+
dispatchSetCodeLanguage(language) {
|
|
2969
|
+
this.editor.update(() => {
|
|
2970
|
+
if (!this.selection.isInsideCodeBlock) return
|
|
2971
|
+
|
|
2972
|
+
const codeNode = this.selection.nearestNodeOfType(CodeNode);
|
|
2973
|
+
if (!codeNode) return
|
|
2974
|
+
|
|
2975
|
+
codeNode.setLanguage(language);
|
|
2976
|
+
});
|
|
2898
2977
|
}
|
|
2899
2978
|
|
|
2900
|
-
|
|
2901
|
-
|
|
2979
|
+
dispatchInsertHorizontalDivider() {
|
|
2980
|
+
this.contents.insertAtCursorEnsuringLineBelow(new HorizontalDividerNode());
|
|
2981
|
+
this.editor.focus();
|
|
2902
2982
|
}
|
|
2903
2983
|
|
|
2904
|
-
|
|
2905
|
-
|
|
2984
|
+
dispatchSetFormatHeadingLarge() {
|
|
2985
|
+
this.contents.applyHeadingFormat("h2");
|
|
2906
2986
|
}
|
|
2907
2987
|
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
previewable: this.previewable || null,
|
|
2912
|
-
url: this.src,
|
|
2913
|
-
alt: this.altText,
|
|
2914
|
-
caption: this.caption,
|
|
2915
|
-
"content-type": this.contentType,
|
|
2916
|
-
filename: this.fileName,
|
|
2917
|
-
filesize: this.fileSize,
|
|
2918
|
-
width: this.width,
|
|
2919
|
-
height: this.height,
|
|
2920
|
-
presentation: "gallery"
|
|
2921
|
-
});
|
|
2988
|
+
dispatchSetFormatHeadingMedium() {
|
|
2989
|
+
this.contents.applyHeadingFormat("h3");
|
|
2990
|
+
}
|
|
2922
2991
|
|
|
2923
|
-
|
|
2992
|
+
dispatchSetFormatHeadingSmall() {
|
|
2993
|
+
this.contents.applyHeadingFormat("h4");
|
|
2924
2994
|
}
|
|
2925
2995
|
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
type: "action_text_attachment",
|
|
2929
|
-
version: 1,
|
|
2930
|
-
tagName: this.tagName,
|
|
2931
|
-
sgid: this.sgid,
|
|
2932
|
-
src: this.src,
|
|
2933
|
-
previewable: this.previewable,
|
|
2934
|
-
altText: this.altText,
|
|
2935
|
-
caption: this.caption,
|
|
2936
|
-
contentType: this.contentType,
|
|
2937
|
-
fileName: this.fileName,
|
|
2938
|
-
fileSize: this.fileSize,
|
|
2939
|
-
width: this.width,
|
|
2940
|
-
height: this.height
|
|
2941
|
-
}
|
|
2996
|
+
dispatchSetFormatParagraph() {
|
|
2997
|
+
this.contents.applyParagraphFormat();
|
|
2942
2998
|
}
|
|
2943
2999
|
|
|
2944
|
-
|
|
2945
|
-
|
|
3000
|
+
dispatchClearFormatting() {
|
|
3001
|
+
this.contents.clearFormatting();
|
|
2946
3002
|
}
|
|
2947
3003
|
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
figure.classList.add("attachment--error");
|
|
2951
|
-
figure.appendChild(createElement("div", { innerText: `Error uploading ${this.fileName || "file"}` }));
|
|
2952
|
-
return figure
|
|
3004
|
+
dispatchUploadImage() {
|
|
3005
|
+
this.#dispatchUploadAttachment("image/*,video/*");
|
|
2953
3006
|
}
|
|
2954
3007
|
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
figure.dataset.lexicalNodeKey = this.__key;
|
|
3008
|
+
dispatchUploadFile() {
|
|
3009
|
+
this.#dispatchUploadAttachment();
|
|
3010
|
+
}
|
|
2959
3011
|
|
|
2960
|
-
|
|
2961
|
-
|
|
3012
|
+
#dispatchUploadAttachment(accept = null) {
|
|
3013
|
+
const attributes = {
|
|
3014
|
+
type: "file",
|
|
3015
|
+
multiple: true,
|
|
3016
|
+
style: "display: none;",
|
|
3017
|
+
onchange: ({ target: { files } }) => {
|
|
3018
|
+
this.contents.uploadFiles(files, { selectLast: true });
|
|
3019
|
+
}
|
|
3020
|
+
};
|
|
2962
3021
|
|
|
2963
|
-
|
|
3022
|
+
if (accept) attributes.accept = accept;
|
|
3023
|
+
|
|
3024
|
+
const input = createElement("input", attributes);
|
|
3025
|
+
|
|
3026
|
+
// Append and remove to make testable
|
|
3027
|
+
this.editorElement.appendChild(input);
|
|
3028
|
+
input.click();
|
|
3029
|
+
setTimeout(() => input.remove(), 1000);
|
|
2964
3030
|
}
|
|
2965
3031
|
|
|
2966
|
-
|
|
2967
|
-
|
|
3032
|
+
dispatchInsertTable() {
|
|
3033
|
+
this.editor.dispatchCommand(INSERT_TABLE_COMMAND, { "rows": 3, "columns": 3, "includeHeaders": true });
|
|
2968
3034
|
}
|
|
2969
3035
|
|
|
2970
|
-
|
|
2971
|
-
|
|
3036
|
+
dispatchUndo() {
|
|
3037
|
+
this.editor.dispatchCommand(UNDO_COMMAND, undefined);
|
|
2972
3038
|
}
|
|
2973
3039
|
|
|
2974
|
-
|
|
2975
|
-
|
|
3040
|
+
dispatchRedo() {
|
|
3041
|
+
this.editor.dispatchCommand(REDO_COMMAND, undefined);
|
|
2976
3042
|
}
|
|
2977
3043
|
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
figure.appendChild(this.#createDOMForFile());
|
|
2981
|
-
figure.appendChild(this.#createDOMForNotImage());
|
|
2982
|
-
this.#pollForPreview(figure);
|
|
2983
|
-
return figure
|
|
3044
|
+
dispose() {
|
|
3045
|
+
this.#listeners.dispose();
|
|
2984
3046
|
}
|
|
2985
3047
|
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
3048
|
+
#registerCommands() {
|
|
3049
|
+
for (const command of COMMANDS) {
|
|
3050
|
+
const methodName = `dispatch${capitalize(command)}`;
|
|
3051
|
+
this.#registerCommandHandler(command, 0, this[methodName].bind(this));
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
this.#registerCommandHandler(PASTE_COMMAND, COMMAND_PRIORITY_LOW, this.dispatchPaste.bind(this));
|
|
2990
3055
|
}
|
|
2991
3056
|
|
|
2992
|
-
|
|
2993
|
-
this.editor.
|
|
2994
|
-
[this.getKey()]: { replace: node }
|
|
2995
|
-
});
|
|
3057
|
+
#registerCommandHandler(command, priority, handler) {
|
|
3058
|
+
this.#listeners.track(this.editor.registerCommand(command, handler, priority));
|
|
2996
3059
|
}
|
|
2997
3060
|
|
|
2998
|
-
#
|
|
2999
|
-
|
|
3000
|
-
|
|
3061
|
+
#registerKeyboardCommands() {
|
|
3062
|
+
this.#registerCommandHandler(KEY_ARROW_RIGHT_COMMAND, COMMAND_PRIORITY_NORMAL, this.#handleArrowRightKey.bind(this));
|
|
3063
|
+
this.#registerCommandHandler(KEY_TAB_COMMAND, COMMAND_PRIORITY_NORMAL, this.#handleTabKey.bind(this));
|
|
3064
|
+
}
|
|
3001
3065
|
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3066
|
+
#handleArrowRightKey(event) {
|
|
3067
|
+
const selection = $getSelection();
|
|
3068
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed()) return false
|
|
3069
|
+
if (this.selection.isInsideCodeBlock || !selection.hasFormat("code")) return false
|
|
3005
3070
|
|
|
3006
|
-
|
|
3007
|
-
|
|
3071
|
+
const anchorNode = selection.anchor.getNode();
|
|
3072
|
+
if (!$isTextNode(anchorNode) || selection.anchor.offset !== anchorNode.getTextContentSize()) return false
|
|
3073
|
+
if (anchorNode.getNextSibling() !== null) return false
|
|
3074
|
+
|
|
3075
|
+
event.preventDefault();
|
|
3076
|
+
selection.toggleFormat("code");
|
|
3077
|
+
return true
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
#registerDragAndDropHandlers() {
|
|
3081
|
+
if (this.editorElement.supportsAttachments) {
|
|
3082
|
+
this.dragCounter = 0;
|
|
3083
|
+
const root = this.editor.getRootElement();
|
|
3084
|
+
this.#listeners.track(
|
|
3085
|
+
registerEventListener(root, "dragover", this.#handleDragOver.bind(this)),
|
|
3086
|
+
registerEventListener(root, "drop", this.#handleDrop.bind(this)),
|
|
3087
|
+
registerEventListener(root, "dragenter", this.#handleDragEnter.bind(this)),
|
|
3088
|
+
registerEventListener(root, "dragleave", this.#handleDragLeave.bind(this))
|
|
3089
|
+
);
|
|
3008
3090
|
}
|
|
3009
|
-
|
|
3010
|
-
const container = createElement("div", { className: "attachment__container" });
|
|
3011
|
-
container.appendChild(img);
|
|
3012
|
-
return container
|
|
3013
3091
|
}
|
|
3014
3092
|
|
|
3015
|
-
#
|
|
3016
|
-
|
|
3017
|
-
const serverImage = new Image();
|
|
3018
|
-
|
|
3019
|
-
serverImage.onload = () => this.#handleImageLoaded(img, previewSrc);
|
|
3020
|
-
serverImage.onerror = () => this.#handleImageLoadError(previewSrc);
|
|
3021
|
-
serverImage.src = this.src;
|
|
3022
|
-
}
|
|
3093
|
+
#handleDragEnter(event) {
|
|
3094
|
+
if (this.#isInternalDrag(event)) return
|
|
3023
3095
|
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3096
|
+
this.dragCounter++;
|
|
3097
|
+
if (this.dragCounter === 1) {
|
|
3098
|
+
this.#saveSelectionBeforeDrag();
|
|
3099
|
+
this.editor.getRootElement().classList.add("lexxy-editor--drag-over");
|
|
3100
|
+
}
|
|
3028
3101
|
}
|
|
3029
3102
|
|
|
3030
|
-
#
|
|
3031
|
-
this
|
|
3032
|
-
previewSrc: null,
|
|
3033
|
-
uploadError: true
|
|
3034
|
-
});
|
|
3035
|
-
this.#revokePreviewSrc(previewSrc);
|
|
3036
|
-
}
|
|
3103
|
+
#handleDragLeave(event) {
|
|
3104
|
+
if (this.#isInternalDrag(event)) return
|
|
3037
3105
|
|
|
3038
|
-
|
|
3039
|
-
if (
|
|
3106
|
+
this.dragCounter--;
|
|
3107
|
+
if (this.dragCounter === 0) {
|
|
3108
|
+
this.#selectionBeforeDrag = null;
|
|
3109
|
+
this.editor.getRootElement().classList.remove("lexxy-editor--drag-over");
|
|
3110
|
+
}
|
|
3040
3111
|
}
|
|
3041
3112
|
|
|
3042
|
-
#
|
|
3043
|
-
|
|
3044
|
-
if (!figure) return
|
|
3113
|
+
#handleDragOver(event) {
|
|
3114
|
+
if (this.#isInternalDrag(event)) return
|
|
3045
3115
|
|
|
3046
|
-
|
|
3047
|
-
figure.appendChild(this.#createDOMForFile());
|
|
3048
|
-
figure.appendChild(this.#createDOMForNotImage());
|
|
3049
|
-
});
|
|
3116
|
+
event.preventDefault();
|
|
3050
3117
|
}
|
|
3051
3118
|
|
|
3052
|
-
#
|
|
3053
|
-
|
|
3054
|
-
const maxAttempts = 10;
|
|
3119
|
+
#handleDrop(event) {
|
|
3120
|
+
if (this.#isInternalDrag(event)) return
|
|
3055
3121
|
|
|
3056
|
-
|
|
3057
|
-
if (!this.editor.read(() => this.isAttached())) return
|
|
3122
|
+
event.preventDefault();
|
|
3058
3123
|
|
|
3059
|
-
|
|
3060
|
-
|
|
3124
|
+
this.dragCounter = 0;
|
|
3125
|
+
this.editor.getRootElement().classList.remove("lexxy-editor--drag-over");
|
|
3061
3126
|
|
|
3062
|
-
|
|
3063
|
-
|
|
3127
|
+
const dataTransfer = event.dataTransfer;
|
|
3128
|
+
if (!dataTransfer) return
|
|
3064
3129
|
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
if (img.naturalWidth > 150 && img.naturalHeight > 150) {
|
|
3068
|
-
this.#swapToPreviewDOM(figure, cacheBustedSrc);
|
|
3069
|
-
} else {
|
|
3070
|
-
retry();
|
|
3071
|
-
}
|
|
3072
|
-
};
|
|
3073
|
-
img.onerror = () => retry();
|
|
3074
|
-
img.src = cacheBustedSrc;
|
|
3075
|
-
};
|
|
3130
|
+
const files = Array.from(dataTransfer.files);
|
|
3131
|
+
if (!files.length) return
|
|
3076
3132
|
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
if (attempt < maxAttempts && this.editor.read(() => this.isAttached())) {
|
|
3080
|
-
const delay = Math.min(2000 * Math.pow(1.5, attempt), 15000);
|
|
3081
|
-
setTimeout(tryLoad, delay);
|
|
3082
|
-
}
|
|
3083
|
-
};
|
|
3133
|
+
this.#restoreSelectionBeforeDrag();
|
|
3134
|
+
this.contents.uploadFiles(files, { selectLast: true });
|
|
3084
3135
|
|
|
3085
|
-
|
|
3086
|
-
setTimeout(tryLoad, 3000);
|
|
3136
|
+
this.editor.focus();
|
|
3087
3137
|
}
|
|
3088
3138
|
|
|
3089
|
-
#
|
|
3090
|
-
this
|
|
3091
|
-
|
|
3092
|
-
img.onerror = () => this.#swapPreviewToFileDOM(img);
|
|
3093
|
-
const container = createElement("div", { className: "attachment__container" });
|
|
3094
|
-
container.appendChild(img);
|
|
3095
|
-
figure.appendChild(container);
|
|
3096
|
-
figure.appendChild(this.#createEditableCaption());
|
|
3139
|
+
#saveSelectionBeforeDrag() {
|
|
3140
|
+
this.editor.getEditorState().read(() => {
|
|
3141
|
+
this.#selectionBeforeDrag = $getSelection()?.clone();
|
|
3097
3142
|
});
|
|
3098
|
-
|
|
3099
|
-
this.patchAndRewriteHistory({ pendingPreview: false });
|
|
3100
3143
|
}
|
|
3101
3144
|
|
|
3102
|
-
#
|
|
3103
|
-
|
|
3145
|
+
#restoreSelectionBeforeDrag() {
|
|
3146
|
+
if (!this.#selectionBeforeDrag) return
|
|
3104
3147
|
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
}
|
|
3148
|
+
this.editor.update(() => {
|
|
3149
|
+
$setSelection(this.#selectionBeforeDrag);
|
|
3150
|
+
});
|
|
3108
3151
|
|
|
3109
|
-
|
|
3152
|
+
this.#selectionBeforeDrag = null;
|
|
3110
3153
|
}
|
|
3111
3154
|
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
return { width: this.width, height: this.height }
|
|
3115
|
-
} else {
|
|
3116
|
-
return {}
|
|
3117
|
-
}
|
|
3155
|
+
#isInternalDrag(event) {
|
|
3156
|
+
return event.dataTransfer?.types.includes("application/x-lexxy-node-key")
|
|
3118
3157
|
}
|
|
3119
3158
|
|
|
3120
|
-
#
|
|
3121
|
-
|
|
3122
|
-
|
|
3159
|
+
#handleTabKey(event) {
|
|
3160
|
+
if (this.selection.isInsideList) {
|
|
3161
|
+
return this.#handleTabForList(event)
|
|
3162
|
+
} else if (this.selection.isInsideCodeBlock) {
|
|
3163
|
+
return this.#handleTabForCode()
|
|
3164
|
+
}
|
|
3165
|
+
return false
|
|
3123
3166
|
}
|
|
3124
3167
|
|
|
3125
|
-
#
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
const nameTag = createElement("strong", { className: "attachment__name", textContent: this.caption || this.fileName });
|
|
3129
|
-
|
|
3130
|
-
figcaption.appendChild(nameTag);
|
|
3131
|
-
|
|
3132
|
-
if (this.fileSize) {
|
|
3133
|
-
const sizeSpan = createElement("span", { className: "attachment__size", textContent: bytesToHumanSize(this.fileSize) });
|
|
3134
|
-
figcaption.appendChild(sizeSpan);
|
|
3135
|
-
}
|
|
3168
|
+
#handleTabForList(event) {
|
|
3169
|
+
if (event.shiftKey && !this.selection.isIndentedList) return false
|
|
3136
3170
|
|
|
3137
|
-
|
|
3171
|
+
event.preventDefault();
|
|
3172
|
+
const command = event.shiftKey? OUTDENT_CONTENT_COMMAND : INDENT_CONTENT_COMMAND;
|
|
3173
|
+
return this.editor.dispatchCommand(command)
|
|
3138
3174
|
}
|
|
3139
3175
|
|
|
3140
|
-
#
|
|
3141
|
-
const
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
placeholder: this.fileName,
|
|
3145
|
-
rows: "1"
|
|
3146
|
-
});
|
|
3147
|
-
|
|
3148
|
-
input.addEventListener("focusin", () => input.placeholder = "Add caption...");
|
|
3149
|
-
input.addEventListener("blur", (event) => this.#handleCaptionInputBlurred(event));
|
|
3150
|
-
input.addEventListener("keydown", (event) => this.#handleCaptionInputKeydown(event));
|
|
3151
|
-
input.addEventListener("copy", (event) => event.stopPropagation());
|
|
3152
|
-
input.addEventListener("cut", (event) => event.stopPropagation());
|
|
3153
|
-
input.addEventListener("paste", (event) => event.stopPropagation());
|
|
3176
|
+
#handleTabForCode() {
|
|
3177
|
+
const selection = $getSelection();
|
|
3178
|
+
return $isRangeSelection(selection) && selection.isCollapsed()
|
|
3179
|
+
}
|
|
3154
3180
|
|
|
3155
|
-
|
|
3181
|
+
}
|
|
3156
3182
|
|
|
3157
|
-
|
|
3158
|
-
|
|
3183
|
+
function capitalize(str) {
|
|
3184
|
+
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
3185
|
+
}
|
|
3159
3186
|
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
}
|
|
3187
|
+
function debounce(fn, wait) {
|
|
3188
|
+
let timeout;
|
|
3163
3189
|
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
this.getWritable().caption = input.value;
|
|
3168
|
-
});
|
|
3190
|
+
return (...args) => {
|
|
3191
|
+
clearTimeout(timeout);
|
|
3192
|
+
timeout = setTimeout(() => fn(...args), wait);
|
|
3169
3193
|
}
|
|
3194
|
+
}
|
|
3170
3195
|
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
event.preventDefault();
|
|
3174
|
-
event.target.blur();
|
|
3196
|
+
function debounceAsync(fn, wait) {
|
|
3197
|
+
let timeout;
|
|
3175
3198
|
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
this.selectNext(0, 0);
|
|
3179
|
-
}, {
|
|
3180
|
-
tag: HISTORY_MERGE_TAG
|
|
3181
|
-
});
|
|
3182
|
-
}
|
|
3199
|
+
return (...args) => {
|
|
3200
|
+
clearTimeout(timeout);
|
|
3183
3201
|
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3202
|
+
return new Promise((resolve, reject) => {
|
|
3203
|
+
timeout = setTimeout(async () => {
|
|
3204
|
+
try {
|
|
3205
|
+
const result = await fn(...args);
|
|
3206
|
+
resolve(result);
|
|
3207
|
+
} catch (err) {
|
|
3208
|
+
reject(err);
|
|
3209
|
+
}
|
|
3210
|
+
}, wait);
|
|
3211
|
+
})
|
|
3188
3212
|
}
|
|
3189
3213
|
}
|
|
3190
3214
|
|
|
3191
|
-
function
|
|
3192
|
-
return new
|
|
3215
|
+
function delay(ms) {
|
|
3216
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
3193
3217
|
}
|
|
3194
3218
|
|
|
3195
|
-
function
|
|
3196
|
-
return
|
|
3219
|
+
function nextFrame() {
|
|
3220
|
+
return new Promise(requestAnimationFrame)
|
|
3197
3221
|
}
|
|
3198
3222
|
|
|
3199
3223
|
class Selection {
|
|
@@ -4782,7 +4806,7 @@ class Contents {
|
|
|
4782
4806
|
this.editor.update(() => {
|
|
4783
4807
|
if ($hasUpdateTag(PASTE_TAG)) this.#stripTableCellColorStyles(doc);
|
|
4784
4808
|
|
|
4785
|
-
const nodes = $
|
|
4809
|
+
const nodes = $generateFilteredNodesFromDOM(this.editorElement, doc);
|
|
4786
4810
|
if (!this.#insertUploadNodes(nodes)) {
|
|
4787
4811
|
this.insertAtCursor(...nodes);
|
|
4788
4812
|
}
|
|
@@ -5331,16 +5355,19 @@ class Contents {
|
|
|
5331
5355
|
|
|
5332
5356
|
#createCustomAttachmentNodeWithHtml(html, options = {}) {
|
|
5333
5357
|
const attachmentConfig = typeof options === "object" ? options : {};
|
|
5334
|
-
|
|
5358
|
+
const contentType = attachmentConfig.contentType || "text/html";
|
|
5359
|
+
if (!this.editorElement.permitsAttachmentContentType(contentType)) {
|
|
5360
|
+
return this.#createHtmlNodeWith(html)
|
|
5361
|
+
}
|
|
5335
5362
|
return new CustomActionTextAttachmentNode({
|
|
5336
5363
|
sgid: attachmentConfig.sgid || null,
|
|
5337
|
-
contentType
|
|
5338
|
-
innerHtml: html
|
|
5364
|
+
contentType,
|
|
5365
|
+
innerHtml: html,
|
|
5339
5366
|
})
|
|
5340
5367
|
}
|
|
5341
5368
|
|
|
5342
5369
|
#createHtmlNodeWith(html) {
|
|
5343
|
-
const htmlNodes = $
|
|
5370
|
+
const htmlNodes = $generateFilteredNodesFromDOM(this.editorElement, parseHtml(html));
|
|
5344
5371
|
return htmlNodes[0] || $createParagraphNode()
|
|
5345
5372
|
}
|
|
5346
5373
|
|
|
@@ -6920,6 +6947,25 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6920
6947
|
return this.dataset.blobUrlTemplate
|
|
6921
6948
|
}
|
|
6922
6949
|
|
|
6950
|
+
get permittedAttachmentTypes() {
|
|
6951
|
+
const raw = this.config.get("permittedAttachmentTypes");
|
|
6952
|
+
if (raw == null) {
|
|
6953
|
+
return null
|
|
6954
|
+
} else {
|
|
6955
|
+
const tokens = Array.isArray(raw) ? raw : String(raw).split(/\s+/);
|
|
6956
|
+
return Object.freeze(tokens.filter(t => t && t !== "false"))
|
|
6957
|
+
}
|
|
6958
|
+
}
|
|
6959
|
+
|
|
6960
|
+
permitsAttachmentContentType(contentType) {
|
|
6961
|
+
if (!this.supportsAttachments) {
|
|
6962
|
+
return false
|
|
6963
|
+
} else {
|
|
6964
|
+
const list = this.permittedAttachmentTypes;
|
|
6965
|
+
return list === null || list.includes(contentType)
|
|
6966
|
+
}
|
|
6967
|
+
}
|
|
6968
|
+
|
|
6923
6969
|
get isEmpty() {
|
|
6924
6970
|
return [ "<p><br></p>", "<p></p>", "" ].includes(this.value.trim())
|
|
6925
6971
|
}
|
|
@@ -7042,7 +7088,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7042
7088
|
|
|
7043
7089
|
#parseHtmlIntoLexicalNodes(html) {
|
|
7044
7090
|
if (!html) html = "<p></p>";
|
|
7045
|
-
const nodes = $
|
|
7091
|
+
const nodes = $generateFilteredNodesFromDOM(this, parseHtml(`${html}`));
|
|
7046
7092
|
|
|
7047
7093
|
return nodes
|
|
7048
7094
|
.filter(this.#isNotWhitespaceOnlyNode)
|
|
@@ -7074,6 +7120,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7074
7120
|
this.#handleEnter();
|
|
7075
7121
|
this.#registerFocusEvents();
|
|
7076
7122
|
this.#registerHistoryEvents();
|
|
7123
|
+
this.#registerFileAcceptFilter();
|
|
7077
7124
|
this.#attachDebugHooks();
|
|
7078
7125
|
this.#attachToolbar();
|
|
7079
7126
|
this.#configureSanitizer();
|
|
@@ -7081,6 +7128,16 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7081
7128
|
this.#resetBeforeTurboCaches();
|
|
7082
7129
|
}
|
|
7083
7130
|
|
|
7131
|
+
#registerFileAcceptFilter() {
|
|
7132
|
+
this.#listeners.track(
|
|
7133
|
+
registerEventListener(this, "lexxy:file-accept", (event) => {
|
|
7134
|
+
if (!this.permitsAttachmentContentType(event.detail.file.type)) {
|
|
7135
|
+
event.preventDefault();
|
|
7136
|
+
}
|
|
7137
|
+
})
|
|
7138
|
+
);
|
|
7139
|
+
}
|
|
7140
|
+
|
|
7084
7141
|
#createEditor() {
|
|
7085
7142
|
this.editorContentElement ||= this.#createEditorContentElement();
|
|
7086
7143
|
this.appendChild(this.editorContentElement);
|
|
@@ -8024,6 +8081,8 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8024
8081
|
}
|
|
8025
8082
|
|
|
8026
8083
|
#addTriggerListener() {
|
|
8084
|
+
if (!this.#promptContentTypePermitted) return
|
|
8085
|
+
|
|
8027
8086
|
this.#popoverListeners.track(this.#editor.registerUpdateListener(({ editorState }) => {
|
|
8028
8087
|
editorState.read(() => {
|
|
8029
8088
|
if (this.#selection.isInsideCodeBlock) return
|
|
@@ -8057,6 +8116,19 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8057
8116
|
}));
|
|
8058
8117
|
}
|
|
8059
8118
|
|
|
8119
|
+
get #promptContentTypePermitted() {
|
|
8120
|
+
const el = this.#editorElement;
|
|
8121
|
+
if (!el.supportsAttachments) {
|
|
8122
|
+
return false
|
|
8123
|
+
} else {
|
|
8124
|
+
const templates = Array.from(this.querySelectorAll("template[type='editor']"));
|
|
8125
|
+
const types = templates.length
|
|
8126
|
+
? templates.map(t => t.getAttribute("content-type") || this.#defaultPromptContentType)
|
|
8127
|
+
: [ this.#defaultPromptContentType ];
|
|
8128
|
+
return types.some(t => el.permitsAttachmentContentType(t))
|
|
8129
|
+
}
|
|
8130
|
+
}
|
|
8131
|
+
|
|
8060
8132
|
#addCursorPositionListener() {
|
|
8061
8133
|
this.#popoverListeners.track(this.#editor.registerUpdateListener(({ editorState }) => {
|
|
8062
8134
|
if (this.closed) return
|
|
@@ -8363,7 +8435,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8363
8435
|
}
|
|
8364
8436
|
|
|
8365
8437
|
#buildEditableTextNodes(template) {
|
|
8366
|
-
return $
|
|
8438
|
+
return $generateFilteredNodesFromDOM(this.#editorElement, parseHtml(`${template.innerHTML}`))
|
|
8367
8439
|
}
|
|
8368
8440
|
|
|
8369
8441
|
#insertTemplatesAsAttachments(templates, stringToReplace, fallbackSgid = null) {
|
|
@@ -8375,8 +8447,10 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8375
8447
|
}
|
|
8376
8448
|
|
|
8377
8449
|
#buildAttachmentNodes(templates, fallbackSgid = null) {
|
|
8378
|
-
return templates
|
|
8379
|
-
template => this.#
|
|
8450
|
+
return templates
|
|
8451
|
+
.filter(template => this.#editorElement.permitsAttachmentContentType(
|
|
8452
|
+
template.getAttribute("content-type") || this.#defaultPromptContentType))
|
|
8453
|
+
.map(template => this.#buildAttachmentNode(
|
|
8380
8454
|
template.innerHTML,
|
|
8381
8455
|
template.getAttribute("content-type") || this.#defaultPromptContentType,
|
|
8382
8456
|
template.getAttribute("sgid") || fallbackSgid
|