lexical-vue
Version:
An extensible Vue 3 web text-editor based on Lexical.
319 lines (318 loc) • 14.1 kB
JavaScript
import { $generateHtmlFromNodes } from "@lexical/html";
import { $isLinkNode } from "@lexical/link";
import { $isMarkNode } from "@lexical/mark";
import { $isTableSelection } from "@lexical/table";
import { $getRoot, $getSelection, $isElementNode, $isNodeSelection, $isParagraphNode, $isRangeSelection, $isTextNode } from "lexical";
const NON_SINGLE_WIDTH_CHARS_REPLACEMENT = Object.freeze({
'\t': '\\t',
'\n': '\\n'
});
const NON_SINGLE_WIDTH_CHARS_REGEX = new RegExp(Object.keys(NON_SINGLE_WIDTH_CHARS_REPLACEMENT).join('|'), 'g');
const SYMBOLS = Object.freeze({
ancestorHasNextSibling: '|',
ancestorIsLastChild: ' ',
hasNextSibling: '├',
isLastChild: '└',
selectedChar: '^',
selectedLine: '>'
});
const FORMAT_PREDICATES = [
(node)=>node.hasFormat('bold') && 'Bold',
(node)=>node.hasFormat('code') && 'Code',
(node)=>node.hasFormat('italic') && 'Italic',
(node)=>node.hasFormat('strikethrough') && 'Strikethrough',
(node)=>node.hasFormat("subscript") && "Subscript",
(node)=>node.hasFormat("superscript") && "Superscript",
(node)=>node.hasFormat('underline') && 'Underline',
(node)=>node.hasFormat('highlight') && 'Highlight'
];
const FORMAT_PREDICATES_PARAGRAPH = [
(node)=>node.hasTextFormat('bold') && 'Bold',
(node)=>node.hasTextFormat('code') && 'Code',
(node)=>node.hasTextFormat('italic') && 'Italic',
(node)=>node.hasTextFormat('strikethrough') && 'Strikethrough',
(node)=>node.hasTextFormat("subscript") && "Subscript",
(node)=>node.hasTextFormat("superscript") && "Superscript",
(node)=>node.hasTextFormat('underline') && 'Underline',
(node)=>node.hasTextFormat('highlight') && 'Highlight'
];
const DETAIL_PREDICATES = [
(node)=>node.isDirectionless() && 'Directionless',
(node)=>node.isUnmergeable() && 'Unmergeable'
];
const MODE_PREDICATES = [
(node)=>node.isToken() && 'Token',
(node)=>node.isSegmented() && 'Segmented'
];
function generateContent(editor, commandsLog, exportDOM, customPrintNode) {
let obfuscateText = arguments.length > 4 && void 0 !== arguments[4] ? arguments[4] : false;
const editorState = editor.getEditorState();
const editorConfig = editor._config;
const compositionKey = editor._compositionKey;
const editable = editor._editable;
if (exportDOM) {
let htmlString = '';
editorState.read(()=>{
htmlString = printPrettyHTML($generateHtmlFromNodes(editor));
});
return htmlString;
}
let res = ' root\n';
const selectionString = editorState.read(()=>{
const selection = $getSelection();
visitTree($getRoot(), (node, indent)=>{
const nodeKey = node.getKey();
const nodeKeyDisplay = "(".concat(nodeKey, ")");
const typeDisplay = node.getType() || '';
const isSelected = node.isSelected();
res += "".concat(isSelected ? SYMBOLS.selectedLine : ' ', " ").concat(indent.join(' '), " ").concat(nodeKeyDisplay, " ").concat(typeDisplay, " ").concat(printNode(node, customPrintNode, obfuscateText), "\n");
res += $printSelectedCharsLine({
indent,
isSelected,
node,
nodeKeyDisplay,
selection,
typeDisplay
});
});
return null === selection ? ': null' : $isRangeSelection(selection) ? printRangeSelection(selection) : $isTableSelection(selection) ? printTableSelection(selection) : printNodeSelection(selection);
});
res += "\n selection".concat(selectionString);
res += '\n\n commands:';
if (commandsLog.length) for (const { index, type, payload } of commandsLog)res += "\n └ ".concat(index, ". { type: ").concat(type, ", payload: ").concat(payload instanceof Event ? payload.constructor.name : payload, " }");
else res += '\n └ None dispatched.';
const { version } = editor.constructor;
res += "\n\n editor".concat(version ? " (v".concat(version, ")") : '', ":");
res += "\n └ namespace ".concat(editorConfig.namespace);
if (null !== compositionKey) res += "\n └ compositionKey ".concat(compositionKey);
res += "\n └ editable ".concat(String(editable));
return res;
}
function printRangeSelection(selection) {
let res = '';
const formatText = printFormatProperties(selection);
res += ": range ".concat('' !== formatText ? "{ ".concat(formatText, " }") : '', " ").concat('' !== selection.style ? "{ style: ".concat(selection.style, " } ") : '');
const anchor = selection.anchor;
const focus = selection.focus;
const anchorOffset = anchor.offset;
const focusOffset = focus.offset;
res += "\n ├ anchor { key: ".concat(anchor.key, ", offset: ").concat(null === anchorOffset ? 'null' : anchorOffset, ", type: ").concat(anchor.type, " }");
res += "\n └ focus { key: ".concat(focus.key, ", offset: ").concat(null === focusOffset ? 'null' : focusOffset, ", type: ").concat(focus.type, " }");
return res;
}
function printNodeSelection(selection) {
if (!$isNodeSelection(selection)) return '';
return ": node\n └ [".concat(Array.from(selection._nodes).join(', '), "]");
}
function printTableSelection(selection) {
return ": table\n └ { table: ".concat(selection.tableKey, ", anchorCell: ").concat(selection.anchor.key, ", focusCell: ").concat(selection.focus.key, " }");
}
function visitTree(currentNode, visitor) {
let indent = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : [];
const childNodes = currentNode.getChildren();
const childNodesLength = childNodes.length;
childNodes.forEach((childNode, i)=>{
visitor(childNode, indent.concat(i === childNodesLength - 1 ? SYMBOLS.isLastChild : SYMBOLS.hasNextSibling));
if ($isElementNode(childNode)) visitTree(childNode, visitor, indent.concat(i === childNodesLength - 1 ? SYMBOLS.ancestorIsLastChild : SYMBOLS.ancestorHasNextSibling));
});
}
function normalize(text) {
let obfuscateText = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : false;
const textToPrint = Object.entries(NON_SINGLE_WIDTH_CHARS_REPLACEMENT).reduce((acc, param)=>{
let [key, value] = param;
return acc.replace(new RegExp(key, 'g'), String(value));
}, text);
if (obfuscateText) return textToPrint.replace(/\S/g, '*');
return textToPrint;
}
function printNode(node, customPrintNode) {
let obfuscateText = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : false;
const customPrint = customPrintNode ? customPrintNode(node, obfuscateText) : void 0;
if (void 0 !== customPrint && customPrint.length > 0) return customPrint;
if ($isTextNode(node)) {
const text = node.getTextContent();
const title = 0 === text.length ? '(empty)' : '"'.concat(normalize(text, obfuscateText), '"');
const properties = printAllTextNodeProperties(node);
return [
title,
0 !== properties.length ? "{ ".concat(properties, " }") : null
].filter(Boolean).join(' ').trim();
}
if ($isLinkNode(node)) {
const link = node.getURL();
const title = 0 === link.length ? '(empty)' : '"'.concat(normalize(link, obfuscateText), '"');
const properties = printAllLinkNodeProperties(node);
return [
title,
0 !== properties.length ? "{ ".concat(properties, " }") : null
].filter(Boolean).join(' ').trim();
}
if ($isMarkNode(node)) return "ids: [ ".concat(node.getIDs().join(', '), " ]");
{
if (!$isParagraphNode(node)) return '';
const formatText = printTextFormatProperties(node);
let paragraphData = '' !== formatText ? "{ ".concat(formatText, " }") : '';
paragraphData += node.__style ? "(".concat(node.__style, ")") : '';
return paragraphData;
}
}
function printTextFormatProperties(nodeOrSelection) {
let str = FORMAT_PREDICATES_PARAGRAPH.map((predicate)=>predicate(nodeOrSelection)).filter(Boolean).join(', ').toLocaleLowerCase();
if ('' !== str) str = "format: ".concat(str);
return str;
}
function printAllTextNodeProperties(node) {
return [
printFormatProperties(node),
printDetailProperties(node),
printModeProperties(node),
printStateProperties(node)
].filter(Boolean).join(', ');
}
function printAllLinkNodeProperties(node) {
return [
printTargetProperties(node),
printRelProperties(node),
printTitleProperties(node),
printStateProperties(node)
].filter(Boolean).join(', ');
}
function printDetailProperties(nodeOrSelection) {
let str = DETAIL_PREDICATES.map((predicate)=>predicate(nodeOrSelection)).filter(Boolean).join(', ').toLocaleLowerCase();
if ('' !== str) str = "detail: ".concat(str);
return str;
}
function printModeProperties(nodeOrSelection) {
let str = MODE_PREDICATES.map((predicate)=>predicate(nodeOrSelection)).filter(Boolean).join(', ').toLocaleLowerCase();
if ('' !== str) str = "mode: ".concat(str);
return str;
}
function printFormatProperties(nodeOrSelection) {
let str = FORMAT_PREDICATES.map((predicate)=>predicate(nodeOrSelection)).filter(Boolean).join(', ').toLocaleLowerCase();
if ('' !== str) str = "format: ".concat(str);
return str;
}
function printTargetProperties(node) {
let str = node.getTarget();
if (null != str) str = "target: ".concat(str);
return str;
}
function printRelProperties(node) {
let str = node.getRel();
if (null != str) str = "rel: ".concat(str);
return str;
}
function printTitleProperties(node) {
let str = node.getTitle();
if (null != str) str = "title: ".concat(str);
return str;
}
function printStateProperties(node) {
if (!node.__state) return false;
const states = [];
for (const [stateType, value] of node.__state.knownState.entries()){
if (stateType.isEqual(value, stateType.defaultValue)) continue;
const textValue = JSON.stringify(stateType.unparse(value));
states.push("[".concat(stateType.key, ": ").concat(textValue, "]"));
}
let str = states.join(',');
if ('' !== str) str = "state: ".concat(str);
return str;
}
function $printSelectedCharsLine(param) {
let { indent, isSelected, node, nodeKeyDisplay, selection, typeDisplay } = param;
if (!$isTextNode(node) || !$isRangeSelection(selection) || !isSelected || $isElementNode(node)) return '';
const anchor = selection.anchor;
const focus = selection.focus;
if ('' === node.getTextContent() || anchor.getNode() === selection.focus.getNode() && anchor.offset === focus.offset) return '';
const [start, end] = $getSelectionStartEnd(node, selection);
if (start === end) return '';
const selectionLastIndent = indent[indent.length - 1] === SYMBOLS.hasNextSibling ? SYMBOLS.ancestorHasNextSibling : SYMBOLS.ancestorIsLastChild;
const indentionChars = [
...indent.slice(0, indent.length - 1),
selectionLastIndent
];
const unselectedChars = new Array(start + 1).fill(' ');
const selectedChars = new Array(end - start).fill(SYMBOLS.selectedChar);
const paddingLength = typeDisplay.length + 2;
const nodePrintSpaces = new Array(nodeKeyDisplay.length + paddingLength).fill(' ');
return "".concat([
SYMBOLS.selectedLine,
indentionChars.join(' '),
[
...nodePrintSpaces,
...unselectedChars,
...selectedChars
].join('')
].join(' '), "\n");
}
function printPrettyHTML(str) {
const div = document.createElement('div');
div.innerHTML = str.trim();
return prettifyHTML(div, 0).innerHTML;
}
function prettifyHTML(node, level) {
const indentBefore = new Array(level++ + 1).join(' ');
const indentAfter = Array.from({
length: level - 1
}).join(' ');
let textNode;
for(let i = 0; i < node.children.length; i++){
textNode = document.createTextNode("\n".concat(indentBefore));
node.insertBefore(textNode, node.children[i]);
prettifyHTML(node.children[i], level);
if (node.lastElementChild === node.children[i]) {
textNode = document.createTextNode("\n".concat(indentAfter));
node.appendChild(textNode);
}
}
return node;
}
function $getSelectionStartEnd(node, selection) {
const anchorAndFocus = selection.getStartEndPoints();
if ($isNodeSelection(selection) || null === anchorAndFocus) return [
-1,
-1
];
const [anchor, focus] = anchorAndFocus;
const textContent = node.getTextContent();
const textLength = textContent.length;
let start = -1;
let end = -1;
if ('text' === anchor.type && 'text' === focus.type) {
const anchorNode = anchor.getNode();
const focusNode = focus.getNode();
if (anchorNode === focusNode && node === anchorNode && anchor.offset !== focus.offset) [start, end] = anchor.offset < focus.offset ? [
anchor.offset,
focus.offset
] : [
focus.offset,
anchor.offset
];
else if (node === anchorNode) [start, end] = anchorNode.isBefore(focusNode) ? [
anchor.offset,
textLength
] : [
0,
anchor.offset
];
else if (node === focusNode) [start, end] = focusNode.isBefore(anchorNode) ? [
focus.offset,
textLength
] : [
0,
focus.offset
];
else [start, end] = [
0,
textLength
];
}
const numNonSingleWidthCharBeforeSelection = (textContent.slice(0, start).match(NON_SINGLE_WIDTH_CHARS_REGEX) || []).length;
const numNonSingleWidthCharInSelection = (textContent.slice(start, end).match(NON_SINGLE_WIDTH_CHARS_REGEX) || []).length;
return [
start + numNonSingleWidthCharBeforeSelection,
end + numNonSingleWidthCharBeforeSelection + numNonSingleWidthCharInSelection
];
}
export { generateContent };