@37signals/lexxy 0.9.9-beta.preview4 → 0.9.9-beta.preview6
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 +1970 -1853
- package/dist/lexxy_helpers.esm.js +1 -1
- 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
|
-
import { getStyleObjectFromCSS, getCSSFromStyleObject, $isAtNodeEnd, $getSelectionStyleValueForProperty, $patchStyleText, $
|
|
5
|
-
import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_REDO_COMMAND, $getSelection, $isRangeSelection, DecoratorNode, $
|
|
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, $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
|
-
import { RichTextExtension, $isQuoteNode, $isHeadingNode,
|
|
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",
|
|
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
|
|
1040
1210
|
}
|
|
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
1211
|
}
|
|
1094
|
-
|
|
1212
|
+
return true
|
|
1213
|
+
}
|
|
1095
1214
|
|
|
1096
|
-
function
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1215
|
+
function isAttachmentSpacerTextNode(node, previousNode, index, childCount) {
|
|
1216
|
+
return $isTextNode(node)
|
|
1217
|
+
&& node.getTextContent() === " "
|
|
1218
|
+
&& index === childCount - 1
|
|
1219
|
+
&& previousNode instanceof CustomActionTextAttachmentNode
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function $splitParagraphsAtLineBreakBoundaries(selection) {
|
|
1223
|
+
$ensureForwardRangeSelection(selection);
|
|
1224
|
+
|
|
1225
|
+
// Split focus first so the anchor split position stays valid.
|
|
1226
|
+
$splitAtNearestLineBreak(selection.focus, "next");
|
|
1227
|
+
$splitAtNearestLineBreak(selection.anchor, "previous");
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function $splitAtNearestLineBreak(point, direction) {
|
|
1231
|
+
const paragraph = point.getNode().getTopLevelElement();
|
|
1232
|
+
if (!paragraph || !$isParagraphNode(paragraph)) return
|
|
1233
|
+
|
|
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
|
|
1238
|
+
|
|
1239
|
+
const lineBreak = lineBreakCaret.origin;
|
|
1240
|
+
const isEdge = lineBreakCaret.getNodeAtCaret() === null;
|
|
1241
|
+
|
|
1242
|
+
if (!isEdge) {
|
|
1243
|
+
$splitNode(paragraph, lineBreak.getIndexWithinParent());
|
|
1102
1244
|
}
|
|
1103
1245
|
|
|
1104
|
-
|
|
1246
|
+
lineBreak.remove();
|
|
1105
1247
|
}
|
|
1106
1248
|
|
|
1107
|
-
function
|
|
1108
|
-
|
|
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
|
|
1109
1254
|
}
|
|
1110
1255
|
|
|
1111
|
-
|
|
1112
|
-
|
|
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");
|
|
1113
1260
|
|
|
1114
|
-
|
|
1115
|
-
|
|
1261
|
+
class RewritableHistoryExtension extends LexxyExtension {
|
|
1262
|
+
#historyState = null
|
|
1263
|
+
|
|
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
|
+
})
|
|
1116
1279
|
}
|
|
1117
1280
|
|
|
1118
|
-
|
|
1119
|
-
return this.#
|
|
1281
|
+
get historyState() {
|
|
1282
|
+
return this.#historyState
|
|
1120
1283
|
}
|
|
1121
1284
|
|
|
1122
|
-
get(
|
|
1123
|
-
const
|
|
1124
|
-
|
|
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)
|
|
1125
1289
|
}
|
|
1126
|
-
}
|
|
1127
1290
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1291
|
+
#rewriteHistory(rewrites) {
|
|
1292
|
+
this.#applyRewritesImmediatelyToCurrentState(rewrites);
|
|
1293
|
+
this.#applyRewritesToHistory(rewrites);
|
|
1131
1294
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
attachmentContentTypeNamespace: "actiontext",
|
|
1135
|
-
authenticatedUploads: false,
|
|
1136
|
-
extensions: []
|
|
1137
|
-
});
|
|
1295
|
+
return true
|
|
1296
|
+
}
|
|
1138
1297
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
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": []
|
|
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
|
|
1303
|
+
|
|
1304
|
+
if (patch) Object.assign(node.getWritable(), patch);
|
|
1305
|
+
if (replace) node.replace(replace);
|
|
1156
1306
|
}
|
|
1157
|
-
}
|
|
1307
|
+
}, { discrete: true, tag: this.#getBackgroundUpdateTags() });
|
|
1158
1308
|
}
|
|
1159
|
-
});
|
|
1160
1309
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1310
|
+
#applyRewritesToHistory(rewrites) {
|
|
1311
|
+
const nodeKeys = Object.keys(rewrites);
|
|
1312
|
+
|
|
1313
|
+
for (const entry of this.#allHistoryEntries) {
|
|
1314
|
+
if (!this.#entryHasSomeKeys(entry, nodeKeys)) continue
|
|
1315
|
+
|
|
1316
|
+
const editorState = entry.editorState = safeCloneEditorState(entry.editorState);
|
|
1317
|
+
|
|
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
|
+
}
|
|
1167
1328
|
}
|
|
1168
|
-
presets.merge(newPresets);
|
|
1169
1329
|
}
|
|
1170
|
-
};
|
|
1171
1330
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
}
|
|
1331
|
+
#entryHasSomeKeys(entry, nodeKeys) {
|
|
1332
|
+
return nodeKeys.some(key => entry.editorState._nodeMap.has(key))
|
|
1333
|
+
}
|
|
1176
1334
|
|
|
1177
|
-
|
|
1178
|
-
|
|
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
|
+
}
|
|
1179
1348
|
}
|
|
1180
1349
|
|
|
1181
|
-
function
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
const value = bytes / Math.pow(1024, i);
|
|
1186
|
-
return `${ value.toFixed(2) } ${ sizes[i] }`
|
|
1350
|
+
function $cloneNodeWithPatch(node, patch) {
|
|
1351
|
+
const clone = $cloneWithProperties(node);
|
|
1352
|
+
Object.assign(clone, patch);
|
|
1353
|
+
return clone
|
|
1187
1354
|
}
|
|
1188
1355
|
|
|
1189
|
-
function
|
|
1190
|
-
|
|
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
|
|
1191
1363
|
}
|
|
1192
1364
|
|
|
1193
|
-
//
|
|
1194
|
-
//
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
}
|
|
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
|
|
1201
1372
|
}
|
|
1202
1373
|
|
|
1203
|
-
class
|
|
1374
|
+
class ActionTextAttachmentNode extends DecoratorNode {
|
|
1204
1375
|
static getType() {
|
|
1205
|
-
return "
|
|
1376
|
+
return "action_text_attachment"
|
|
1206
1377
|
}
|
|
1207
1378
|
|
|
1208
1379
|
static clone(node) {
|
|
1209
|
-
return new
|
|
1380
|
+
return new ActionTextAttachmentNode({ ...node }, node.__key)
|
|
1210
1381
|
}
|
|
1211
1382
|
|
|
1212
1383
|
static importJSON(serializedNode) {
|
|
1213
|
-
return new
|
|
1384
|
+
return new ActionTextAttachmentNode({ ...serializedNode })
|
|
1214
1385
|
}
|
|
1215
1386
|
|
|
1216
1387
|
static importDOM() {
|
|
1217
1388
|
return {
|
|
1218
|
-
[this.TAG_NAME]: (
|
|
1219
|
-
if (!element.getAttribute("content")) {
|
|
1220
|
-
return null
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1389
|
+
[this.TAG_NAME]: () => {
|
|
1223
1390
|
return {
|
|
1224
|
-
conversion: (attachment) => {
|
|
1225
|
-
|
|
1226
|
-
const nodes = [];
|
|
1227
|
-
const previousSibling = attachment.previousSibling;
|
|
1228
|
-
if (previousSibling && previousSibling.nodeType === Node.TEXT_NODE && /\s$/.test(previousSibling.textContent)) {
|
|
1229
|
-
nodes.push($createTextNode(" "));
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
const innerHtml = parseAttachmentContent(attachment.getAttribute("content"));
|
|
1233
|
-
|
|
1234
|
-
nodes.push(new CustomActionTextAttachmentNode({
|
|
1391
|
+
conversion: (attachment) => ({
|
|
1392
|
+
node: new ActionTextAttachmentNode({
|
|
1235
1393
|
sgid: attachment.getAttribute("sgid"),
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
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
|
+
})
|
|
1244
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/*";
|
|
1245
1430
|
|
|
1246
|
-
return {
|
|
1247
|
-
|
|
1248
|
-
|
|
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,1860 +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
1547
|
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
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
|
|
1553
|
+
}
|
|
1329
1554
|
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1555
|
+
createAttachmentFigure(previewable = this.isPreviewableAttachment) {
|
|
1556
|
+
const figure = createAttachmentFigure(this.contentType, previewable, this.fileName);
|
|
1557
|
+
figure.draggable = true;
|
|
1558
|
+
figure.dataset.lexicalNodeKey = this.__key;
|
|
1333
1559
|
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
const parent = node.createRequiredParent();
|
|
1339
|
-
return $wrapNodeInElement(node, parent)
|
|
1340
|
-
} else {
|
|
1341
|
-
return node
|
|
1560
|
+
const deleteButton = createElement("lexxy-node-delete-button");
|
|
1561
|
+
figure.appendChild(deleteButton);
|
|
1562
|
+
|
|
1563
|
+
return figure
|
|
1342
1564
|
}
|
|
1343
|
-
}
|
|
1344
1565
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
}
|
|
1566
|
+
get isPreviewableAttachment() {
|
|
1567
|
+
return this.isPreviewableImage || this.previewable
|
|
1568
|
+
}
|
|
1349
1569
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
}
|
|
1570
|
+
get isPreviewableImage() {
|
|
1571
|
+
return isPreviewableImage(this.contentType)
|
|
1572
|
+
}
|
|
1354
1573
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
return $isAtNodeEdge(point, true) || $isAtNodeEdge(point, false)
|
|
1358
|
-
} else {
|
|
1359
|
-
return atStart ? $isAtNodeStart(point) : $isAtNodeEnd(point)
|
|
1574
|
+
get isVideo() {
|
|
1575
|
+
return this.contentType.startsWith("video/")
|
|
1360
1576
|
}
|
|
1361
|
-
}
|
|
1362
1577
|
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
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
|
+
}
|
|
1366
1585
|
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
let childNode = originalForChild(lexicalNode, parentNode);
|
|
1586
|
+
patchAndRewriteHistory(patch) {
|
|
1587
|
+
this.editor.dispatchCommand(REWRITE_HISTORY_COMMAND, {
|
|
1588
|
+
[this.getKey()]: { patch }
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1373
1591
|
|
|
1592
|
+
replaceAndRewriteHistory(node) {
|
|
1593
|
+
this.editor.dispatchCommand(REWRITE_HISTORY_COMMAND, {
|
|
1594
|
+
[this.getKey()]: { replace: node }
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1374
1597
|
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
childNode
|
|
1379
|
-
);
|
|
1380
|
-
return childNode
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
}))
|
|
1384
|
-
}
|
|
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 });
|
|
1385
1601
|
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
if (!converter) return null
|
|
1602
|
+
if (this.previewable && !this.isPreviewableImage) {
|
|
1603
|
+
img.onerror = () => this.#swapPreviewToFileDOM(img);
|
|
1604
|
+
}
|
|
1390
1605
|
|
|
1391
|
-
|
|
1392
|
-
|
|
1606
|
+
if (this.previewSrc) {
|
|
1607
|
+
this.#preloadAndSwapSrc(img);
|
|
1608
|
+
}
|
|
1393
1609
|
|
|
1394
|
-
|
|
1610
|
+
const container = createElement("div", { className: "attachment__container" });
|
|
1611
|
+
container.appendChild(img);
|
|
1612
|
+
return container
|
|
1395
1613
|
}
|
|
1396
|
-
}
|
|
1397
1614
|
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
const children = elementNode.getChildren();
|
|
1402
|
-
if (children.length === 0) return true
|
|
1403
|
-
|
|
1404
|
-
const lastChild = children[children.length - 1];
|
|
1405
|
-
|
|
1406
|
-
if (anchorNode === elementNode.getLatest() && selection.anchor.offset === children.length) return true
|
|
1407
|
-
if (anchorNode === lastChild) return true
|
|
1408
|
-
|
|
1409
|
-
const lastLineBreakIndex = children.findLastIndex(child => $isLineBreakNode(child));
|
|
1410
|
-
if (lastLineBreakIndex === -1) return true
|
|
1411
|
-
|
|
1412
|
-
const anchorIndex = children.indexOf(anchorNode);
|
|
1413
|
-
return anchorIndex > lastLineBreakIndex
|
|
1414
|
-
}
|
|
1615
|
+
#preloadAndSwapSrc(img) {
|
|
1616
|
+
const previewSrc = this.previewSrc;
|
|
1617
|
+
const serverImage = new Image();
|
|
1415
1618
|
|
|
1416
|
-
|
|
1417
|
-
|
|
1619
|
+
serverImage.onload = () => this.#handleImageLoaded(img, previewSrc);
|
|
1620
|
+
serverImage.onerror = () => this.#handleImageLoadError(previewSrc);
|
|
1621
|
+
serverImage.src = this.src;
|
|
1622
|
+
}
|
|
1418
1623
|
|
|
1419
|
-
|
|
1420
|
-
|
|
1624
|
+
#handleImageLoaded(img, previewSrc) {
|
|
1625
|
+
img.src = this.src;
|
|
1626
|
+
this.patchAndRewriteHistory({ previewSrc: null });
|
|
1627
|
+
this.#revokePreviewSrc(previewSrc);
|
|
1628
|
+
}
|
|
1421
1629
|
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
}
|
|
1630
|
+
#handleImageLoadError(previewSrc) {
|
|
1631
|
+
this.patchAndRewriteHistory({
|
|
1632
|
+
previewSrc: null,
|
|
1633
|
+
uploadError: true
|
|
1634
|
+
});
|
|
1635
|
+
this.#revokePreviewSrc(previewSrc);
|
|
1636
|
+
}
|
|
1427
1637
|
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
if ($isBlankNode(child)) {
|
|
1431
|
-
child.remove();
|
|
1432
|
-
} else {
|
|
1433
|
-
break
|
|
1434
|
-
}
|
|
1638
|
+
#revokePreviewSrc(previewSrc) {
|
|
1639
|
+
if (previewSrc?.startsWith("blob:")) URL.revokeObjectURL(previewSrc);
|
|
1435
1640
|
}
|
|
1436
|
-
}
|
|
1437
1641
|
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
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
|
-
}
|
|
1642
|
+
#swapPreviewToFileDOM(img) {
|
|
1643
|
+
const figure = img.closest("figure.attachment");
|
|
1644
|
+
if (!figure) return
|
|
1645
|
+
|
|
1646
|
+
this.#swapFigureContent(figure, "attachment--preview", "attachment--file", () => {
|
|
1647
|
+
figure.appendChild(this.#createDOMForFile());
|
|
1648
|
+
figure.appendChild(this.#createDOMForNotImage());
|
|
1649
|
+
});
|
|
1452
1650
|
}
|
|
1453
|
-
return true
|
|
1454
|
-
}
|
|
1455
1651
|
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
&& index === childCount - 1
|
|
1460
|
-
&& previousNode instanceof CustomActionTextAttachmentNode
|
|
1461
|
-
}
|
|
1652
|
+
#pollForPreview(figure) {
|
|
1653
|
+
let attempt = 0;
|
|
1654
|
+
const maxAttempts = 10;
|
|
1462
1655
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
// CSS custom properties). The container is created once and attached to
|
|
1466
|
-
// `document.body` once; subsequent child mutations happen *inside* the
|
|
1467
|
-
// contained subtree so they do not invalidate style on the rest of the page.
|
|
1468
|
-
//
|
|
1469
|
-
// Without this, `document.body.appendChild(...)` / `element.remove()` calls
|
|
1470
|
-
// forced the browser to re-evaluate every ancestor-dependent selector (`:has()`,
|
|
1471
|
-
// descendant combinators, universal sibling rules) across the document on each
|
|
1472
|
-
// invocation — a 13,000+ element style recalc per call on a typical Basecamp
|
|
1473
|
-
// page.
|
|
1656
|
+
const tryLoad = () => {
|
|
1657
|
+
if (!this.editor.read(() => this.isAttached())) return
|
|
1474
1658
|
|
|
1475
|
-
|
|
1659
|
+
const img = new Image();
|
|
1660
|
+
const cacheBustedSrc = `${this.src}${this.src.includes("?") ? "&" : "?"}_=${Date.now()}`;
|
|
1476
1661
|
|
|
1477
|
-
|
|
1478
|
-
|
|
1662
|
+
img.onload = () => {
|
|
1663
|
+
if (!this.editor.read(() => this.isAttached())) return
|
|
1479
1664
|
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
}
|
|
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
|
+
};
|
|
1491
1676
|
|
|
1492
|
-
|
|
1493
|
-
|
|
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
|
+
};
|
|
1494
1684
|
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
} else {
|
|
1498
|
-
return selection.hasFormat("highlight")
|
|
1685
|
+
// Give the server time to start processing before the first attempt
|
|
1686
|
+
setTimeout(tryLoad, 3000);
|
|
1499
1687
|
}
|
|
1500
|
-
}
|
|
1501
1688
|
|
|
1502
|
-
|
|
1503
|
-
|
|
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
|
+
});
|
|
1504
1698
|
|
|
1505
|
-
|
|
1506
|
-
if (!styles.color && !styles["background-color"]) {
|
|
1507
|
-
const anchorNode = selection.anchor.getNode();
|
|
1508
|
-
if ($isTextNode(anchorNode)) {
|
|
1509
|
-
styles = getStyleObjectFromCSS(anchorNode.getStyle());
|
|
1510
|
-
}
|
|
1699
|
+
this.patchAndRewriteHistory({ pendingPreview: false });
|
|
1511
1700
|
}
|
|
1512
1701
|
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
if (!color && !backgroundColor) return null
|
|
1702
|
+
#swapFigureContent(figure, fromClass, toClass, renderContent) {
|
|
1703
|
+
figure.className = figure.className.replace(fromClass, toClass);
|
|
1516
1704
|
|
|
1517
|
-
|
|
1518
|
-
|
|
1705
|
+
for (const child of [ ...figure.querySelectorAll(".attachment__container, .attachment__icon, figcaption") ]) {
|
|
1706
|
+
child.remove();
|
|
1707
|
+
}
|
|
1519
1708
|
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
return !!(styles.color || styles["background-color"])
|
|
1523
|
-
}
|
|
1709
|
+
renderContent();
|
|
1710
|
+
}
|
|
1524
1711
|
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
}
|
|
1712
|
+
get #imageDimensions() {
|
|
1713
|
+
if (this.width && this.height) {
|
|
1714
|
+
return { width: this.width, height: this.height }
|
|
1715
|
+
} else {
|
|
1716
|
+
return {}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1530
1719
|
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
this._allowedValues = allowedValues;
|
|
1535
|
-
this._canonicalValues = this.#allowedValuesIdentityObject;
|
|
1720
|
+
#createDOMForFile() {
|
|
1721
|
+
const extension = this.fileName ? this.fileName.split(".").pop().toLowerCase() : "unknown";
|
|
1722
|
+
return createElement("span", { className: "attachment__icon", textContent: `${extension}` })
|
|
1536
1723
|
}
|
|
1537
1724
|
|
|
1538
|
-
|
|
1539
|
-
const
|
|
1725
|
+
#createDOMForNotImage() {
|
|
1726
|
+
const figcaption = createElement("figcaption", { className: "attachment__caption" });
|
|
1540
1727
|
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1728
|
+
const nameTag = createElement("strong", { className: "attachment__name", textContent: this.caption || this.fileName });
|
|
1729
|
+
|
|
1730
|
+
figcaption.appendChild(nameTag);
|
|
1731
|
+
|
|
1732
|
+
if (this.fileSize) {
|
|
1733
|
+
const sizeSpan = createElement("span", { className: "attachment__size", textContent: bytesToHumanSize(this.fileSize) });
|
|
1734
|
+
figcaption.appendChild(sizeSpan);
|
|
1544
1735
|
}
|
|
1545
1736
|
|
|
1546
|
-
return
|
|
1737
|
+
return figcaption
|
|
1547
1738
|
}
|
|
1548
1739
|
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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
|
+
});
|
|
1552
1747
|
|
|
1553
|
-
|
|
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());
|
|
1554
1754
|
|
|
1555
|
-
|
|
1556
|
-
|
|
1755
|
+
caption.appendChild(input);
|
|
1756
|
+
|
|
1757
|
+
return caption
|
|
1557
1758
|
}
|
|
1558
1759
|
|
|
1559
|
-
#
|
|
1560
|
-
|
|
1561
|
-
if (index === -1) {
|
|
1562
|
-
index = this.#computedAllowedValues.indexOf(computeStyleValues(this._property, [ value ])[0]);
|
|
1563
|
-
}
|
|
1564
|
-
return index === -1 ? null : this._allowedValues[index]
|
|
1760
|
+
#handleCaptionInputBlurred(event) {
|
|
1761
|
+
this.#updateCaptionValueFromInput(event.target);
|
|
1565
1762
|
}
|
|
1566
1763
|
|
|
1567
|
-
|
|
1568
|
-
|
|
1764
|
+
#updateCaptionValueFromInput(input) {
|
|
1765
|
+
input.placeholder = this.fileName;
|
|
1766
|
+
this.editor.update(() => {
|
|
1767
|
+
this.getWritable().caption = input.value;
|
|
1768
|
+
});
|
|
1569
1769
|
}
|
|
1570
|
-
}
|
|
1571
1770
|
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
// page-wide style recalc on every canonicalization pass.
|
|
1577
|
-
function computeStyleValues(property, values) {
|
|
1578
|
-
const fragment = document.createDocumentFragment();
|
|
1771
|
+
#handleCaptionInputKeydown(event) {
|
|
1772
|
+
if (event.key === "Enter") {
|
|
1773
|
+
event.preventDefault();
|
|
1774
|
+
event.target.blur();
|
|
1579
1775
|
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
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
|
+
}
|
|
1585
1783
|
|
|
1586
|
-
|
|
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
|
+
}
|
|
1587
1790
|
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1791
|
+
function $createActionTextAttachmentNode(...args) {
|
|
1792
|
+
return new ActionTextAttachmentNode(...args)
|
|
1793
|
+
}
|
|
1591
1794
|
|
|
1592
|
-
|
|
1593
|
-
return
|
|
1795
|
+
function $isActionTextAttachmentNode(node) {
|
|
1796
|
+
return node instanceof ActionTextAttachmentNode
|
|
1594
1797
|
}
|
|
1595
1798
|
|
|
1596
|
-
|
|
1597
|
-
|
|
1799
|
+
function $generateFilteredNodesFromDOM(editorElement, doc) {
|
|
1800
|
+
const nodes = $generateNodesFromDOM(editorElement.editor, doc);
|
|
1801
|
+
return filterDisallowedAttachmentNodes(nodes, editorElement)
|
|
1802
|
+
}
|
|
1598
1803
|
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
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
|
+
}
|
|
1602
1813
|
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
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
|
+
}
|
|
1606
1821
|
|
|
1607
|
-
|
|
1608
|
-
|
|
1822
|
+
class HorizontalDividerNode extends DecoratorNode {
|
|
1823
|
+
static getType() {
|
|
1824
|
+
return "horizontal_divider"
|
|
1609
1825
|
}
|
|
1610
1826
|
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
return true
|
|
1827
|
+
static clone(node) {
|
|
1828
|
+
return new HorizontalDividerNode(node.__key)
|
|
1614
1829
|
}
|
|
1615
1830
|
|
|
1616
|
-
|
|
1617
|
-
return
|
|
1831
|
+
static importJSON(serializedNode) {
|
|
1832
|
+
return new HorizontalDividerNode()
|
|
1618
1833
|
}
|
|
1619
1834
|
|
|
1620
|
-
|
|
1621
|
-
return
|
|
1835
|
+
static importDOM() {
|
|
1836
|
+
return {
|
|
1837
|
+
"hr": (hr) => {
|
|
1838
|
+
return {
|
|
1839
|
+
conversion: () => ({
|
|
1840
|
+
node: new HorizontalDividerNode()
|
|
1841
|
+
}),
|
|
1842
|
+
priority: 1
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1622
1846
|
}
|
|
1623
1847
|
|
|
1624
|
-
|
|
1625
|
-
|
|
1848
|
+
constructor(key) {
|
|
1849
|
+
super(key);
|
|
1626
1850
|
}
|
|
1627
|
-
}
|
|
1628
1851
|
|
|
1629
|
-
|
|
1630
|
-
const
|
|
1631
|
-
const
|
|
1852
|
+
createDOM() {
|
|
1853
|
+
const figure = createElement("figure", { className: "horizontal-divider" });
|
|
1854
|
+
const hr = createElement("hr");
|
|
1632
1855
|
|
|
1633
|
-
|
|
1634
|
-
parse: (value) => value || false
|
|
1635
|
-
});
|
|
1856
|
+
figure.appendChild(hr);
|
|
1636
1857
|
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
// reads this map and re-applies the highlight styles. Scoped per editor instance
|
|
1640
|
-
// so entries don't leak across editors or outlive a torn-down editor.
|
|
1641
|
-
const pendingCodeHighlights = new WeakMap();
|
|
1858
|
+
const deleteButton = createElement("lexxy-node-delete-button");
|
|
1859
|
+
figure.appendChild(deleteButton);
|
|
1642
1860
|
|
|
1643
|
-
|
|
1644
|
-
get enabled() {
|
|
1645
|
-
return this.editorElement.supportsRichText
|
|
1861
|
+
return figure
|
|
1646
1862
|
}
|
|
1647
1863
|
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
dependencies: [ RichTextExtension ],
|
|
1651
|
-
name: "lexxy/highlight",
|
|
1652
|
-
config: {
|
|
1653
|
-
color: { buttons: [], permit: [] },
|
|
1654
|
-
"background-color": { buttons: [], permit: [] }
|
|
1655
|
-
},
|
|
1656
|
-
html: {
|
|
1657
|
-
import: {
|
|
1658
|
-
mark: $markConversion
|
|
1659
|
-
}
|
|
1660
|
-
},
|
|
1661
|
-
register(editor, config) {
|
|
1662
|
-
// keep the ref to the canonicalizers for optimized css conversion
|
|
1663
|
-
const canonicalizers = buildCanonicalizers(config);
|
|
1664
|
-
|
|
1665
|
-
// Register the <pre> converter directly in the conversion cache so it
|
|
1666
|
-
// coexists with other extensions' "pre" converters (the extension-level
|
|
1667
|
-
// html.import uses Object.assign, which means only one "pre" per key).
|
|
1668
|
-
$registerPreConversion(editor);
|
|
1669
|
-
|
|
1670
|
-
return mergeRegister(
|
|
1671
|
-
editor.registerCommand(TOGGLE_HIGHLIGHT_COMMAND, (styles) => $toggleSelectionStyles(editor, styles), COMMAND_PRIORITY_NORMAL),
|
|
1672
|
-
editor.registerCommand(REMOVE_HIGHLIGHT_COMMAND, () => $toggleSelectionStyles(editor, BLANK_STYLES), COMMAND_PRIORITY_NORMAL),
|
|
1673
|
-
editor.registerNodeTransform(TextNode, $syncHighlightWithStyle),
|
|
1674
|
-
editor.registerNodeTransform(CodeHighlightNode, $syncHighlightWithCodeHighlightNode),
|
|
1675
|
-
editor.registerNodeTransform(TextNode, (textNode) => $canonicalizePastedStyles(textNode, canonicalizers)),
|
|
1676
|
-
editor.registerMutationListener(CodeNode, (mutations) => {
|
|
1677
|
-
$applyPendingCodeHighlights(editor, mutations);
|
|
1678
|
-
}, { skipInitialization: true })
|
|
1679
|
-
)
|
|
1680
|
-
}
|
|
1681
|
-
});
|
|
1682
|
-
|
|
1683
|
-
return [ extension, this.editorConfig.get("highlight") ]
|
|
1864
|
+
updateDOM() {
|
|
1865
|
+
return true
|
|
1684
1866
|
}
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
function $applyHighlightStyle(textNode, element) {
|
|
1688
|
-
const elementStyles = {
|
|
1689
|
-
color: element.style?.color,
|
|
1690
|
-
"background-color": element.style?.backgroundColor
|
|
1691
|
-
};
|
|
1692
|
-
|
|
1693
|
-
if ($hasUpdateTag(PASTE_TAG)) { $setPastedStyles(textNode); }
|
|
1694
|
-
const highlightStyle = getCSSFromStyleObject(elementStyles);
|
|
1695
1867
|
|
|
1696
|
-
|
|
1697
|
-
return
|
|
1868
|
+
getTextContent() {
|
|
1869
|
+
return "┄\n\n"
|
|
1698
1870
|
}
|
|
1699
|
-
}
|
|
1700
1871
|
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
conversion: extendTextNodeConversion("mark", $applyHighlightStyle),
|
|
1704
|
-
priority: 1
|
|
1872
|
+
isInline() {
|
|
1873
|
+
return false
|
|
1705
1874
|
}
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
// Register a custom <pre> converter directly in the editor's HTML conversion
|
|
1709
|
-
// cache. We can't use the extension-level html.import because Object.assign
|
|
1710
|
-
// merges all extensions' converters by tag, and a later extension (e.g.
|
|
1711
|
-
// TrixContentExtension) would overwrite ours.
|
|
1712
|
-
function $registerPreConversion(editor) {
|
|
1713
|
-
if (!editor._htmlConversions) return
|
|
1714
1875
|
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
editor._htmlConversions.set("pre", preEntries);
|
|
1876
|
+
exportDOM() {
|
|
1877
|
+
const hr = createElement("hr");
|
|
1878
|
+
return { element: hr }
|
|
1719
1879
|
}
|
|
1720
|
-
preEntries.push($preConversionWithHighlightsFactory(editor));
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
// Returns a <pre> converter factory scoped to a specific editor instance.
|
|
1724
|
-
// The factory extracts highlight ranges from <mark> elements before the code
|
|
1725
|
-
// retokenizer can destroy them. The ranges are stored in pendingCodeHighlights
|
|
1726
|
-
// and applied after retokenization via a mutation listener.
|
|
1727
|
-
function $preConversionWithHighlightsFactory(editor) {
|
|
1728
|
-
return function $preConversionWithHighlights(domNode) {
|
|
1729
|
-
const highlights = extractHighlightRanges(domNode);
|
|
1730
|
-
if (highlights.length === 0) return null
|
|
1731
1880
|
|
|
1881
|
+
exportJSON() {
|
|
1732
1882
|
return {
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
const codeNode = $createCodeNode(language);
|
|
1736
|
-
$getPendingHighlights(editor).set(codeNode.getKey(), highlights);
|
|
1737
|
-
return { node: codeNode }
|
|
1738
|
-
},
|
|
1739
|
-
priority: 2
|
|
1883
|
+
type: "horizontal_divider",
|
|
1884
|
+
version: 1
|
|
1740
1885
|
}
|
|
1741
1886
|
}
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
// Walk the DOM tree inside a <pre> element and build a list of
|
|
1745
|
-
// { start, end, style } ranges for every <mark> element found.
|
|
1746
|
-
function extractHighlightRanges(preElement) {
|
|
1747
|
-
const ranges = [];
|
|
1748
|
-
const codeElement = preElement.querySelector("code") || preElement;
|
|
1749
|
-
|
|
1750
|
-
let offset = 0;
|
|
1751
|
-
|
|
1752
|
-
function walk(node) {
|
|
1753
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
1754
|
-
offset += node.textContent.length;
|
|
1755
|
-
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
1756
|
-
// <br> maps to a LineBreakNode (1 character) in Lexical
|
|
1757
|
-
if (node.tagName === "BR") {
|
|
1758
|
-
offset += 1;
|
|
1759
|
-
return
|
|
1760
|
-
}
|
|
1761
1887
|
|
|
1762
|
-
|
|
1763
|
-
|
|
1888
|
+
decorate() {
|
|
1889
|
+
return null
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1764
1892
|
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
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);
|
|
1768
1902
|
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
}
|
|
1774
|
-
}
|
|
1903
|
+
if (!isImport) {
|
|
1904
|
+
const paragraph = $createParagraphNode();
|
|
1905
|
+
hrNode.insertAfter(paragraph);
|
|
1906
|
+
paragraph.select();
|
|
1775
1907
|
}
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
walk(child);
|
|
1780
|
-
}
|
|
1908
|
+
},
|
|
1909
|
+
type: "multiline-element"
|
|
1910
|
+
};
|
|
1781
1911
|
|
|
1782
|
-
|
|
1783
|
-
}
|
|
1912
|
+
const PUNCTUATION_OR_SPACE = /[^\w]/;
|
|
1784
1913
|
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
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
|
|
1793
1931
|
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
if (element.style?.backgroundColor) styles["background-color"] = element.style.backgroundColor;
|
|
1798
|
-
const css = getCSSFromStyleObject(styles);
|
|
1799
|
-
return css.length > 0 ? css : null
|
|
1800
|
-
}
|
|
1932
|
+
return editor.registerUpdateListener(({ tags, dirtyLeaves, editorState, prevEditorState }) => {
|
|
1933
|
+
if (tags.has("historic") || tags.has("collaboration")) return
|
|
1934
|
+
if (editor.isComposing()) return
|
|
1801
1935
|
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
function $applyPendingCodeHighlights(editor, mutations) {
|
|
1805
|
-
const pending = $getPendingHighlights(editor);
|
|
1806
|
-
const keysToProcess = [];
|
|
1936
|
+
const selection = editorState.read($getSelection);
|
|
1937
|
+
const prevSelection = prevEditorState.read($getSelection);
|
|
1807
1938
|
|
|
1808
|
-
|
|
1809
|
-
if (type !== "destroyed" && pending.has(key)) {
|
|
1810
|
-
keysToProcess.push(key);
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1939
|
+
if (!$isRangeSelection(prevSelection) || !$isRangeSelection(selection) || !selection.isCollapsed()) return
|
|
1813
1940
|
|
|
1814
|
-
|
|
1941
|
+
const anchorKey = selection.anchor.key;
|
|
1942
|
+
const anchorOffset = selection.anchor.offset;
|
|
1815
1943
|
|
|
1816
|
-
|
|
1817
|
-
// skipTransforms update before we touch the nodes.
|
|
1818
|
-
editor.update(() => {
|
|
1819
|
-
for (const key of keysToProcess) {
|
|
1820
|
-
const highlights = pending.get(key);
|
|
1821
|
-
pending.delete(key);
|
|
1822
|
-
if (!highlights) continue
|
|
1944
|
+
if (!dirtyLeaves.has(anchorKey)) return
|
|
1823
1945
|
|
|
1824
|
-
|
|
1825
|
-
|
|
1946
|
+
const anchorNode = editorState.read(() => $getNodeByKey(anchorKey));
|
|
1947
|
+
if (!$isTextNode(anchorNode)) return
|
|
1826
1948
|
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
}
|
|
1949
|
+
// Only trigger when cursor moved forward (typing)
|
|
1950
|
+
const prevOffset = prevSelection.anchor.key === anchorKey ? prevSelection.anchor.offset : 0;
|
|
1951
|
+
if (anchorOffset <= prevOffset) return
|
|
1831
1952
|
|
|
1832
|
-
|
|
1833
|
-
// of a CodeNode, splitting nodes at range boundaries as needed.
|
|
1834
|
-
// We can't use TextNode.splitText() because it creates TextNode
|
|
1835
|
-
// instances (not CodeHighlightNodes) for the split parts. Instead,
|
|
1836
|
-
// we manually create CodeHighlightNode replacements.
|
|
1837
|
-
function $applyHighlightRangesToCodeNode(codeNode, highlights) {
|
|
1838
|
-
if (highlights.length === 0) return
|
|
1953
|
+
const textContent = editorState.read(() => anchorNode.getTextContent());
|
|
1839
1954
|
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1955
|
+
// Try each transformer, longest tags first
|
|
1956
|
+
for (const transformer of textFormatTransformers) {
|
|
1957
|
+
const tag = transformer.tag;
|
|
1958
|
+
const tagLen = tag.length;
|
|
1844
1959
|
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
// TextNode children back to CodeHighlightNodes before this runs,
|
|
1849
|
-
// but the iteration over $buildChildRanges has to keep counting
|
|
1850
|
-
// them so character offsets stay aligned with the saved ranges.
|
|
1851
|
-
if (!$isCodeHighlightNode(node)) continue
|
|
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
|
|
1852
1963
|
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
const overlapEnd = Math.min(hlEnd, nodeEnd);
|
|
1964
|
+
const candidateOpenTag = textContent.slice(openTagStart, anchorOffset);
|
|
1965
|
+
if (candidateOpenTag !== tag) continue
|
|
1856
1966
|
|
|
1857
|
-
if
|
|
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
|
|
1858
1972
|
|
|
1859
|
-
//
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
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
|
|
1978
|
+
}
|
|
1863
1979
|
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
} else {
|
|
1869
|
-
// Need to split: replace the node with 2 or 3 CodeHighlightNodes
|
|
1870
|
-
const text = node.getTextContent();
|
|
1871
|
-
const highlightType = node.getHighlightType();
|
|
1872
|
-
const replacements = [];
|
|
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
|
|
1873
1984
|
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
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
|
|
1877
1989
|
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
replacements.push(styledNode);
|
|
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
|
|
1882
1993
|
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
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
|
|
1886
1998
|
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1999
|
+
// No space immediately after opening tag
|
|
2000
|
+
if (textContent[innerStart] === " ") continue
|
|
2001
|
+
|
|
2002
|
+
// No space immediately before closing tag
|
|
2003
|
+
if (textContent[innerEnd - 1] === " ") continue
|
|
2004
|
+
|
|
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
|
|
1891
2009
|
}
|
|
1892
|
-
}
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
2010
|
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
2011
|
+
editor.update(() => {
|
|
2012
|
+
const node = $getNodeByKey(anchorKey);
|
|
2013
|
+
if (!node || !$isTextNode(node)) return
|
|
1899
2014
|
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
const text = child.getTextContent();
|
|
1903
|
-
childRanges.push({ node: child, start: charOffset, end: charOffset + text.length });
|
|
1904
|
-
charOffset += text.length;
|
|
1905
|
-
} else {
|
|
1906
|
-
// LineBreakNode, TabNode - count as 1 character each (\n, \t)
|
|
1907
|
-
charOffset += 1;
|
|
1908
|
-
}
|
|
1909
|
-
}
|
|
2015
|
+
const parent = node.getParent();
|
|
2016
|
+
if (parent === null || $isCodeNode(parent)) return
|
|
1910
2017
|
|
|
1911
|
-
|
|
2018
|
+
$applyFormatFromLeadingTag(node, openTagStart, transformer);
|
|
2019
|
+
});
|
|
2020
|
+
|
|
2021
|
+
break // Only apply the first (longest) matching transformer
|
|
2022
|
+
}
|
|
2023
|
+
})
|
|
1912
2024
|
}
|
|
1913
2025
|
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
const ranges = [];
|
|
1919
|
-
const childRanges = $buildChildRanges(codeNode);
|
|
2026
|
+
function $applyFormatFromLeadingTag(anchorNode, openTagStart, transformer) {
|
|
2027
|
+
const tag = transformer.tag;
|
|
2028
|
+
const tagLen = tag.length;
|
|
2029
|
+
const textContent = anchorNode.getTextContent();
|
|
1920
2030
|
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
ranges.push({ start, end, style });
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
2031
|
+
const innerStart = openTagStart + tagLen;
|
|
2032
|
+
const closeTagIndex = textContent.indexOf(tag, innerStart);
|
|
2033
|
+
if (closeTagIndex < 0) return
|
|
1927
2034
|
|
|
1928
|
-
|
|
1929
|
-
|
|
2035
|
+
const inner = textContent.slice(innerStart, closeTagIndex);
|
|
2036
|
+
if (inner.length === 0) return
|
|
1930
2037
|
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
new StyleCanonicalizer("background-color", [ ...config.buttons["background-color"], ...config.permit["background-color"] ])
|
|
1935
|
-
]
|
|
1936
|
-
}
|
|
2038
|
+
// Remove both tags and apply format
|
|
2039
|
+
const before = textContent.slice(0, openTagStart);
|
|
2040
|
+
const after = textContent.slice(closeTagIndex + tagLen);
|
|
1937
2041
|
|
|
1938
|
-
|
|
1939
|
-
const selection = $getSelection();
|
|
1940
|
-
if (!$isRangeSelection(selection)) return
|
|
2042
|
+
anchorNode.setTextContent(before + inner + after);
|
|
1941
2043
|
|
|
1942
|
-
const
|
|
1943
|
-
|
|
1944
|
-
const oldValue = $getSelectionStyleValueForProperty(selection, property);
|
|
1945
|
-
patch[property] = toggleOrReplace(oldValue, styles[property]);
|
|
1946
|
-
}
|
|
2044
|
+
const nextSelection = $createRangeSelection();
|
|
2045
|
+
$setSelection(nextSelection);
|
|
1947
2046
|
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
$patchStyleText(selection, patch);
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
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");
|
|
1954
2050
|
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
// A text node inside a code block may be either a CodeHighlightNode
|
|
1959
|
-
// (after retokenization) or a plain TextNode (after splitText or before
|
|
1960
|
-
// the retokenizer has run). Check the parent in both cases.
|
|
1961
|
-
if ($isCodeHighlightNode(node) || $isTextNode(node)) {
|
|
1962
|
-
return $isCodeNode(node.getParent())
|
|
2051
|
+
for (const format of transformer.format) {
|
|
2052
|
+
if (!nextSelection.hasFormat(format)) {
|
|
2053
|
+
nextSelection.formatText(format);
|
|
1963
2054
|
}
|
|
1964
|
-
|
|
1965
|
-
})
|
|
1966
|
-
}
|
|
2055
|
+
}
|
|
1967
2056
|
|
|
1968
|
-
|
|
1969
|
-
//
|
|
1970
|
-
|
|
1971
|
-
// because splitText creates TextNode instances and the retokenizer
|
|
1972
|
-
// may not have converted them back to CodeHighlightNodes yet.
|
|
1973
|
-
const nodeKeys = selection.getNodes()
|
|
1974
|
-
.filter((node) => ($isCodeHighlightNode(node) || $isTextNode(node)) && $isCodeNode(node.getParent()))
|
|
1975
|
-
.map((node) => ({
|
|
1976
|
-
key: node.getKey(),
|
|
1977
|
-
startOffset: $getNodeSelectionOffsets(node, selection)[0],
|
|
1978
|
-
endOffset: $getNodeSelectionOffsets(node, selection)[1],
|
|
1979
|
-
textSize: node.getTextContentSize()
|
|
1980
|
-
}));
|
|
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);
|
|
1981
2060
|
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
const affectedCodeNodes = new Set();
|
|
2061
|
+
for (const format of transformer.format) {
|
|
2062
|
+
if (nextSelection.hasFormat(format)) {
|
|
2063
|
+
nextSelection.toggleFormat(format);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
1989
2067
|
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
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",
|
|
2138
|
+
}
|
|
2139
|
+
};
|
|
1993
2140
|
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
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.
|
|
1997
2152
|
|
|
1998
|
-
|
|
2153
|
+
let resolverRoot = null;
|
|
1999
2154
|
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
} else {
|
|
2003
|
-
const splitNodes = node.splitText(startOffset, endOffset);
|
|
2004
|
-
const targetNode = splitNodes[startOffset === 0 ? 0 : 1];
|
|
2005
|
-
$applyStylePatchToNode(targetNode, patch);
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2155
|
+
function styleResolverRoot() {
|
|
2156
|
+
if (resolverRoot && resolverRoot.isConnected) return resolverRoot
|
|
2008
2157
|
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
}, { skipTransforms: true, discrete: true });
|
|
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
|
|
2020
2168
|
}
|
|
2021
2169
|
|
|
2022
|
-
function
|
|
2023
|
-
|
|
2024
|
-
const anchorKey = selection.anchor.key;
|
|
2025
|
-
const focusKey = selection.focus.key;
|
|
2026
|
-
const textSize = node.getTextContentSize();
|
|
2027
|
-
|
|
2028
|
-
const isAnchor = nodeKey === anchorKey;
|
|
2029
|
-
const isFocus = nodeKey === focusKey;
|
|
2030
|
-
|
|
2031
|
-
// Determine if selection is forward or backward
|
|
2032
|
-
const isForward = selection.isBackward() === false;
|
|
2033
|
-
|
|
2034
|
-
let start = 0;
|
|
2035
|
-
let end = textSize;
|
|
2170
|
+
function isSelectionHighlighted(selection) {
|
|
2171
|
+
if (!$isRangeSelection(selection)) return false
|
|
2036
2172
|
|
|
2037
|
-
if (
|
|
2038
|
-
|
|
2039
|
-
if (isFocus) end = selection.focus.offset;
|
|
2173
|
+
if (selection.isCollapsed()) {
|
|
2174
|
+
return hasHighlightStyles(selection.style)
|
|
2040
2175
|
} else {
|
|
2041
|
-
|
|
2042
|
-
if (isAnchor) end = selection.anchor.offset;
|
|
2176
|
+
return selection.hasFormat("highlight")
|
|
2043
2177
|
}
|
|
2044
|
-
|
|
2045
|
-
return [ start, end ]
|
|
2046
2178
|
}
|
|
2047
2179
|
|
|
2048
|
-
function
|
|
2049
|
-
|
|
2050
|
-
const newStyles = { ...prevStyles };
|
|
2180
|
+
function getHighlightStyles(selection) {
|
|
2181
|
+
if (!$isRangeSelection(selection)) return null
|
|
2051
2182
|
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
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());
|
|
2057
2188
|
}
|
|
2058
2189
|
}
|
|
2059
2190
|
|
|
2060
|
-
const
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
// Sync the highlight format using TextNode's setFormat to bypass
|
|
2064
|
-
// CodeHighlightNode's no-op override
|
|
2065
|
-
const shouldHaveHighlight = hasHighlightStyles(newCSSText);
|
|
2066
|
-
const hasHighlight = node.hasFormat("highlight");
|
|
2191
|
+
const color = styles.color || null;
|
|
2192
|
+
const backgroundColor = styles["background-color"] || null;
|
|
2193
|
+
if (!color && !backgroundColor) return null
|
|
2067
2194
|
|
|
2068
|
-
|
|
2069
|
-
$setCodeHighlightFormat(node, shouldHaveHighlight);
|
|
2070
|
-
}
|
|
2195
|
+
return { color, backgroundColor }
|
|
2071
2196
|
}
|
|
2072
2197
|
|
|
2073
|
-
function
|
|
2074
|
-
const
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
if (shouldHaveHighlight) {
|
|
2078
|
-
writable.__format |= IS_HIGHLIGHT;
|
|
2079
|
-
} else {
|
|
2080
|
-
writable.__format &= ~IS_HIGHLIGHT;
|
|
2081
|
-
}
|
|
2198
|
+
function hasHighlightStyles(cssOrStyles) {
|
|
2199
|
+
const styles = typeof cssOrStyles === "string" ? getStyleObjectFromCSS(cssOrStyles) : cssOrStyles;
|
|
2200
|
+
return !!(styles.color || styles["background-color"])
|
|
2082
2201
|
}
|
|
2083
2202
|
|
|
2084
|
-
function
|
|
2085
|
-
return
|
|
2203
|
+
function applyCanonicalizers(styles, canonicalizers = []) {
|
|
2204
|
+
return canonicalizers.reduce((css, canonicalizer) => {
|
|
2205
|
+
return canonicalizer.applyCanonicalization(css)
|
|
2206
|
+
}, styles)
|
|
2086
2207
|
}
|
|
2087
2208
|
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2209
|
+
class StyleCanonicalizer {
|
|
2210
|
+
constructor(property, allowedValues= []) {
|
|
2211
|
+
this._property = property;
|
|
2212
|
+
this._allowedValues = allowedValues;
|
|
2213
|
+
this._canonicalValues = this.#allowedValuesIdentityObject;
|
|
2091
2214
|
}
|
|
2092
|
-
}
|
|
2093
2215
|
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
if (!$isCodeNode(parent)) return
|
|
2216
|
+
applyCanonicalization(css) {
|
|
2217
|
+
const styles = { ...getStyleObjectFromCSS(css) };
|
|
2097
2218
|
|
|
2098
|
-
|
|
2099
|
-
|
|
2219
|
+
styles[this._property] = this.getCanonicalAllowedValue(styles[this._property]);
|
|
2220
|
+
if (!styles[this._property]) {
|
|
2221
|
+
delete styles[this._property];
|
|
2222
|
+
}
|
|
2100
2223
|
|
|
2101
|
-
|
|
2102
|
-
$setCodeHighlightFormat(node, shouldHaveHighlight);
|
|
2224
|
+
return getCSSFromStyleObject(styles)
|
|
2103
2225
|
}
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
function $canonicalizePastedStyles(textNode, canonicalizers = []) {
|
|
2107
|
-
if ($hasPastedStyles(textNode)) {
|
|
2108
|
-
$setPastedStyles(textNode, false);
|
|
2109
|
-
|
|
2110
|
-
const canonicalizedCSS = applyCanonicalizers(textNode.getStyle(), canonicalizers);
|
|
2111
|
-
textNode.setStyle(canonicalizedCSS);
|
|
2112
2226
|
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
selection.setStyle(textNode.getStyle());
|
|
2116
|
-
selection.setFormat(textNode.getFormat());
|
|
2117
|
-
}
|
|
2227
|
+
getCanonicalAllowedValue(value) {
|
|
2228
|
+
return this._canonicalValues[value] ||= this.#resolveCannonicalValue(value)
|
|
2118
2229
|
}
|
|
2119
|
-
}
|
|
2120
|
-
|
|
2121
|
-
function $setPastedStyles(textNode, value = true) {
|
|
2122
|
-
$setState(textNode, hasPastedStylesState, value);
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
|
-
function $hasPastedStyles(textNode) {
|
|
2126
|
-
return $getState(textNode, hasPastedStylesState)
|
|
2127
|
-
}
|
|
2128
|
-
|
|
2129
|
-
const COMMANDS = [
|
|
2130
|
-
"bold",
|
|
2131
|
-
"italic",
|
|
2132
|
-
"strikethrough",
|
|
2133
|
-
"underline",
|
|
2134
|
-
"link",
|
|
2135
|
-
"unlink",
|
|
2136
|
-
"toggleHighlight",
|
|
2137
|
-
"removeHighlight",
|
|
2138
|
-
"setFormatHeadingLarge",
|
|
2139
|
-
"setFormatHeadingMedium",
|
|
2140
|
-
"setFormatHeadingSmall",
|
|
2141
|
-
"setFormatParagraph",
|
|
2142
|
-
"clearFormatting",
|
|
2143
|
-
"insertUnorderedList",
|
|
2144
|
-
"insertOrderedList",
|
|
2145
|
-
"insertQuoteBlock",
|
|
2146
|
-
"insertCodeBlock",
|
|
2147
|
-
"setCodeLanguage",
|
|
2148
|
-
"insertHorizontalDivider",
|
|
2149
|
-
"uploadImage",
|
|
2150
|
-
"uploadFile",
|
|
2151
2230
|
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
"undo",
|
|
2155
|
-
"redo"
|
|
2156
|
-
];
|
|
2157
|
-
|
|
2158
|
-
class CommandDispatcher {
|
|
2159
|
-
#selectionBeforeDrag = null
|
|
2160
|
-
#listeners = new ListenerBin()
|
|
2231
|
+
// Private
|
|
2161
2232
|
|
|
2162
|
-
|
|
2163
|
-
return
|
|
2233
|
+
get #allowedValuesIdentityObject() {
|
|
2234
|
+
return this._allowedValues.reduce((object, value) => ({ ...object, [value]: value }), {})
|
|
2164
2235
|
}
|
|
2165
2236
|
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
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]
|
|
2243
|
+
}
|
|
2172
2244
|
|
|
2173
|
-
|
|
2174
|
-
this
|
|
2175
|
-
this.#registerDragAndDropHandlers();
|
|
2245
|
+
get #computedAllowedValues() {
|
|
2246
|
+
return this._computedAllowedValues ||= computeStyleValues(this._property, this._allowedValues)
|
|
2176
2247
|
}
|
|
2248
|
+
}
|
|
2249
|
+
|
|
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();
|
|
2177
2257
|
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2258
|
+
const elements = values.map(value => {
|
|
2259
|
+
const element = createElement("span", { style: `display: none; ${property}: ${value};` });
|
|
2260
|
+
fragment.appendChild(element);
|
|
2261
|
+
return element
|
|
2262
|
+
});
|
|
2181
2263
|
|
|
2182
|
-
|
|
2183
|
-
this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold");
|
|
2184
|
-
}
|
|
2264
|
+
styleResolverRoot().appendChild(fragment);
|
|
2185
2265
|
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2266
|
+
const computed = elements.map(element =>
|
|
2267
|
+
window.getComputedStyle(element).getPropertyValue(property)
|
|
2268
|
+
);
|
|
2189
2269
|
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2270
|
+
elements.forEach(element => element.remove());
|
|
2271
|
+
return computed
|
|
2272
|
+
}
|
|
2193
2273
|
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2274
|
+
const TOGGLE_HIGHLIGHT_COMMAND = createCommand();
|
|
2275
|
+
const REMOVE_HIGHLIGHT_COMMAND = createCommand();
|
|
2276
|
+
const BLANK_STYLES = { "color": null, "background-color": null };
|
|
2197
2277
|
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2278
|
+
const hasPastedStylesState = createState("hasPastedStyles", {
|
|
2279
|
+
parse: (value) => value || false
|
|
2280
|
+
});
|
|
2201
2281
|
|
|
2202
|
-
|
|
2203
|
-
|
|
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();
|
|
2287
|
+
|
|
2288
|
+
class HighlightExtension extends LexxyExtension {
|
|
2289
|
+
get enabled() {
|
|
2290
|
+
return this.editorElement.supportsRichText
|
|
2204
2291
|
}
|
|
2205
2292
|
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
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);
|
|
2210
2309
|
|
|
2211
|
-
|
|
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);
|
|
2212
2314
|
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
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
|
+
)
|
|
2220
2325
|
}
|
|
2221
2326
|
});
|
|
2222
|
-
}
|
|
2223
|
-
|
|
2224
|
-
dispatchUnlink() {
|
|
2225
|
-
this.editor.update(() => {
|
|
2226
|
-
// Let adapters signal whether unlink should target a frozen link key.
|
|
2227
|
-
if (this.editorElement.adapter.unlinkFrozenNode?.()) {
|
|
2228
|
-
return
|
|
2229
|
-
}
|
|
2230
2327
|
|
|
2231
|
-
|
|
2232
|
-
});
|
|
2328
|
+
return [ extension, this.editorConfig.get("highlight") ]
|
|
2233
2329
|
}
|
|
2330
|
+
}
|
|
2234
2331
|
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2332
|
+
function $applyHighlightStyle(textNode, element) {
|
|
2333
|
+
const elementStyles = {
|
|
2334
|
+
color: element.style?.color,
|
|
2335
|
+
"background-color": element.style?.backgroundColor
|
|
2336
|
+
};
|
|
2238
2337
|
|
|
2239
|
-
|
|
2338
|
+
if ($hasUpdateTag(PASTE_TAG)) { $setPastedStyles(textNode); }
|
|
2339
|
+
const highlightStyle = getCSSFromStyleObject(elementStyles);
|
|
2240
2340
|
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
} else {
|
|
2244
|
-
this.contents.applyUnorderedListFormat();
|
|
2245
|
-
}
|
|
2341
|
+
if (highlightStyle.length) {
|
|
2342
|
+
return textNode.setStyle(textNode.getStyle() + highlightStyle)
|
|
2246
2343
|
}
|
|
2344
|
+
}
|
|
2247
2345
|
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
const anchorNode = selection.anchor.getNode();
|
|
2253
|
-
|
|
2254
|
-
if (this.selection.isInsideList && anchorNode && getListType(anchorNode) === "number") {
|
|
2255
|
-
this.contents.applyParagraphFormat();
|
|
2256
|
-
} else {
|
|
2257
|
-
this.contents.applyOrderedListFormat();
|
|
2258
|
-
}
|
|
2346
|
+
function $markConversion() {
|
|
2347
|
+
return {
|
|
2348
|
+
conversion: extendTextNodeConversion("mark", $applyHighlightStyle),
|
|
2349
|
+
priority: 1
|
|
2259
2350
|
}
|
|
2351
|
+
}
|
|
2260
2352
|
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
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
|
|
2264
2359
|
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
this.contents.toggleCodeBlock();
|
|
2270
|
-
}
|
|
2360
|
+
let preEntries = editor._htmlConversions.get("pre");
|
|
2361
|
+
if (!preEntries) {
|
|
2362
|
+
preEntries = [];
|
|
2363
|
+
editor._htmlConversions.set("pre", preEntries);
|
|
2271
2364
|
}
|
|
2365
|
+
preEntries.push($preConversionWithHighlightsFactory(editor));
|
|
2366
|
+
}
|
|
2272
2367
|
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
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
|
|
2280
2376
|
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
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
|
|
2284
2385
|
}
|
|
2285
|
-
|
|
2286
|
-
this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code");
|
|
2287
2386
|
}
|
|
2387
|
+
}
|
|
2288
2388
|
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
const startPoint = isBackward ? selection.focus : selection.anchor;
|
|
2295
|
-
const endPoint = isBackward ? selection.anchor : selection.focus;
|
|
2296
|
-
|
|
2297
|
-
for (let i = 0; i < textNodes.length; i++) {
|
|
2298
|
-
const node = textNodes[i];
|
|
2299
|
-
if (node.getFormat() === 0) continue
|
|
2300
|
-
|
|
2301
|
-
const isFirst = i === 0;
|
|
2302
|
-
const isLast = i === textNodes.length - 1;
|
|
2303
|
-
const startOffset = isFirst && startPoint.type === "text" ? startPoint.offset : 0;
|
|
2304
|
-
const endOffset = isLast && endPoint.type === "text" ? endPoint.offset : node.getTextContentSize();
|
|
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;
|
|
2305
2394
|
|
|
2306
|
-
|
|
2307
|
-
node.setFormat(0);
|
|
2308
|
-
} else {
|
|
2309
|
-
const splits = node.splitText(startOffset, endOffset);
|
|
2310
|
-
const target = startOffset === 0 ? splits[0] : splits[1];
|
|
2311
|
-
target.setFormat(0);
|
|
2395
|
+
let offset = 0;
|
|
2312
2396
|
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
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
|
|
2319
2405
|
}
|
|
2320
|
-
}
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
|
-
dispatchSetCodeLanguage(language) {
|
|
2324
|
-
this.editor.update(() => {
|
|
2325
|
-
if (!this.selection.isInsideCodeBlock) return
|
|
2326
|
-
|
|
2327
|
-
const codeNode = this.selection.nearestNodeOfType(CodeNode);
|
|
2328
|
-
if (!codeNode) return
|
|
2329
2406
|
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
}
|
|
2407
|
+
const isMark = node.tagName === "MARK";
|
|
2408
|
+
const start = offset;
|
|
2333
2409
|
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
}
|
|
2410
|
+
for (const child of node.childNodes) {
|
|
2411
|
+
walk(child);
|
|
2412
|
+
}
|
|
2338
2413
|
|
|
2339
|
-
|
|
2340
|
-
|
|
2414
|
+
if (isMark) {
|
|
2415
|
+
const style = extractHighlightStyleFromElement(node);
|
|
2416
|
+
if (style) {
|
|
2417
|
+
ranges.push({ start, end: offset, style });
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2341
2421
|
}
|
|
2342
2422
|
|
|
2343
|
-
|
|
2344
|
-
|
|
2423
|
+
for (const child of codeElement.childNodes) {
|
|
2424
|
+
walk(child);
|
|
2345
2425
|
}
|
|
2346
2426
|
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
}
|
|
2427
|
+
return ranges
|
|
2428
|
+
}
|
|
2350
2429
|
|
|
2351
|
-
|
|
2352
|
-
|
|
2430
|
+
function $getPendingHighlights(editor) {
|
|
2431
|
+
let map = pendingCodeHighlights.get(editor);
|
|
2432
|
+
if (!map) {
|
|
2433
|
+
map = new Map();
|
|
2434
|
+
pendingCodeHighlights.set(editor, map);
|
|
2353
2435
|
}
|
|
2436
|
+
return map
|
|
2437
|
+
}
|
|
2354
2438
|
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
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
|
+
}
|
|
2358
2446
|
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
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 = [];
|
|
2362
2452
|
|
|
2363
|
-
|
|
2364
|
-
|
|
2453
|
+
for (const [ key, type ] of mutations) {
|
|
2454
|
+
if (type !== "destroyed" && pending.has(key)) {
|
|
2455
|
+
keysToProcess.push(key);
|
|
2456
|
+
}
|
|
2365
2457
|
}
|
|
2366
2458
|
|
|
2367
|
-
|
|
2368
|
-
const attributes = {
|
|
2369
|
-
type: "file",
|
|
2370
|
-
multiple: true,
|
|
2371
|
-
style: "display: none;",
|
|
2372
|
-
onchange: ({ target: { files } }) => {
|
|
2373
|
-
this.contents.uploadFiles(files, { selectLast: true });
|
|
2374
|
-
}
|
|
2375
|
-
};
|
|
2376
|
-
|
|
2377
|
-
if (accept) attributes.accept = accept;
|
|
2459
|
+
if (keysToProcess.length === 0) return
|
|
2378
2460
|
|
|
2379
|
-
|
|
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
|
|
2380
2468
|
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
input.click();
|
|
2384
|
-
setTimeout(() => input.remove(), 1000);
|
|
2385
|
-
}
|
|
2469
|
+
const codeNode = $getNodeByKey(key);
|
|
2470
|
+
if (!codeNode || !$isCodeNode(codeNode)) continue
|
|
2386
2471
|
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
}
|
|
2472
|
+
$applyHighlightRangesToCodeNode(codeNode, highlights);
|
|
2473
|
+
}
|
|
2474
|
+
}, { skipTransforms: true, discrete: true });
|
|
2475
|
+
}
|
|
2390
2476
|
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
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
|
|
2394
2484
|
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
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);
|
|
2398
2489
|
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
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
|
|
2402
2497
|
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
const
|
|
2406
|
-
this.#registerCommandHandler(command, 0, this[methodName].bind(this));
|
|
2407
|
-
}
|
|
2498
|
+
// Check if this child overlaps with the highlight range
|
|
2499
|
+
const overlapStart = Math.max(hlStart, nodeStart);
|
|
2500
|
+
const overlapEnd = Math.min(hlEnd, nodeEnd);
|
|
2408
2501
|
|
|
2409
|
-
|
|
2410
|
-
}
|
|
2502
|
+
if (overlapStart >= overlapEnd) continue
|
|
2411
2503
|
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2504
|
+
// Calculate offsets relative to this node
|
|
2505
|
+
const relStart = overlapStart - nodeStart;
|
|
2506
|
+
const relEnd = overlapEnd - nodeStart;
|
|
2507
|
+
const nodeLength = nodeEnd - nodeStart;
|
|
2415
2508
|
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
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 = [];
|
|
2420
2518
|
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
if (this.selection.isInsideCodeBlock || !selection.hasFormat("code")) return false
|
|
2519
|
+
if (relStart > 0) {
|
|
2520
|
+
replacements.push($createCodeHighlightNode(text.slice(0, relStart), highlightType));
|
|
2521
|
+
}
|
|
2425
2522
|
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2523
|
+
const styledNode = $createCodeHighlightNode(text.slice(relStart, relEnd), highlightType);
|
|
2524
|
+
styledNode.setStyle(style);
|
|
2525
|
+
$setCodeHighlightFormat(styledNode, true);
|
|
2526
|
+
replacements.push(styledNode);
|
|
2429
2527
|
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
}
|
|
2528
|
+
if (relEnd < nodeLength) {
|
|
2529
|
+
replacements.push($createCodeHighlightNode(text.slice(relEnd), highlightType));
|
|
2530
|
+
}
|
|
2434
2531
|
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
registerEventListener(root, "dragover", this.#handleDragOver.bind(this)),
|
|
2441
|
-
registerEventListener(root, "drop", this.#handleDrop.bind(this)),
|
|
2442
|
-
registerEventListener(root, "dragenter", this.#handleDragEnter.bind(this)),
|
|
2443
|
-
registerEventListener(root, "dragleave", this.#handleDragLeave.bind(this))
|
|
2444
|
-
);
|
|
2532
|
+
for (const replacement of replacements) {
|
|
2533
|
+
node.insertBefore(replacement);
|
|
2534
|
+
}
|
|
2535
|
+
node.remove();
|
|
2536
|
+
}
|
|
2445
2537
|
}
|
|
2446
2538
|
}
|
|
2539
|
+
}
|
|
2447
2540
|
|
|
2448
|
-
|
|
2449
|
-
|
|
2541
|
+
function $buildChildRanges(codeNode) {
|
|
2542
|
+
const childRanges = [];
|
|
2543
|
+
let charOffset = 0;
|
|
2450
2544
|
|
|
2451
|
-
|
|
2452
|
-
if (
|
|
2453
|
-
|
|
2454
|
-
|
|
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;
|
|
2455
2553
|
}
|
|
2456
2554
|
}
|
|
2457
2555
|
|
|
2458
|
-
|
|
2459
|
-
|
|
2556
|
+
return childRanges
|
|
2557
|
+
}
|
|
2460
2558
|
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
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 });
|
|
2465
2570
|
}
|
|
2466
2571
|
}
|
|
2467
2572
|
|
|
2468
|
-
|
|
2469
|
-
|
|
2573
|
+
return ranges
|
|
2574
|
+
}
|
|
2470
2575
|
|
|
2471
|
-
|
|
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
|
+
}
|
|
2582
|
+
|
|
2583
|
+
function $toggleSelectionStyles(editor, styles) {
|
|
2584
|
+
const selection = $getSelection();
|
|
2585
|
+
if (!$isRangeSelection(selection)) return
|
|
2586
|
+
|
|
2587
|
+
const patch = {};
|
|
2588
|
+
for (const property in styles) {
|
|
2589
|
+
const oldValue = $getSelectionStyleValueForProperty(selection, property);
|
|
2590
|
+
patch[property] = toggleOrReplace(oldValue, styles[property]);
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
if ($selectionIsInCodeBlock(selection)) {
|
|
2594
|
+
$patchCodeHighlightStyles(editor, selection, patch);
|
|
2595
|
+
} else {
|
|
2596
|
+
$patchStyleText(selection, patch);
|
|
2472
2597
|
}
|
|
2598
|
+
}
|
|
2599
|
+
|
|
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
|
+
}
|
|
2612
|
+
|
|
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
|
+
}));
|
|
2473
2626
|
|
|
2474
|
-
|
|
2475
|
-
|
|
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();
|
|
2476
2634
|
|
|
2477
|
-
|
|
2635
|
+
for (const { key, startOffset, endOffset, textSize } of nodeKeys) {
|
|
2636
|
+
const node = $getNodeByKey(key);
|
|
2637
|
+
if (!node) continue
|
|
2478
2638
|
|
|
2479
|
-
|
|
2480
|
-
|
|
2639
|
+
const parent = node.getParent();
|
|
2640
|
+
if (!$isCodeNode(parent)) continue
|
|
2641
|
+
if (startOffset === endOffset) continue
|
|
2481
2642
|
|
|
2482
|
-
|
|
2483
|
-
if (!dataTransfer) return
|
|
2643
|
+
affectedCodeNodes.add(parent);
|
|
2484
2644
|
|
|
2485
|
-
|
|
2486
|
-
|
|
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
|
+
}
|
|
2487
2653
|
|
|
2488
|
-
|
|
2489
|
-
|
|
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
|
+
}
|
|
2490
2666
|
|
|
2491
|
-
|
|
2492
|
-
|
|
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();
|
|
2493
2672
|
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
this.#selectionBeforeDrag = $getSelection()?.clone();
|
|
2497
|
-
});
|
|
2498
|
-
}
|
|
2673
|
+
const isAnchor = nodeKey === anchorKey;
|
|
2674
|
+
const isFocus = nodeKey === focusKey;
|
|
2499
2675
|
|
|
2500
|
-
|
|
2501
|
-
|
|
2676
|
+
// Determine if selection is forward or backward
|
|
2677
|
+
const isForward = selection.isBackward() === false;
|
|
2502
2678
|
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
});
|
|
2679
|
+
let start = 0;
|
|
2680
|
+
let end = textSize;
|
|
2506
2681
|
|
|
2507
|
-
|
|
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;
|
|
2508
2688
|
}
|
|
2509
2689
|
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
}
|
|
2690
|
+
return [ start, end ]
|
|
2691
|
+
}
|
|
2513
2692
|
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2693
|
+
function $applyStylePatchToNode(node, patch) {
|
|
2694
|
+
const prevStyles = getStyleObjectFromCSS(node.getStyle());
|
|
2695
|
+
const newStyles = { ...prevStyles };
|
|
2696
|
+
|
|
2697
|
+
for (const [ key, value ] of Object.entries(patch)) {
|
|
2698
|
+
if (value === null) {
|
|
2699
|
+
delete newStyles[key];
|
|
2700
|
+
} else {
|
|
2701
|
+
newStyles[key] = value;
|
|
2519
2702
|
}
|
|
2520
|
-
return false
|
|
2521
2703
|
}
|
|
2522
2704
|
|
|
2523
|
-
|
|
2524
|
-
|
|
2705
|
+
const newCSSText = getCSSFromStyleObject(newStyles);
|
|
2706
|
+
node.setStyle(newCSSText);
|
|
2525
2707
|
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
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");
|
|
2530
2712
|
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
return $isRangeSelection(selection) && selection.isCollapsed()
|
|
2713
|
+
if (shouldHaveHighlight !== hasHighlight) {
|
|
2714
|
+
$setCodeHighlightFormat(node, shouldHaveHighlight);
|
|
2534
2715
|
}
|
|
2535
|
-
|
|
2536
|
-
}
|
|
2537
|
-
|
|
2538
|
-
function capitalize(str) {
|
|
2539
|
-
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
2540
2716
|
}
|
|
2541
2717
|
|
|
2542
|
-
function
|
|
2543
|
-
|
|
2718
|
+
function $setCodeHighlightFormat(node, shouldHaveHighlight) {
|
|
2719
|
+
const writable = node.getWritable();
|
|
2720
|
+
const IS_HIGHLIGHT = 1 << 7;
|
|
2544
2721
|
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2722
|
+
if (shouldHaveHighlight) {
|
|
2723
|
+
writable.__format |= IS_HIGHLIGHT;
|
|
2724
|
+
} else {
|
|
2725
|
+
writable.__format &= ~IS_HIGHLIGHT;
|
|
2548
2726
|
}
|
|
2549
2727
|
}
|
|
2550
2728
|
|
|
2551
|
-
function
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
return (...args) => {
|
|
2555
|
-
clearTimeout(timeout);
|
|
2556
|
-
|
|
2557
|
-
return new Promise((resolve, reject) => {
|
|
2558
|
-
timeout = setTimeout(async () => {
|
|
2559
|
-
try {
|
|
2560
|
-
const result = await fn(...args);
|
|
2561
|
-
resolve(result);
|
|
2562
|
-
} catch (err) {
|
|
2563
|
-
reject(err);
|
|
2564
|
-
}
|
|
2565
|
-
}, wait);
|
|
2566
|
-
})
|
|
2567
|
-
}
|
|
2729
|
+
function toggleOrReplace(oldValue, newValue) {
|
|
2730
|
+
return oldValue === newValue ? null : newValue
|
|
2568
2731
|
}
|
|
2569
2732
|
|
|
2570
|
-
function
|
|
2571
|
-
|
|
2733
|
+
function $syncHighlightWithStyle(textNode) {
|
|
2734
|
+
if (hasHighlightStyles(textNode.getStyle()) !== textNode.hasFormat("highlight")) {
|
|
2735
|
+
textNode.toggleFormat("highlight");
|
|
2736
|
+
}
|
|
2572
2737
|
}
|
|
2573
2738
|
|
|
2574
|
-
function
|
|
2575
|
-
|
|
2576
|
-
|
|
2739
|
+
function $syncHighlightWithCodeHighlightNode(node) {
|
|
2740
|
+
const parent = node.getParent();
|
|
2741
|
+
if (!$isCodeNode(parent)) return
|
|
2577
2742
|
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
}
|
|
2743
|
+
const shouldHaveHighlight = hasHighlightStyles(node.getStyle());
|
|
2744
|
+
const hasHighlight = node.hasFormat("highlight");
|
|
2581
2745
|
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
new URL(string);
|
|
2585
|
-
return true
|
|
2586
|
-
} catch {
|
|
2587
|
-
return false
|
|
2746
|
+
if (shouldHaveHighlight !== hasHighlight) {
|
|
2747
|
+
$setCodeHighlightFormat(node, shouldHaveHighlight);
|
|
2588
2748
|
}
|
|
2589
2749
|
}
|
|
2590
2750
|
|
|
2591
|
-
function
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
.normalize("NFD").replace(/[\u0300-\u036f]/g, "") // Remove diacritics
|
|
2595
|
-
}
|
|
2596
|
-
|
|
2597
|
-
function filterMatchPosition(text, potentialMatch) {
|
|
2598
|
-
const normalizedText = normalizeFilteredText(text);
|
|
2599
|
-
const normalizedMatch = normalizeFilteredText(potentialMatch);
|
|
2600
|
-
|
|
2601
|
-
if (!normalizedMatch) return 0
|
|
2751
|
+
function $canonicalizePastedStyles(textNode, canonicalizers = []) {
|
|
2752
|
+
if ($hasPastedStyles(textNode)) {
|
|
2753
|
+
$setPastedStyles(textNode, false);
|
|
2602
2754
|
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
}
|
|
2755
|
+
const canonicalizedCSS = applyCanonicalizers(textNode.getStyle(), canonicalizers);
|
|
2756
|
+
textNode.setStyle(canonicalizedCSS);
|
|
2606
2757
|
|
|
2607
|
-
|
|
2608
|
-
|
|
2758
|
+
const selection = $getSelection();
|
|
2759
|
+
if (textNode.isSelected(selection)) {
|
|
2760
|
+
selection.setStyle(textNode.getStyle());
|
|
2761
|
+
selection.setFormat(textNode.getFormat());
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2609
2764
|
}
|
|
2610
2765
|
|
|
2611
|
-
function
|
|
2612
|
-
|
|
2766
|
+
function $setPastedStyles(textNode, value = true) {
|
|
2767
|
+
$setState(textNode, hasPastedStylesState, value);
|
|
2613
2768
|
}
|
|
2614
2769
|
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
function parseBoolean(value) {
|
|
2618
|
-
if (typeof value === "string") return value === "true"
|
|
2619
|
-
return Boolean(value)
|
|
2770
|
+
function $hasPastedStyles(textNode) {
|
|
2771
|
+
return $getState(textNode, hasPastedStylesState)
|
|
2620
2772
|
}
|
|
2621
2773
|
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
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",
|
|
2629
2796
|
|
|
2630
|
-
|
|
2631
|
-
return defineExtension({
|
|
2632
|
-
name: "lexxy/rewritable-history",
|
|
2633
|
-
dependencies: [ HistoryExtension ],
|
|
2634
|
-
register: (editor, _config, state) => {
|
|
2635
|
-
const historyOutput = state.getDependency(HistoryExtension).output;
|
|
2636
|
-
this.#historyState = historyOutput.historyState.value;
|
|
2797
|
+
"insertTable",
|
|
2637
2798
|
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2799
|
+
"undo",
|
|
2800
|
+
"redo"
|
|
2801
|
+
];
|
|
2802
|
+
|
|
2803
|
+
class CommandDispatcher {
|
|
2804
|
+
#selectionBeforeDrag = null
|
|
2805
|
+
#listeners = new ListenerBin()
|
|
2806
|
+
|
|
2807
|
+
static configureFor(editorElement) {
|
|
2808
|
+
return new CommandDispatcher(editorElement)
|
|
2645
2809
|
}
|
|
2646
2810
|
|
|
2647
|
-
|
|
2648
|
-
|
|
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;
|
|
2817
|
+
|
|
2818
|
+
this.#registerCommands();
|
|
2819
|
+
this.#registerKeyboardCommands();
|
|
2820
|
+
this.#registerDragAndDropHandlers();
|
|
2649
2821
|
}
|
|
2650
2822
|
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
if (this.#historyState.current) entries.push(this.#historyState.current);
|
|
2654
|
-
return entries.concat(this.#historyState.redoStack)
|
|
2823
|
+
dispatchPaste(event) {
|
|
2824
|
+
return this.clipboard.paste(event)
|
|
2655
2825
|
}
|
|
2656
2826
|
|
|
2657
|
-
|
|
2658
|
-
this
|
|
2659
|
-
|
|
2827
|
+
dispatchBold() {
|
|
2828
|
+
this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold");
|
|
2829
|
+
}
|
|
2660
2830
|
|
|
2661
|
-
|
|
2831
|
+
dispatchItalic() {
|
|
2832
|
+
this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic");
|
|
2662
2833
|
}
|
|
2663
2834
|
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
const node = $getNodeByKey(nodeKey);
|
|
2668
|
-
if (!node) continue
|
|
2835
|
+
dispatchStrikethrough() {
|
|
2836
|
+
this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough");
|
|
2837
|
+
}
|
|
2669
2838
|
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
}
|
|
2673
|
-
}, { discrete: true, tag: this.#getBackgroundUpdateTags() });
|
|
2839
|
+
dispatchUnderline() {
|
|
2840
|
+
this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline");
|
|
2674
2841
|
}
|
|
2675
2842
|
|
|
2676
|
-
|
|
2677
|
-
|
|
2843
|
+
dispatchToggleHighlight(styles) {
|
|
2844
|
+
this.editor.dispatchCommand(TOGGLE_HIGHLIGHT_COMMAND, styles);
|
|
2845
|
+
}
|
|
2678
2846
|
|
|
2679
|
-
|
|
2680
|
-
|
|
2847
|
+
dispatchRemoveHighlight() {
|
|
2848
|
+
this.editor.dispatchCommand(REMOVE_HIGHLIGHT_COMMAND);
|
|
2849
|
+
}
|
|
2681
2850
|
|
|
2682
|
-
|
|
2851
|
+
dispatchLink(url) {
|
|
2852
|
+
this.editor.update(() => {
|
|
2853
|
+
const selection = $getSelection();
|
|
2854
|
+
if (!$isRangeSelection(selection)) return
|
|
2683
2855
|
|
|
2684
|
-
|
|
2685
|
-
const node = editorState._nodeMap.get(nodeKey);
|
|
2686
|
-
if (!node) continue
|
|
2856
|
+
const anchorNode = selection.anchor.getNode();
|
|
2687
2857
|
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
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);
|
|
2693
2865
|
}
|
|
2694
|
-
}
|
|
2866
|
+
});
|
|
2695
2867
|
}
|
|
2696
2868
|
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
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
|
+
}
|
|
2700
2875
|
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
if (!isEditorFocused(this.editorElement.editor)) { tags.push(SKIP_DOM_SELECTION_TAG); }
|
|
2704
|
-
return tags
|
|
2876
|
+
$toggleLink(null);
|
|
2877
|
+
});
|
|
2705
2878
|
}
|
|
2706
2879
|
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2880
|
+
dispatchInsertUnorderedList() {
|
|
2881
|
+
const selection = $getSelection();
|
|
2882
|
+
if (!$isRangeSelection(selection)) return
|
|
2710
2883
|
|
|
2711
|
-
|
|
2712
|
-
editorState._nodeMap.set(node.__key, $cloneNodeAdoptingKeys(replaceWith, node));
|
|
2713
|
-
}
|
|
2714
|
-
}
|
|
2884
|
+
const anchorNode = selection.anchor.getNode();
|
|
2715
2885
|
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
}
|
|
2886
|
+
if (this.selection.isInsideList && anchorNode && getListType(anchorNode) === "bullet") {
|
|
2887
|
+
this.contents.applyParagraphFormat();
|
|
2888
|
+
} else {
|
|
2889
|
+
this.contents.applyUnorderedListFormat();
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2721
2892
|
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
clone.__parent = previousNode.__parent;
|
|
2726
|
-
clone.__prev = previousNode.__prev;
|
|
2727
|
-
clone.__next = previousNode.__next;
|
|
2728
|
-
return clone
|
|
2729
|
-
}
|
|
2893
|
+
dispatchInsertOrderedList() {
|
|
2894
|
+
const selection = $getSelection();
|
|
2895
|
+
if (!$isRangeSelection(selection)) return
|
|
2730
2896
|
|
|
2731
|
-
|
|
2732
|
-
// A new Map is needed to prevent editing Lexical's internal map
|
|
2733
|
-
// Warning: this bypasses DEV's safety map freezing
|
|
2734
|
-
function safeCloneEditorState(editorState) {
|
|
2735
|
-
const clone = editorState.clone();
|
|
2736
|
-
clone._nodeMap = new Map(editorState._nodeMap);
|
|
2737
|
-
return clone
|
|
2738
|
-
}
|
|
2897
|
+
const anchorNode = selection.anchor.getNode();
|
|
2739
2898
|
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2899
|
+
if (this.selection.isInsideList && anchorNode && getListType(anchorNode) === "number") {
|
|
2900
|
+
this.contents.applyParagraphFormat();
|
|
2901
|
+
} else {
|
|
2902
|
+
this.contents.applyOrderedListFormat();
|
|
2903
|
+
}
|
|
2743
2904
|
}
|
|
2744
2905
|
|
|
2745
|
-
|
|
2746
|
-
|
|
2906
|
+
dispatchInsertQuoteBlock() {
|
|
2907
|
+
this.contents.toggleBlockquote();
|
|
2747
2908
|
}
|
|
2748
2909
|
|
|
2749
|
-
|
|
2750
|
-
|
|
2910
|
+
dispatchInsertCodeBlock() {
|
|
2911
|
+
if (this.selection.hasSelectedWordsInSingleLine) {
|
|
2912
|
+
this.#toggleInlineCode();
|
|
2913
|
+
} else {
|
|
2914
|
+
this.contents.toggleCodeBlock();
|
|
2915
|
+
}
|
|
2751
2916
|
}
|
|
2752
2917
|
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
return {
|
|
2757
|
-
conversion: (attachment) => ({
|
|
2758
|
-
node: new ActionTextAttachmentNode({
|
|
2759
|
-
sgid: attachment.getAttribute("sgid"),
|
|
2760
|
-
src: attachment.getAttribute("url"),
|
|
2761
|
-
previewable: attachment.getAttribute("previewable"),
|
|
2762
|
-
altText: attachment.getAttribute("alt"),
|
|
2763
|
-
caption: attachment.getAttribute("caption"),
|
|
2764
|
-
contentType: attachment.getAttribute("content-type"),
|
|
2765
|
-
fileName: attachment.getAttribute("filename"),
|
|
2766
|
-
fileSize: attachment.getAttribute("filesize"),
|
|
2767
|
-
width: attachment.getAttribute("width"),
|
|
2768
|
-
height: attachment.getAttribute("height")
|
|
2769
|
-
})
|
|
2770
|
-
}), priority: 1
|
|
2771
|
-
}
|
|
2772
|
-
},
|
|
2773
|
-
"img": () => {
|
|
2774
|
-
return {
|
|
2775
|
-
conversion: (img) => {
|
|
2776
|
-
const fileName = extractFileName(img.getAttribute("src") ?? "");
|
|
2777
|
-
return {
|
|
2778
|
-
node: new ActionTextAttachmentNode({
|
|
2779
|
-
src: img.getAttribute("src"),
|
|
2780
|
-
fileName: fileName,
|
|
2781
|
-
caption: img.getAttribute("alt") || "",
|
|
2782
|
-
contentType: "image/*",
|
|
2783
|
-
width: img.getAttribute("width"),
|
|
2784
|
-
height: img.getAttribute("height")
|
|
2785
|
-
})
|
|
2786
|
-
}
|
|
2787
|
-
}, priority: 1
|
|
2788
|
-
}
|
|
2789
|
-
},
|
|
2790
|
-
"video": () => {
|
|
2791
|
-
return {
|
|
2792
|
-
conversion: (video) => {
|
|
2793
|
-
const videoSource = video.getAttribute("src") || video.querySelector("source")?.src;
|
|
2794
|
-
const fileName = videoSource?.split("/")?.pop();
|
|
2795
|
-
const contentType = video.querySelector("source")?.getAttribute("content-type") || "video/*";
|
|
2918
|
+
#toggleInlineCode() {
|
|
2919
|
+
const selection = $getSelection();
|
|
2920
|
+
if (!$isRangeSelection(selection)) return
|
|
2796
2921
|
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
}
|
|
2804
|
-
}, priority: 1
|
|
2805
|
-
}
|
|
2922
|
+
if (!selection.isCollapsed()) {
|
|
2923
|
+
const textNodes = selection.getNodes().filter($isTextNode);
|
|
2924
|
+
const applyingCode = !textNodes.every((node) => node.hasFormat("code"));
|
|
2925
|
+
|
|
2926
|
+
if (applyingCode) {
|
|
2927
|
+
this.#stripInlineFormattingFromSelection(selection, textNodes);
|
|
2806
2928
|
}
|
|
2807
2929
|
}
|
|
2808
|
-
}
|
|
2809
2930
|
|
|
2810
|
-
|
|
2811
|
-
return Lexxy.global.get("attachmentTagName")
|
|
2931
|
+
this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code");
|
|
2812
2932
|
}
|
|
2813
2933
|
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
this.previewable = parseBoolean(previewable);
|
|
2822
|
-
this.pendingPreview = pendingPreview;
|
|
2823
|
-
this.altText = altText || "";
|
|
2824
|
-
this.caption = caption || "";
|
|
2825
|
-
this.contentType = contentType || "";
|
|
2826
|
-
this.fileName = fileName || "";
|
|
2827
|
-
this.fileSize = fileSize;
|
|
2828
|
-
this.width = width;
|
|
2829
|
-
this.height = height;
|
|
2830
|
-
this.uploadError = uploadError;
|
|
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;
|
|
2831
2941
|
|
|
2832
|
-
|
|
2833
|
-
|
|
2942
|
+
for (let i = 0; i < textNodes.length; i++) {
|
|
2943
|
+
const node = textNodes[i];
|
|
2944
|
+
if (node.getFormat() === 0) continue
|
|
2834
2945
|
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
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();
|
|
2838
2950
|
|
|
2839
|
-
|
|
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);
|
|
2840
2957
|
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
figure.appendChild(this.#createDOMForFile());
|
|
2849
|
-
figure.appendChild(this.#createDOMForNotImage());
|
|
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
|
+
}
|
|
2850
2965
|
}
|
|
2851
|
-
|
|
2852
|
-
return figure
|
|
2853
2966
|
}
|
|
2854
2967
|
|
|
2855
|
-
|
|
2856
|
-
|
|
2968
|
+
dispatchSetCodeLanguage(language) {
|
|
2969
|
+
this.editor.update(() => {
|
|
2970
|
+
if (!this.selection.isInsideCodeBlock) return
|
|
2857
2971
|
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
caption.value = this.caption;
|
|
2861
|
-
}
|
|
2972
|
+
const codeNode = this.selection.nearestNodeOfType(CodeNode);
|
|
2973
|
+
if (!codeNode) return
|
|
2862
2974
|
|
|
2863
|
-
|
|
2975
|
+
codeNode.setLanguage(language);
|
|
2976
|
+
});
|
|
2864
2977
|
}
|
|
2865
2978
|
|
|
2866
|
-
|
|
2867
|
-
|
|
2979
|
+
dispatchInsertHorizontalDivider() {
|
|
2980
|
+
this.contents.insertAtCursorEnsuringLineBelow(new HorizontalDividerNode());
|
|
2981
|
+
this.editor.focus();
|
|
2868
2982
|
}
|
|
2869
2983
|
|
|
2870
|
-
|
|
2871
|
-
|
|
2984
|
+
dispatchSetFormatHeadingLarge() {
|
|
2985
|
+
this.contents.applyHeadingFormat("h2");
|
|
2872
2986
|
}
|
|
2873
2987
|
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
previewable: this.previewable || null,
|
|
2878
|
-
url: this.src,
|
|
2879
|
-
alt: this.altText,
|
|
2880
|
-
caption: this.caption,
|
|
2881
|
-
"content-type": this.contentType,
|
|
2882
|
-
filename: this.fileName,
|
|
2883
|
-
filesize: this.fileSize,
|
|
2884
|
-
width: this.width,
|
|
2885
|
-
height: this.height,
|
|
2886
|
-
presentation: "gallery"
|
|
2887
|
-
});
|
|
2988
|
+
dispatchSetFormatHeadingMedium() {
|
|
2989
|
+
this.contents.applyHeadingFormat("h3");
|
|
2990
|
+
}
|
|
2888
2991
|
|
|
2889
|
-
|
|
2992
|
+
dispatchSetFormatHeadingSmall() {
|
|
2993
|
+
this.contents.applyHeadingFormat("h4");
|
|
2890
2994
|
}
|
|
2891
2995
|
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
type: "action_text_attachment",
|
|
2895
|
-
version: 1,
|
|
2896
|
-
tagName: this.tagName,
|
|
2897
|
-
sgid: this.sgid,
|
|
2898
|
-
src: this.src,
|
|
2899
|
-
previewable: this.previewable,
|
|
2900
|
-
altText: this.altText,
|
|
2901
|
-
caption: this.caption,
|
|
2902
|
-
contentType: this.contentType,
|
|
2903
|
-
fileName: this.fileName,
|
|
2904
|
-
fileSize: this.fileSize,
|
|
2905
|
-
width: this.width,
|
|
2906
|
-
height: this.height
|
|
2907
|
-
}
|
|
2996
|
+
dispatchSetFormatParagraph() {
|
|
2997
|
+
this.contents.applyParagraphFormat();
|
|
2908
2998
|
}
|
|
2909
2999
|
|
|
2910
|
-
|
|
2911
|
-
|
|
3000
|
+
dispatchClearFormatting() {
|
|
3001
|
+
this.contents.clearFormatting();
|
|
2912
3002
|
}
|
|
2913
3003
|
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
figure.classList.add("attachment--error");
|
|
2917
|
-
figure.appendChild(createElement("div", { innerText: `Error uploading ${this.fileName || "file"}` }));
|
|
2918
|
-
return figure
|
|
3004
|
+
dispatchUploadImage() {
|
|
3005
|
+
this.#dispatchUploadAttachment("image/*,video/*");
|
|
2919
3006
|
}
|
|
2920
3007
|
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
figure.dataset.lexicalNodeKey = this.__key;
|
|
3008
|
+
dispatchUploadFile() {
|
|
3009
|
+
this.#dispatchUploadAttachment();
|
|
3010
|
+
}
|
|
2925
3011
|
|
|
2926
|
-
|
|
2927
|
-
|
|
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
|
+
};
|
|
2928
3021
|
|
|
2929
|
-
|
|
2930
|
-
}
|
|
3022
|
+
if (accept) attributes.accept = accept;
|
|
2931
3023
|
|
|
2932
|
-
|
|
2933
|
-
return this.isPreviewableImage || this.previewable
|
|
2934
|
-
}
|
|
3024
|
+
const input = createElement("input", attributes);
|
|
2935
3025
|
|
|
2936
|
-
|
|
2937
|
-
|
|
3026
|
+
// Append and remove to make testable
|
|
3027
|
+
this.editorElement.appendChild(input);
|
|
3028
|
+
input.click();
|
|
3029
|
+
setTimeout(() => input.remove(), 1000);
|
|
2938
3030
|
}
|
|
2939
3031
|
|
|
2940
|
-
|
|
2941
|
-
|
|
3032
|
+
dispatchInsertTable() {
|
|
3033
|
+
this.editor.dispatchCommand(INSERT_TABLE_COMMAND, { "rows": 3, "columns": 3, "includeHeaders": true });
|
|
2942
3034
|
}
|
|
2943
3035
|
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
figure.appendChild(this.#createDOMForFile());
|
|
2947
|
-
figure.appendChild(this.#createDOMForNotImage());
|
|
2948
|
-
this.#pollForPreview(figure);
|
|
2949
|
-
return figure
|
|
3036
|
+
dispatchUndo() {
|
|
3037
|
+
this.editor.dispatchCommand(UNDO_COMMAND, undefined);
|
|
2950
3038
|
}
|
|
2951
3039
|
|
|
2952
|
-
|
|
2953
|
-
this.editor.dispatchCommand(
|
|
2954
|
-
[this.getKey()]: { patch }
|
|
2955
|
-
});
|
|
3040
|
+
dispatchRedo() {
|
|
3041
|
+
this.editor.dispatchCommand(REDO_COMMAND, undefined);
|
|
2956
3042
|
}
|
|
2957
3043
|
|
|
2958
|
-
|
|
2959
|
-
this.
|
|
2960
|
-
[this.getKey()]: { replace: node }
|
|
2961
|
-
});
|
|
3044
|
+
dispose() {
|
|
3045
|
+
this.#listeners.dispose();
|
|
2962
3046
|
}
|
|
2963
3047
|
|
|
2964
|
-
#
|
|
2965
|
-
const
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
if (this.previewable && !this.isPreviewableImage) {
|
|
2969
|
-
img.onerror = () => this.#swapPreviewToFileDOM(img);
|
|
3048
|
+
#registerCommands() {
|
|
3049
|
+
for (const command of COMMANDS) {
|
|
3050
|
+
const methodName = `dispatch${capitalize(command)}`;
|
|
3051
|
+
this.#registerCommandHandler(command, 0, this[methodName].bind(this));
|
|
2970
3052
|
}
|
|
2971
3053
|
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
}
|
|
3054
|
+
this.#registerCommandHandler(PASTE_COMMAND, COMMAND_PRIORITY_LOW, this.dispatchPaste.bind(this));
|
|
3055
|
+
}
|
|
2975
3056
|
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
return container
|
|
3057
|
+
#registerCommandHandler(command, priority, handler) {
|
|
3058
|
+
this.#listeners.track(this.editor.registerCommand(command, handler, priority));
|
|
2979
3059
|
}
|
|
2980
3060
|
|
|
2981
|
-
#
|
|
2982
|
-
|
|
2983
|
-
|
|
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
|
+
}
|
|
2984
3065
|
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
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
|
|
3070
|
+
|
|
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
|
|
2988
3078
|
}
|
|
2989
3079
|
|
|
2990
|
-
#
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
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
|
+
);
|
|
3090
|
+
}
|
|
2994
3091
|
}
|
|
2995
3092
|
|
|
2996
|
-
#
|
|
2997
|
-
this
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3093
|
+
#handleDragEnter(event) {
|
|
3094
|
+
if (this.#isInternalDrag(event)) return
|
|
3095
|
+
|
|
3096
|
+
this.dragCounter++;
|
|
3097
|
+
if (this.dragCounter === 1) {
|
|
3098
|
+
this.#saveSelectionBeforeDrag();
|
|
3099
|
+
this.editor.getRootElement().classList.add("lexxy-editor--drag-over");
|
|
3100
|
+
}
|
|
3002
3101
|
}
|
|
3003
3102
|
|
|
3004
|
-
#
|
|
3005
|
-
if (
|
|
3103
|
+
#handleDragLeave(event) {
|
|
3104
|
+
if (this.#isInternalDrag(event)) return
|
|
3105
|
+
|
|
3106
|
+
this.dragCounter--;
|
|
3107
|
+
if (this.dragCounter === 0) {
|
|
3108
|
+
this.#selectionBeforeDrag = null;
|
|
3109
|
+
this.editor.getRootElement().classList.remove("lexxy-editor--drag-over");
|
|
3110
|
+
}
|
|
3006
3111
|
}
|
|
3007
3112
|
|
|
3008
|
-
#
|
|
3009
|
-
|
|
3010
|
-
if (!figure) return
|
|
3113
|
+
#handleDragOver(event) {
|
|
3114
|
+
if (this.#isInternalDrag(event)) return
|
|
3011
3115
|
|
|
3012
|
-
|
|
3013
|
-
figure.appendChild(this.#createDOMForFile());
|
|
3014
|
-
figure.appendChild(this.#createDOMForNotImage());
|
|
3015
|
-
});
|
|
3116
|
+
event.preventDefault();
|
|
3016
3117
|
}
|
|
3017
3118
|
|
|
3018
|
-
#
|
|
3019
|
-
|
|
3020
|
-
const maxAttempts = 10;
|
|
3119
|
+
#handleDrop(event) {
|
|
3120
|
+
if (this.#isInternalDrag(event)) return
|
|
3021
3121
|
|
|
3022
|
-
|
|
3023
|
-
if (!this.editor.read(() => this.isAttached())) return
|
|
3122
|
+
event.preventDefault();
|
|
3024
3123
|
|
|
3025
|
-
|
|
3026
|
-
|
|
3124
|
+
this.dragCounter = 0;
|
|
3125
|
+
this.editor.getRootElement().classList.remove("lexxy-editor--drag-over");
|
|
3027
3126
|
|
|
3028
|
-
|
|
3029
|
-
|
|
3127
|
+
const dataTransfer = event.dataTransfer;
|
|
3128
|
+
if (!dataTransfer) return
|
|
3030
3129
|
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
if (img.naturalWidth > 150 && img.naturalHeight > 150) {
|
|
3034
|
-
this.#swapToPreviewDOM(figure, cacheBustedSrc);
|
|
3035
|
-
} else {
|
|
3036
|
-
retry();
|
|
3037
|
-
}
|
|
3038
|
-
};
|
|
3039
|
-
img.onerror = () => retry();
|
|
3040
|
-
img.src = cacheBustedSrc;
|
|
3041
|
-
};
|
|
3130
|
+
const files = Array.from(dataTransfer.files);
|
|
3131
|
+
if (!files.length) return
|
|
3042
3132
|
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
if (attempt < maxAttempts && this.editor.read(() => this.isAttached())) {
|
|
3046
|
-
const delay = Math.min(2000 * Math.pow(1.5, attempt), 15000);
|
|
3047
|
-
setTimeout(tryLoad, delay);
|
|
3048
|
-
}
|
|
3049
|
-
};
|
|
3133
|
+
this.#restoreSelectionBeforeDrag();
|
|
3134
|
+
this.contents.uploadFiles(files, { selectLast: true });
|
|
3050
3135
|
|
|
3051
|
-
|
|
3052
|
-
setTimeout(tryLoad, 3000);
|
|
3136
|
+
this.editor.focus();
|
|
3053
3137
|
}
|
|
3054
3138
|
|
|
3055
|
-
#
|
|
3056
|
-
this
|
|
3057
|
-
|
|
3058
|
-
img.onerror = () => this.#swapPreviewToFileDOM(img);
|
|
3059
|
-
const container = createElement("div", { className: "attachment__container" });
|
|
3060
|
-
container.appendChild(img);
|
|
3061
|
-
figure.appendChild(container);
|
|
3062
|
-
figure.appendChild(this.#createEditableCaption());
|
|
3139
|
+
#saveSelectionBeforeDrag() {
|
|
3140
|
+
this.editor.getEditorState().read(() => {
|
|
3141
|
+
this.#selectionBeforeDrag = $getSelection()?.clone();
|
|
3063
3142
|
});
|
|
3064
|
-
|
|
3065
|
-
this.patchAndRewriteHistory({ pendingPreview: false });
|
|
3066
3143
|
}
|
|
3067
3144
|
|
|
3068
|
-
#
|
|
3069
|
-
|
|
3145
|
+
#restoreSelectionBeforeDrag() {
|
|
3146
|
+
if (!this.#selectionBeforeDrag) return
|
|
3070
3147
|
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
}
|
|
3148
|
+
this.editor.update(() => {
|
|
3149
|
+
$setSelection(this.#selectionBeforeDrag);
|
|
3150
|
+
});
|
|
3074
3151
|
|
|
3075
|
-
|
|
3152
|
+
this.#selectionBeforeDrag = null;
|
|
3076
3153
|
}
|
|
3077
3154
|
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
return { width: this.width, height: this.height }
|
|
3081
|
-
} else {
|
|
3082
|
-
return {}
|
|
3083
|
-
}
|
|
3155
|
+
#isInternalDrag(event) {
|
|
3156
|
+
return event.dataTransfer?.types.includes("application/x-lexxy-node-key")
|
|
3084
3157
|
}
|
|
3085
3158
|
|
|
3086
|
-
#
|
|
3087
|
-
|
|
3088
|
-
|
|
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
|
|
3089
3166
|
}
|
|
3090
3167
|
|
|
3091
|
-
#
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
const nameTag = createElement("strong", { className: "attachment__name", textContent: this.caption || this.fileName });
|
|
3095
|
-
|
|
3096
|
-
figcaption.appendChild(nameTag);
|
|
3097
|
-
|
|
3098
|
-
if (this.fileSize) {
|
|
3099
|
-
const sizeSpan = createElement("span", { className: "attachment__size", textContent: bytesToHumanSize(this.fileSize) });
|
|
3100
|
-
figcaption.appendChild(sizeSpan);
|
|
3101
|
-
}
|
|
3168
|
+
#handleTabForList(event) {
|
|
3169
|
+
if (event.shiftKey && !this.selection.isIndentedList) return false
|
|
3102
3170
|
|
|
3103
|
-
|
|
3171
|
+
event.preventDefault();
|
|
3172
|
+
const command = event.shiftKey? OUTDENT_CONTENT_COMMAND : INDENT_CONTENT_COMMAND;
|
|
3173
|
+
return this.editor.dispatchCommand(command)
|
|
3104
3174
|
}
|
|
3105
3175
|
|
|
3106
|
-
#
|
|
3107
|
-
const
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
placeholder: this.fileName,
|
|
3111
|
-
rows: "1"
|
|
3112
|
-
});
|
|
3113
|
-
|
|
3114
|
-
input.addEventListener("focusin", () => input.placeholder = "Add caption...");
|
|
3115
|
-
input.addEventListener("blur", (event) => this.#handleCaptionInputBlurred(event));
|
|
3116
|
-
input.addEventListener("keydown", (event) => this.#handleCaptionInputKeydown(event));
|
|
3117
|
-
input.addEventListener("copy", (event) => event.stopPropagation());
|
|
3118
|
-
input.addEventListener("cut", (event) => event.stopPropagation());
|
|
3119
|
-
input.addEventListener("paste", (event) => event.stopPropagation());
|
|
3176
|
+
#handleTabForCode() {
|
|
3177
|
+
const selection = $getSelection();
|
|
3178
|
+
return $isRangeSelection(selection) && selection.isCollapsed()
|
|
3179
|
+
}
|
|
3120
3180
|
|
|
3121
|
-
|
|
3181
|
+
}
|
|
3122
3182
|
|
|
3123
|
-
|
|
3124
|
-
|
|
3183
|
+
function capitalize(str) {
|
|
3184
|
+
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
3185
|
+
}
|
|
3125
3186
|
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
}
|
|
3187
|
+
function debounce(fn, wait) {
|
|
3188
|
+
let timeout;
|
|
3129
3189
|
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
this.getWritable().caption = input.value;
|
|
3134
|
-
});
|
|
3190
|
+
return (...args) => {
|
|
3191
|
+
clearTimeout(timeout);
|
|
3192
|
+
timeout = setTimeout(() => fn(...args), wait);
|
|
3135
3193
|
}
|
|
3194
|
+
}
|
|
3136
3195
|
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
event.preventDefault();
|
|
3140
|
-
event.target.blur();
|
|
3196
|
+
function debounceAsync(fn, wait) {
|
|
3197
|
+
let timeout;
|
|
3141
3198
|
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
this.selectNext(0, 0);
|
|
3145
|
-
}, {
|
|
3146
|
-
tag: HISTORY_MERGE_TAG
|
|
3147
|
-
});
|
|
3148
|
-
}
|
|
3199
|
+
return (...args) => {
|
|
3200
|
+
clearTimeout(timeout);
|
|
3149
3201
|
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
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
|
+
})
|
|
3154
3212
|
}
|
|
3155
3213
|
}
|
|
3156
3214
|
|
|
3157
|
-
function
|
|
3158
|
-
return new
|
|
3215
|
+
function delay(ms) {
|
|
3216
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
3159
3217
|
}
|
|
3160
3218
|
|
|
3161
|
-
function
|
|
3162
|
-
return
|
|
3219
|
+
function nextFrame() {
|
|
3220
|
+
return new Promise(requestAnimationFrame)
|
|
3163
3221
|
}
|
|
3164
3222
|
|
|
3165
3223
|
class Selection {
|
|
@@ -4646,7 +4704,6 @@ class NodeInserter {
|
|
|
4646
4704
|
static for(selection) {
|
|
4647
4705
|
const INSERTERS = [
|
|
4648
4706
|
CodeNodeInserter,
|
|
4649
|
-
QuoteNodeInserter,
|
|
4650
4707
|
ShadowRootNodeInserter,
|
|
4651
4708
|
NodeSelectionNodeInserter
|
|
4652
4709
|
];
|
|
@@ -4695,25 +4752,6 @@ class CodeNodeInserter extends NodeInserter {
|
|
|
4695
4752
|
|
|
4696
4753
|
}
|
|
4697
4754
|
|
|
4698
|
-
// Lexical will split a QuoteNode when inserting other Elements - we want them simply inserted as-is
|
|
4699
|
-
class QuoteNodeInserter extends NodeInserter {
|
|
4700
|
-
static handles(selection) {
|
|
4701
|
-
return $getNearestNodeOfType(selection.anchor?.getNode(), QuoteNode)
|
|
4702
|
-
}
|
|
4703
|
-
|
|
4704
|
-
insertNodes(nodes) {
|
|
4705
|
-
if (!this.selection.isCollapsed()) { this.selection.removeText(); }
|
|
4706
|
-
|
|
4707
|
-
$ensureForwardRangeSelection(this.selection);
|
|
4708
|
-
let lastNode = this.selection.focus.getNode();
|
|
4709
|
-
for (const node of nodes) {
|
|
4710
|
-
lastNode = lastNode.insertAfter(node);
|
|
4711
|
-
}
|
|
4712
|
-
|
|
4713
|
-
lastNode.selectEnd();
|
|
4714
|
-
}
|
|
4715
|
-
}
|
|
4716
|
-
|
|
4717
4755
|
class ShadowRootNodeInserter extends NodeInserter {
|
|
4718
4756
|
static handles(selection) {
|
|
4719
4757
|
return $isShadowRoot(selection?.anchor.getNode())
|
|
@@ -4768,7 +4806,7 @@ class Contents {
|
|
|
4768
4806
|
this.editor.update(() => {
|
|
4769
4807
|
if ($hasUpdateTag(PASTE_TAG)) this.#stripTableCellColorStyles(doc);
|
|
4770
4808
|
|
|
4771
|
-
const nodes = $
|
|
4809
|
+
const nodes = $generateFilteredNodesFromDOM(this.editorElement, doc);
|
|
4772
4810
|
if (!this.#insertUploadNodes(nodes)) {
|
|
4773
4811
|
this.insertAtCursor(...nodes);
|
|
4774
4812
|
}
|
|
@@ -4861,7 +4899,7 @@ class Contents {
|
|
|
4861
4899
|
} else {
|
|
4862
4900
|
topLevelElements.filter($isQuoteNode).forEach(node => this.#unwrap(node));
|
|
4863
4901
|
|
|
4864
|
-
|
|
4902
|
+
$splitParagraphsAtLineBreakBoundaries(selection);
|
|
4865
4903
|
|
|
4866
4904
|
const elements = this.#topLevelElementsInSelection(selection);
|
|
4867
4905
|
if (elements.length === 0) return
|
|
@@ -5317,16 +5355,19 @@ class Contents {
|
|
|
5317
5355
|
|
|
5318
5356
|
#createCustomAttachmentNodeWithHtml(html, options = {}) {
|
|
5319
5357
|
const attachmentConfig = typeof options === "object" ? options : {};
|
|
5320
|
-
|
|
5358
|
+
const contentType = attachmentConfig.contentType || "text/html";
|
|
5359
|
+
if (!this.editorElement.permitsAttachmentContentType(contentType)) {
|
|
5360
|
+
return this.#createHtmlNodeWith(html)
|
|
5361
|
+
}
|
|
5321
5362
|
return new CustomActionTextAttachmentNode({
|
|
5322
5363
|
sgid: attachmentConfig.sgid || null,
|
|
5323
|
-
contentType
|
|
5324
|
-
innerHtml: html
|
|
5364
|
+
contentType,
|
|
5365
|
+
innerHtml: html,
|
|
5325
5366
|
})
|
|
5326
5367
|
}
|
|
5327
5368
|
|
|
5328
5369
|
#createHtmlNodeWith(html) {
|
|
5329
|
-
const htmlNodes = $
|
|
5370
|
+
const htmlNodes = $generateFilteredNodesFromDOM(this.editorElement, parseHtml(html));
|
|
5330
5371
|
return htmlNodes[0] || $createParagraphNode()
|
|
5331
5372
|
}
|
|
5332
5373
|
|
|
@@ -5364,7 +5405,13 @@ class Clipboard {
|
|
|
5364
5405
|
paste(event) {
|
|
5365
5406
|
const clipboardData = event.clipboardData;
|
|
5366
5407
|
|
|
5367
|
-
if (!clipboardData
|
|
5408
|
+
if (!clipboardData) return false
|
|
5409
|
+
|
|
5410
|
+
if (this.#isPastingIntoCodeBlock()) {
|
|
5411
|
+
this.#pastePlainTextIntoCodeBlock(clipboardData);
|
|
5412
|
+
event.preventDefault();
|
|
5413
|
+
return true
|
|
5414
|
+
}
|
|
5368
5415
|
|
|
5369
5416
|
if (this.#isPlainTextOrURLPasted(clipboardData)) {
|
|
5370
5417
|
this.#pastePlainText(clipboardData);
|
|
@@ -5413,6 +5460,16 @@ class Clipboard {
|
|
|
5413
5460
|
return result
|
|
5414
5461
|
}
|
|
5415
5462
|
|
|
5463
|
+
#pastePlainTextIntoCodeBlock(clipboardData) {
|
|
5464
|
+
const text = clipboardData.getData("text/plain");
|
|
5465
|
+
if (!text) return
|
|
5466
|
+
|
|
5467
|
+
this.editor.update(() => {
|
|
5468
|
+
const selection = $getSelection();
|
|
5469
|
+
if ($isRangeSelection(selection)) selection.insertRawText(text);
|
|
5470
|
+
}, { tag: PASTE_TAG });
|
|
5471
|
+
}
|
|
5472
|
+
|
|
5416
5473
|
#pastePlainText(clipboardData) {
|
|
5417
5474
|
const item = clipboardData.items[0];
|
|
5418
5475
|
item.getAsString((text) => {
|
|
@@ -6510,39 +6567,16 @@ class EarlyEscapeCodeNode extends CodeNode {
|
|
|
6510
6567
|
}
|
|
6511
6568
|
|
|
6512
6569
|
insertNewAfter(selection, restoreSelection) {
|
|
6513
|
-
if (!selection.isCollapsed())
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
if (this.#isCursorAtStart(selection)) {
|
|
6525
|
-
this.insertBefore($createParagraphNode());
|
|
6526
|
-
return null
|
|
6527
|
-
}
|
|
6528
|
-
|
|
6529
|
-
if (this.#isCursorOnEmptyLastLine(selection)) {
|
|
6530
|
-
$trimTrailingBlankNodes(this);
|
|
6531
|
-
|
|
6532
|
-
const paragraph = $createParagraphNode();
|
|
6533
|
-
this.insertAfter(paragraph);
|
|
6534
|
-
return paragraph
|
|
6535
|
-
}
|
|
6536
|
-
|
|
6537
|
-
return super.insertNewAfter(selection, restoreSelection)
|
|
6538
|
-
}
|
|
6539
|
-
|
|
6540
|
-
#clampSelectionOffset(selection) {
|
|
6541
|
-
const childrenSize = this.getChildrenSize();
|
|
6542
|
-
for (const point of [ selection.anchor, selection.focus ]) {
|
|
6543
|
-
if (point.type === "element" && point.key === this.__key && point.offset > childrenSize) {
|
|
6544
|
-
point.set(this.__key, childrenSize, "element");
|
|
6545
|
-
}
|
|
6570
|
+
if ($hasUpdateTag(PASTE_TAG) || !selection.isCollapsed()) {
|
|
6571
|
+
return super.insertNewAfter(selection, restoreSelection)
|
|
6572
|
+
} else if (this.#isCursorAtStart(selection)) {
|
|
6573
|
+
return this.#insertParagraphBefore()
|
|
6574
|
+
} else if (this.#isCursorOnWhitespaceOnlyLastLine(selection)) {
|
|
6575
|
+
return this.#insertBlankLineBelow(selection, restoreSelection)
|
|
6576
|
+
} else if (this.#isCursorOnEmptyLastLine(selection)) {
|
|
6577
|
+
return this.#escapeToNewParagraphAfter()
|
|
6578
|
+
} else {
|
|
6579
|
+
return super.insertNewAfter(selection, restoreSelection)
|
|
6546
6580
|
}
|
|
6547
6581
|
}
|
|
6548
6582
|
|
|
@@ -6561,6 +6595,32 @@ class EarlyEscapeCodeNode extends CodeNode {
|
|
|
6561
6595
|
return textContent === "" || textContent.endsWith("\n")
|
|
6562
6596
|
}
|
|
6563
6597
|
|
|
6598
|
+
#isCursorOnWhitespaceOnlyLastLine(selection) {
|
|
6599
|
+
if (!$isCursorOnLastLine(selection)) return false
|
|
6600
|
+
|
|
6601
|
+
const textContent = this.getTextContent();
|
|
6602
|
+
const lastNewlineIndex = textContent.lastIndexOf("\n");
|
|
6603
|
+
const lastLine = lastNewlineIndex === -1 ? textContent : textContent.slice(lastNewlineIndex + 1);
|
|
6604
|
+
return lastLine.length > 0 && lastLine.trim() === ""
|
|
6605
|
+
}
|
|
6606
|
+
|
|
6607
|
+
#insertParagraphBefore() {
|
|
6608
|
+
this.insertBefore($createParagraphNode());
|
|
6609
|
+
return null
|
|
6610
|
+
}
|
|
6611
|
+
|
|
6612
|
+
#insertBlankLineBelow(selection, restoreSelection) {
|
|
6613
|
+
super.insertNewAfter(selection, restoreSelection);
|
|
6614
|
+
this.getLastChild().remove();
|
|
6615
|
+
return null
|
|
6616
|
+
}
|
|
6617
|
+
|
|
6618
|
+
#escapeToNewParagraphAfter() {
|
|
6619
|
+
$trimTrailingBlankNodes(this);
|
|
6620
|
+
const paragraph = $createParagraphNode();
|
|
6621
|
+
this.insertAfter(paragraph);
|
|
6622
|
+
return paragraph
|
|
6623
|
+
}
|
|
6564
6624
|
}
|
|
6565
6625
|
|
|
6566
6626
|
class EarlyEscapeListItemNode extends ListItemNode {
|
|
@@ -6887,6 +6947,25 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6887
6947
|
return this.dataset.blobUrlTemplate
|
|
6888
6948
|
}
|
|
6889
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
|
+
|
|
6890
6969
|
get isEmpty() {
|
|
6891
6970
|
return [ "<p><br></p>", "<p></p>", "" ].includes(this.value.trim())
|
|
6892
6971
|
}
|
|
@@ -6965,7 +7044,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6965
7044
|
}
|
|
6966
7045
|
|
|
6967
7046
|
get #isContentFocused() {
|
|
6968
|
-
return !!this.
|
|
7047
|
+
return !!this.editor && isEditorFocused(this.editor)
|
|
6969
7048
|
}
|
|
6970
7049
|
|
|
6971
7050
|
get value() {
|
|
@@ -6979,14 +7058,24 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6979
7058
|
}
|
|
6980
7059
|
|
|
6981
7060
|
set value(html) {
|
|
7061
|
+
const editorHasFocus = this.#isContentFocused;
|
|
7062
|
+
|
|
6982
7063
|
this.editor.update(() => {
|
|
7064
|
+
if (editorHasFocus) {
|
|
7065
|
+
// Address Safari inconsistently placing the cursor in the contenteditable by forcing focus back onto the editor
|
|
7066
|
+
// Use direct `editor.focus` to bypass the pre-existing focus optimization and skip the callback
|
|
7067
|
+
$onUpdate(() => this.editor.focus());
|
|
7068
|
+
} else {
|
|
7069
|
+
$addUpdateTag(SKIP_DOM_SELECTION_TAG);
|
|
7070
|
+
}
|
|
7071
|
+
|
|
6983
7072
|
$getRoot()
|
|
6984
7073
|
.clear()
|
|
6985
7074
|
.selectEnd()
|
|
6986
7075
|
.insertNodes(this.#parseHtmlIntoLexicalNodes(html));
|
|
6987
7076
|
|
|
6988
7077
|
this.#toggleEmptyStatus();
|
|
6989
|
-
}, { discrete: true
|
|
7078
|
+
}, { discrete: true });
|
|
6990
7079
|
}
|
|
6991
7080
|
|
|
6992
7081
|
get canUndo() {
|
|
@@ -6999,7 +7088,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6999
7088
|
|
|
7000
7089
|
#parseHtmlIntoLexicalNodes(html) {
|
|
7001
7090
|
if (!html) html = "<p></p>";
|
|
7002
|
-
const nodes = $
|
|
7091
|
+
const nodes = $generateFilteredNodesFromDOM(this, parseHtml(`${html}`));
|
|
7003
7092
|
|
|
7004
7093
|
return nodes
|
|
7005
7094
|
.filter(this.#isNotWhitespaceOnlyNode)
|
|
@@ -7031,6 +7120,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7031
7120
|
this.#handleEnter();
|
|
7032
7121
|
this.#registerFocusEvents();
|
|
7033
7122
|
this.#registerHistoryEvents();
|
|
7123
|
+
this.#registerFileAcceptFilter();
|
|
7034
7124
|
this.#attachDebugHooks();
|
|
7035
7125
|
this.#attachToolbar();
|
|
7036
7126
|
this.#configureSanitizer();
|
|
@@ -7038,6 +7128,16 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7038
7128
|
this.#resetBeforeTurboCaches();
|
|
7039
7129
|
}
|
|
7040
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
|
+
|
|
7041
7141
|
#createEditor() {
|
|
7042
7142
|
this.editorContentElement ||= this.#createEditorContentElement();
|
|
7043
7143
|
this.appendChild(this.editorContentElement);
|
|
@@ -7981,6 +8081,8 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
7981
8081
|
}
|
|
7982
8082
|
|
|
7983
8083
|
#addTriggerListener() {
|
|
8084
|
+
if (!this.#promptContentTypePermitted) return
|
|
8085
|
+
|
|
7984
8086
|
this.#popoverListeners.track(this.#editor.registerUpdateListener(({ editorState }) => {
|
|
7985
8087
|
editorState.read(() => {
|
|
7986
8088
|
if (this.#selection.isInsideCodeBlock) return
|
|
@@ -8014,6 +8116,19 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8014
8116
|
}));
|
|
8015
8117
|
}
|
|
8016
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
|
+
|
|
8017
8132
|
#addCursorPositionListener() {
|
|
8018
8133
|
this.#popoverListeners.track(this.#editor.registerUpdateListener(({ editorState }) => {
|
|
8019
8134
|
if (this.closed) return
|
|
@@ -8320,7 +8435,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8320
8435
|
}
|
|
8321
8436
|
|
|
8322
8437
|
#buildEditableTextNodes(template) {
|
|
8323
|
-
return $
|
|
8438
|
+
return $generateFilteredNodesFromDOM(this.#editorElement, parseHtml(`${template.innerHTML}`))
|
|
8324
8439
|
}
|
|
8325
8440
|
|
|
8326
8441
|
#insertTemplatesAsAttachments(templates, stringToReplace, fallbackSgid = null) {
|
|
@@ -8332,8 +8447,10 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8332
8447
|
}
|
|
8333
8448
|
|
|
8334
8449
|
#buildAttachmentNodes(templates, fallbackSgid = null) {
|
|
8335
|
-
return templates
|
|
8336
|
-
template => this.#
|
|
8450
|
+
return templates
|
|
8451
|
+
.filter(template => this.#editorElement.permitsAttachmentContentType(
|
|
8452
|
+
template.getAttribute("content-type") || this.#defaultPromptContentType))
|
|
8453
|
+
.map(template => this.#buildAttachmentNode(
|
|
8337
8454
|
template.innerHTML,
|
|
8338
8455
|
template.getAttribute("content-type") || this.#defaultPromptContentType,
|
|
8339
8456
|
template.getAttribute("sgid") || fallbackSgid
|