0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-08 16:00:19 -05:00
penpot/frontend/vendor/text_editor_v2.js
2024-10-03 09:51:04 +02:00

2744 lines
86 KiB
JavaScript
Vendored

var __typeError = (msg) => {
throw TypeError(msg);
};
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
var _timeout, _time, _hasPendingChanges, _onTimeout, _rootNode, _currentNode, _added, _removed, _updated, _textEditor, _selection, _ranges, _range, _focusNode, _focusOffset, _anchorNode, _anchorOffset, _savedSelection, _textNodeIterator, _currentStyle, _inertElement, _debug, _mutations, _styleDefaults, _SelectionController_instances, applyDefaultStylesToCurrentStyle_fn, applyStylesToCurrentStyle_fn, updateCurrentStyle_fn, _onSelectionChange, notifyStyleChange_fn, setup_fn, getSavedRange_fn, applyStylesTo_fn, _element, _events, _root, _changeController, _selectionController, _selectionImposterElement, _styleDefaults2, _TextEditor_instances, setupElementProperties_fn, setupRoot_fn, _onChange, _onStyleChange, setup_fn2, createSelectionImposter_fn, _onBlur, _onFocus, _onPaste, _onCut, _onCopy, _onBeforeInput, _onInput, notifyLayout_fn;
function copy(event, editor) {
}
function cut(event, editor) {
}
let canvas = null;
let context = null;
function getContext() {
if (!canvas) {
canvas = createCanvas(1, 1);
}
if (!context) {
context = canvas.getContext("2d");
}
return context;
}
function createCanvas(width, height) {
if ("OffscreenCanvas" in globalThis) {
return new OffscreenCanvas(width, height);
}
return document.createElement("canvas");
}
function getByteAsHex(byte) {
return byte.toString(16).padStart(2, "0");
}
function getColor(fillStyle) {
const context2 = getContext();
context2.fillStyle = fillStyle;
context2.fillRect(0, 0, 1, 1);
const imageData = context2.getImageData(0, 0, 1, 1);
const [r, g, b, a] = imageData.data;
return [`#${getByteAsHex(r)}${getByteAsHex(g)}${getByteAsHex(b)}`, a / 255];
}
function getFills(fillStyle) {
const [color, opacity] = getColor(fillStyle);
return `[["^ ","~:fill-color","${color}","~:fill-opacity",${opacity}]]`;
}
function mergeStyleDeclarations(target, source) {
for (let index = 0; index < source.length; index++) {
const styleName = source.item(index);
target.setProperty(styleName, source.getPropertyValue(styleName));
}
return target;
}
function getComputedStyle(element) {
const inertElement = document.createElement("div");
let currentElement = element;
while (currentElement) {
for (let index = 0; index < currentElement.style.length; index++) {
const styleName = currentElement.style.item(index);
const currentValue = inertElement.style.getPropertyValue(styleName);
if (currentValue) {
const priority = currentElement.style.getPropertyPriority(styleName);
if (priority === "important") {
const newValue = currentElement.style.getPropertyValue(styleName);
inertElement.style.setProperty(styleName, newValue);
}
} else {
inertElement.style.setProperty(
styleName,
currentElement.style.getPropertyValue(styleName)
);
}
}
currentElement = currentElement.parentElement;
}
return inertElement.style;
}
function normalizeStyles(styleDeclaration) {
const color = styleDeclaration.getPropertyValue("color");
if (color) {
styleDeclaration.removeProperty("color");
styleDeclaration.setProperty("--fills", getFills(color));
}
const fontFamily = styleDeclaration.getPropertyValue("font-family");
const fontId = styleDeclaration.getPropertyPriority("--font-id");
if (fontFamily && !fontId) {
styleDeclaration.removeProperty("font-family");
}
return styleDeclaration;
}
function setStyle(element, styleName, styleValue, styleUnit) {
if (styleName.startsWith("--") && typeof styleValue !== "string" && typeof styleValue !== "number") {
if (styleName === "--fills" && styleValue === null) debugger;
element.style.setProperty(styleName, JSON.stringify(styleValue));
} else {
element.style.setProperty(styleName, styleValue + (styleUnit ?? ""));
}
return element;
}
function getStyleFromDeclaration(style, styleName, styleUnit) {
if (styleName.startsWith("--")) {
return style.getPropertyValue(styleName);
}
const styleValue = style.getPropertyValue(styleName);
if (styleValue.endsWith(styleUnit)) {
return styleValue.slice(0, -styleUnit.length);
}
return styleValue;
}
function setStylesFromObject(element, allowedStyles, styleObject) {
for (const [styleName, styleUnit] of allowedStyles) {
if (!(styleName in styleObject)) {
continue;
}
const styleValue = styleObject[styleName];
if (styleValue) {
setStyle(element, styleName, styleValue, styleUnit);
}
}
return element;
}
function setStylesFromDeclaration(element, allowedStyles, styleDeclaration) {
for (const [styleName, styleUnit] of allowedStyles) {
const styleValue = getStyleFromDeclaration(styleDeclaration, styleName, styleUnit);
if (styleValue) {
setStyle(element, styleName, styleValue, styleUnit);
}
}
return element;
}
function setStyles(element, allowedStyles, styleObjectOrDeclaration) {
if (styleObjectOrDeclaration instanceof CSSStyleDeclaration) {
return setStylesFromDeclaration(
element,
allowedStyles,
styleObjectOrDeclaration
);
}
return setStylesFromObject(element, allowedStyles, styleObjectOrDeclaration);
}
function mergeStyles(allowedStyles, styleDeclaration, newStyles) {
const mergedStyles = {};
for (const [styleName, styleUnit] of allowedStyles) {
if (styleName in newStyles) {
mergedStyles[styleName] = newStyles[styleName];
} else {
mergedStyles[styleName] = getStyleFromDeclaration(styleDeclaration, styleName, styleUnit);
}
}
return mergedStyles;
}
function isDisplayBlock(style) {
return style.display === "block";
}
function createRandomId() {
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(36);
}
function createElement(tag, options) {
const element = document.createElement(tag);
if (options == null ? void 0 : options.attributes) {
Object.entries(options.attributes).forEach(
([name, value]) => element.setAttribute(name, value)
);
}
if (options == null ? void 0 : options.data) {
Object.entries(options.data).forEach(
([name, value]) => element.dataset[name] = value
);
}
if ((options == null ? void 0 : options.styles) && (options == null ? void 0 : options.allowedStyles)) {
setStyles(element, options.allowedStyles, options.styles);
}
if (options == null ? void 0 : options.children) {
if (Array.isArray(options.children)) {
element.append(...options.children);
} else {
element.appendChild(options.children);
}
}
return element;
}
function isElement(element, nodeName) {
return element.nodeType === Node.ELEMENT_NODE && element.nodeName === nodeName.toUpperCase();
}
function isOffsetAtStart(node, offset) {
return offset === 0;
}
function isOffsetAtEnd(node, offset) {
if (node.nodeType === Node.TEXT_NODE) {
return node.nodeValue.length === offset;
}
return true;
}
const TAG$3 = "BR";
function createLineBreak() {
return document.createElement(TAG$3);
}
function isLineBreak(node) {
return node.nodeType === Node.ELEMENT_NODE && node.nodeName === TAG$3;
}
const TAG$2 = "SPAN";
const TYPE$2 = "inline";
const QUERY$1 = `[data-itype="${TYPE$2}"]`;
const STYLES$2 = [
["--typography-ref-id"],
["--typography-ref-file"],
["--font-id"],
["--font-variant-id"],
["--fills"],
["font-variant"],
["font-family"],
["font-size", "px"],
["font-weight"],
["font-style"],
["line-height"],
["letter-spacing", "px"],
["text-decoration"],
["text-transform"]
];
function isInline(node) {
if (!node) return false;
if (!isElement(node, TAG$2)) return false;
if (node.dataset.itype !== TYPE$2) return false;
return true;
}
function isLikeInline(element) {
return element ? [
"A",
"ABBR",
"ACRONYM",
"B",
"BDO",
"BIG",
"BR",
"BUTTON",
"CITE",
"CODE",
"DFN",
"EM",
"I",
"IMG",
"INPUT",
"KBD",
"LABEL",
"MAP",
"OBJECT",
"OUTPUT",
"Q",
"SAMP",
"SCRIPT",
"SELECT",
"SMALL",
"SPAN",
"STRONG",
"SUB",
"SUP",
"TEXTAREA",
"TIME",
"TT",
"VAR"
].includes(element.nodeName) : false;
}
function createInline(textOrLineBreak, styles, attrs) {
if (!(textOrLineBreak instanceof HTMLBRElement) && !(textOrLineBreak instanceof Text)) {
throw new TypeError("Invalid inline child");
}
if (textOrLineBreak instanceof Text && textOrLineBreak.nodeValue.length === 0) {
console.trace("nodeValue", textOrLineBreak.nodeValue);
throw new TypeError("Invalid inline child, cannot be an empty text");
}
return createElement(TAG$2, {
attributes: { id: createRandomId(), ...attrs },
data: { itype: TYPE$2 },
styles,
allowedStyles: STYLES$2,
children: textOrLineBreak
});
}
function createInlineFrom(inline, textOrLineBreak, styles, attrs) {
return createInline(
textOrLineBreak,
mergeStyles(STYLES$2, inline.style, styles),
attrs
);
}
function createEmptyInline(styles) {
return createInline(createLineBreak(), styles);
}
function setInlineStyles(element, styles) {
return setStyles(element, STYLES$2, styles);
}
function getInline(node) {
if (!node) return null;
if (isInline(node)) return node;
if (node.nodeType === Node.TEXT_NODE) {
const inline = node == null ? void 0 : node.parentElement;
if (!inline) return null;
if (!isInline(inline)) return null;
return inline;
}
return node.closest(QUERY$1);
}
function isInlineStart(node, offset) {
const inline = getInline(node);
if (!inline) return false;
return isOffsetAtStart(inline, offset);
}
function isInlineEnd(node, offset) {
const inline = getInline(node);
if (!inline) return false;
return isOffsetAtEnd(inline.firstChild, offset);
}
function splitInline(inline, offset) {
const textNode = inline.firstChild;
const style = inline.style;
const newTextNode = textNode.splitText(offset);
return createInline(newTextNode, style);
}
function getInlinesFrom(startInline) {
const inlines = [];
let currentInline = startInline;
let index = 0;
while (currentInline) {
if (index > 0) inlines.push(currentInline);
currentInline = currentInline.nextElementSibling;
index++;
}
return inlines;
}
function getInlineLength(inline) {
if (!isInline(inline)) throw new Error("Invalid inline");
if (isLineBreak(inline.firstChild)) return 0;
return inline.firstChild.nodeValue.length;
}
const TAG$1 = "DIV";
const TYPE$1 = "root";
const STYLES$1 = [["--vertical-align"]];
function isRoot(node) {
if (!node) return false;
if (!isElement(node, TAG$1)) return false;
if (node.dataset.itype !== TYPE$1) return false;
return true;
}
function createRoot(paragraphs, styles, attrs) {
if (!Array.isArray(paragraphs) || !paragraphs.every(isParagraph))
throw new TypeError("Invalid root children");
return createElement(TAG$1, {
attributes: { id: createRandomId(), ...attrs },
data: { itype: TYPE$1 },
styles,
allowedStyles: STYLES$1,
children: paragraphs
});
}
function createEmptyRoot(styles) {
return createRoot([createEmptyParagraph(styles)], styles);
}
function setRootStyles(element, styles) {
return setStyles(element, STYLES$1, styles);
}
function isTextNode(node) {
if (!node) throw new TypeError("Invalid text node");
return node.nodeType === Node.TEXT_NODE || isLineBreak(node);
}
function getTextNodeLength(node) {
if (!node) throw new TypeError("Invalid text node");
if (isLineBreak(node)) return 0;
return node.nodeValue.length;
}
function getClosestTextNode(node) {
if (isTextNode(node)) return node;
if (isInline(node)) return node.firstChild;
if (isParagraph(node)) return node.firstChild.firstChild;
if (isRoot(node)) return node.firstChild.firstChild.firstChild;
throw new Error("Cannot find a text node");
}
const TAG = "DIV";
const TYPE = "paragraph";
const QUERY = `[data-itype="${TYPE}"]`;
const STYLES = [
["--typography-ref-id"],
["--typography-ref-file"],
["--font-id"],
["--font-variant-id"],
["--fills"],
["font-variant"],
["font-family"],
["font-size", "px"],
["font-weight"],
["font-style"],
["line-height"],
["letter-spacing", "px"],
["text-decoration"],
["text-transform"],
["text-align"],
["direction"]
];
function isLikeParagraph(element) {
return !isLikeInline(element);
}
function isEmptyParagraph(element) {
if (!isParagraph(element)) throw new TypeError("Invalid paragraph");
const inline = element.firstChild;
if (!isInline(inline)) throw new TypeError("Invalid inline");
return isLineBreak(inline.firstChild);
}
function isParagraph(node) {
if (!node) return false;
if (!isElement(node, TAG)) return false;
if (node.dataset.itype !== TYPE) return false;
return true;
}
function createParagraph(inlines, styles, attrs) {
if (inlines && (!Array.isArray(inlines) || !inlines.every(isInline)))
throw new TypeError("Invalid paragraph children");
return createElement(TAG, {
attributes: { id: createRandomId(), ...attrs },
data: { itype: TYPE },
styles,
allowedStyles: STYLES,
children: inlines
});
}
function createEmptyParagraph(styles) {
return createParagraph([
createEmptyInline(styles)
], styles);
}
function setParagraphStyles(element, styles) {
return setStyles(element, STYLES, styles);
}
function getParagraph(node) {
var _a;
if (!node) return null;
if (isParagraph(node)) return node;
if (node.nodeType === Node.TEXT_NODE || isLineBreak(node)) {
const paragraph = (_a = node == null ? void 0 : node.parentElement) == null ? void 0 : _a.parentElement;
if (!paragraph) {
return null;
}
if (!isParagraph(paragraph)) {
return null;
}
return paragraph;
}
return node.closest(QUERY);
}
function isParagraphStart(node, offset) {
const paragraph = getParagraph(node);
if (!paragraph) throw new Error("Can't find the paragraph");
const inline = getInline(node);
if (!inline) throw new Error("Can't find the inline");
return paragraph.firstElementChild === inline && isOffsetAtStart(inline.firstChild, offset);
}
function isParagraphEnd(node, offset) {
const paragraph = getParagraph(node);
if (!paragraph) throw new Error("Cannot find the paragraph");
const inline = getInline(node);
if (!inline) throw new Error("Cannot find the inline");
return paragraph.lastElementChild === inline && isOffsetAtEnd(inline.firstChild, offset);
}
function splitParagraph(paragraph, inline, offset) {
const style = paragraph.style;
if (isInlineEnd(inline, offset)) {
const newParagraph2 = createParagraph(getInlinesFrom(inline), style);
return newParagraph2;
}
const newInline = splitInline(inline, offset);
const newParagraph = createParagraph([newInline], style);
return newParagraph;
}
function mergeParagraphs(a, b) {
a.append(...b.children);
b.remove();
return a;
}
function mapContentFragmentFromDocument(document2, root, styleDefaults) {
const nodeIterator = document2.createNodeIterator(root, NodeFilter.SHOW_TEXT);
const fragment = document2.createDocumentFragment();
let currentParagraph = null;
let currentNode = nodeIterator.nextNode();
while (currentNode) {
const parentStyle = normalizeStyles(mergeStyleDeclarations(styleDefaults, getComputedStyle(currentNode.parentElement)));
if (isDisplayBlock(currentNode.parentElement.style) || isDisplayBlock(parentStyle) || isLikeParagraph(currentNode.parentElement)) {
if (currentParagraph) {
fragment.appendChild(currentParagraph);
}
currentParagraph = createParagraph(void 0, parentStyle);
} else {
if (currentParagraph === null) {
currentParagraph = createParagraph();
}
}
currentParagraph.appendChild(
createInline(new Text(currentNode.nodeValue), parentStyle)
);
currentNode = nodeIterator.nextNode();
}
fragment.appendChild(currentParagraph);
return fragment;
}
function mapContentFragmentFromHTML(html, styleDefaults) {
const parser = new DOMParser();
const htmlDocument = parser.parseFromString(html, "text/html");
return mapContentFragmentFromDocument(
htmlDocument,
htmlDocument.documentElement,
styleDefaults
);
}
function mapContentFragmentFromString(string, styleDefaults) {
const lines = string.replace(/\r/g, "").split("\n");
const fragment = document.createDocumentFragment();
for (const line of lines) {
if (line === "") {
fragment.appendChild(createEmptyParagraph(styleDefaults));
} else {
fragment.appendChild(createParagraph([createInline(new Text(line), styleDefaults)], styleDefaults));
}
}
return fragment;
}
function paste(event, editor, selectionController) {
event.preventDefault();
let fragment = null;
if (event.clipboardData.types.includes("text/html")) {
const html = event.clipboardData.getData("text/html");
fragment = mapContentFragmentFromHTML(html, selectionController.currentStyle);
} else if (event.clipboardData.types.includes("text/plain")) {
const plain = event.clipboardData.getData("text/plain");
fragment = mapContentFragmentFromString(plain, selectionController.currentStyle);
}
if (!fragment) {
return;
}
if (selectionController.isCollapsed) {
selectionController.insertPaste(fragment);
} else {
selectionController.replaceWithPaste(fragment);
}
}
const clipboard = {
copy,
cut,
paste
};
function insertText(event, editor, selectionController) {
event.preventDefault();
if (selectionController.isCollapsed) {
if (selectionController.isTextFocus) {
return selectionController.insertText(event.data);
} else if (selectionController.isLineBreakFocus) {
return selectionController.replaceLineBreak(event.data);
}
} else {
if (selectionController.isMultiParagraph) {
return selectionController.replaceParagraphs(event.data);
} else if (selectionController.isMultiInline) {
return selectionController.replaceInlines(event.data);
} else if (selectionController.isTextSame) {
return selectionController.replaceText(event.data);
}
}
}
function insertParagraph(event, editor, selectionController) {
event.preventDefault();
if (selectionController.isCollapsed) {
return selectionController.insertParagraph();
}
return selectionController.replaceWithParagraph();
}
function deleteByCut(event, editor, selectionController) {
event.preventDefault();
if (selectionController.isCollapsed) {
throw new Error("This should be impossible");
}
return selectionController.removeSelected();
}
function deleteContentBackward(event, editor, selectionController) {
event.preventDefault();
if (editor.isEmpty) return;
if (!selectionController.isCollapsed) {
return selectionController.removeSelected({ direction: "backward" });
}
if (selectionController.isTextFocus && selectionController.focusOffset > 0) {
return selectionController.removeBackwardText();
} else if (selectionController.isTextFocus && selectionController.focusAtStart) {
return selectionController.mergeBackwardParagraph();
} else if (selectionController.isInlineFocus || selectionController.isLineBreakFocus) {
return selectionController.removeBackwardParagraph();
}
}
function deleteContentForward(event, editor, selectionController) {
event.preventDefault();
if (editor.isEmpty) return;
if (!selectionController.isCollapsed) {
return selectionController.removeSelected({ direction: "forward" });
}
if (selectionController.isTextFocus && selectionController.focusOffset >= 0) {
return selectionController.removeForwardText();
} else if (selectionController.isTextFocus && selectionController.focusAtEnd) {
return selectionController.mergeForwardParagraph();
} else if ((selectionController.isInlineFocus || selectionController.isLineBreakFocus) && editor.numParagraphs > 1) {
return selectionController.removeForwardParagraph();
}
}
const commands = {
insertText,
insertParagraph,
deleteByCut,
deleteContentBackward,
deleteContentForward
};
class ChangeController extends EventTarget {
/**
* Constructor
*
* @param {number} [time=500]
*/
constructor(time = 500) {
super();
/**
* Keeps the timeout id.
*
* @type {number}
*/
__privateAdd(this, _timeout, null);
/**
* Keeps the time at which we're going to
* call the debounced change calls.
*
* @type {number}
*/
__privateAdd(this, _time, 1e3);
/**
* Keeps if we have some pending changes or not.
*
* @type {boolean}
*/
__privateAdd(this, _hasPendingChanges, false);
__privateAdd(this, _onTimeout, () => {
this.dispatchEvent(new Event("change"));
});
if (typeof time === "number" && (!Number.isInteger(time) || time <= 0)) {
throw new TypeError("Invalid time");
}
__privateSet(this, _time, time ?? 500);
}
/**
* Indicates that there are some pending changes.
*
* @type {boolean}
*/
get hasPendingChanges() {
return __privateGet(this, _hasPendingChanges);
}
/**
* Tells the ChangeController that a change has been made
* but that you need to delay the notification (and debounce)
* for sometime.
*/
notifyDebounced() {
__privateSet(this, _hasPendingChanges, true);
clearTimeout(__privateGet(this, _timeout));
__privateSet(this, _timeout, setTimeout(__privateGet(this, _onTimeout), __privateGet(this, _time)));
}
/**
* Tells the ChangeController that a change should be notified
* immediately.
*/
notifyImmediately() {
clearTimeout(__privateGet(this, _timeout));
__privateGet(this, _onTimeout).call(this);
}
/**
* Disposes the referenced resources.
*/
dispose() {
if (this.hasPendingChanges) {
this.notifyImmediately();
}
clearTimeout(__privateGet(this, _timeout));
}
}
_timeout = new WeakMap();
_time = new WeakMap();
_hasPendingChanges = new WeakMap();
_onTimeout = new WeakMap();
function tryOffset(offset) {
if (!Number.isInteger(offset) || offset < 0)
throw new TypeError("Invalid offset");
}
function tryString(str) {
if (typeof str !== "string") throw new TypeError("Invalid string");
}
function insertInto(str, offset, text) {
tryString(str);
tryOffset(offset);
tryString(text);
return str.slice(0, offset) + text + str.slice(offset);
}
function replaceWith(str, startOffset, endOffset, text) {
tryString(str);
tryOffset(startOffset);
tryOffset(endOffset);
tryString(text);
return str.slice(0, startOffset) + text + str.slice(endOffset);
}
function removeBackward(str, offset) {
tryString(str);
tryOffset(offset);
if (offset === 0) {
return str;
}
return str.slice(0, offset - 1) + str.slice(offset);
}
function removeForward(str, offset) {
tryString(str);
tryOffset(offset);
return str.slice(0, offset) + str.slice(offset + 1);
}
function removeSlice(str, start2, end) {
tryString(str);
tryOffset(start2);
tryOffset(end);
return str.slice(0, start2) + str.slice(end);
}
const TextNodeIteratorDirection = {
FORWARD: 1,
BACKWARD: 0
};
const _TextNodeIterator = class _TextNodeIterator {
/**
* Constructor
*
* @param {HTMLElement} rootNode
*/
constructor(rootNode) {
/**
* This is the root text node.
*
* @type {HTMLElement}
*/
__privateAdd(this, _rootNode, null);
/**
* This is the current text node.
*
* @type {Text|null}
*/
__privateAdd(this, _currentNode, null);
if (!(rootNode instanceof HTMLElement)) {
throw new TypeError("Invalid root node");
}
__privateSet(this, _rootNode, rootNode);
__privateSet(this, _currentNode, _TextNodeIterator.findDown(rootNode, rootNode));
}
/**
* Returns if a specific node is a text node.
*
* @param {Node} node
* @returns {boolean}
*/
static isTextNode(node) {
return node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ELEMENT_NODE && node.nodeName === "BR";
}
/**
* Returns if a specific node is a container node.
*
* @param {Node} node
* @returns {boolean}
*/
static isContainerNode(node) {
return node.nodeType === Node.ELEMENT_NODE && node.nodeName !== "BR";
}
/**
* Finds a node from an initial node and down the tree.
*
* @param {Node} startNode
* @param {Node} rootNode
* @param {Set<Node>} skipNodes
* @param {number} direction
* @returns {Node}
*/
static findDown(startNode, rootNode, skipNodes = /* @__PURE__ */ new Set(), direction = TextNodeIteratorDirection.FORWARD) {
if (startNode === rootNode) {
return _TextNodeIterator.findDown(
direction === TextNodeIteratorDirection.FORWARD ? startNode.firstChild : startNode.lastChild,
rootNode,
skipNodes,
direction
);
}
let safeGuard = Date.now();
let currentNode = startNode;
while (currentNode) {
if (Date.now() - safeGuard >= 1e3) {
throw new Error("Iteration timeout");
}
if (skipNodes.has(currentNode)) {
currentNode = direction === TextNodeIteratorDirection.FORWARD ? currentNode.nextSibling : currentNode.previousSibling;
continue;
}
if (_TextNodeIterator.isTextNode(currentNode)) {
return currentNode;
} else if (_TextNodeIterator.isContainerNode(currentNode)) {
return _TextNodeIterator.findDown(
direction === TextNodeIteratorDirection.FORWARD ? currentNode.firstChild : currentNode.lastChild,
rootNode,
skipNodes,
direction
);
}
currentNode = direction === TextNodeIteratorDirection.FORWARD ? currentNode.nextSibling : currentNode.previousSibling;
}
return null;
}
/**
* Finds a node from an initial node and up the tree.
*
* @param {Node} startNode
* @param {Node} rootNode
* @param {Set} backTrack
* @param {number} direction
* @returns {Node}
*/
static findUp(startNode, rootNode, backTrack = /* @__PURE__ */ new Set(), direction = TextNodeIteratorDirection.FORWARD) {
backTrack.add(startNode);
if (_TextNodeIterator.isTextNode(startNode)) {
return _TextNodeIterator.findUp(
startNode.parentNode,
rootNode,
backTrack,
direction
);
} else if (_TextNodeIterator.isContainerNode(startNode)) {
const found = _TextNodeIterator.findDown(
startNode,
rootNode,
backTrack,
direction
);
if (found) {
return found;
}
if (startNode !== rootNode) {
return _TextNodeIterator.findUp(
startNode.parentNode,
rootNode,
backTrack,
direction
);
}
}
return null;
}
/**
* Current node we're into.
*
* @type {TextNode|HTMLBRElement}
*/
get currentNode() {
return __privateGet(this, _currentNode);
}
set currentNode(newCurrentNode) {
const isContained = (newCurrentNode.compareDocumentPosition(__privateGet(this, _rootNode)) & Node.DOCUMENT_POSITION_CONTAINS) === Node.DOCUMENT_POSITION_CONTAINS;
if (!(newCurrentNode instanceof Node) || !_TextNodeIterator.isTextNode(newCurrentNode) || !isContained) {
throw new TypeError("Invalid new current node");
}
__privateSet(this, _currentNode, newCurrentNode);
}
/**
* Returns the next Text node or <br> element or null if there are.
*
* @returns {Text|HTMLBRElement}
*/
nextNode() {
if (!__privateGet(this, _currentNode)) return null;
const nextNode = _TextNodeIterator.findUp(
__privateGet(this, _currentNode),
__privateGet(this, _rootNode),
/* @__PURE__ */ new Set(),
TextNodeIteratorDirection.FORWARD
);
if (!nextNode) {
return null;
}
__privateSet(this, _currentNode, nextNode);
return __privateGet(this, _currentNode);
}
/**
* Returns the previous Text node or <br> element or null.
*
* @returns {Text|HTMLBRElement}
*/
previousNode() {
if (!__privateGet(this, _currentNode)) return null;
const previousNode = _TextNodeIterator.findUp(
__privateGet(this, _currentNode),
__privateGet(this, _rootNode),
/* @__PURE__ */ new Set(),
TextNodeIteratorDirection.BACKWARD
);
if (!previousNode) {
return null;
}
__privateSet(this, _currentNode, previousNode);
return __privateGet(this, _currentNode);
}
};
_rootNode = new WeakMap();
_currentNode = new WeakMap();
let TextNodeIterator = _TextNodeIterator;
class CommandMutations {
constructor(added, updated, removed) {
__privateAdd(this, _added, /* @__PURE__ */ new Set());
__privateAdd(this, _removed, /* @__PURE__ */ new Set());
__privateAdd(this, _updated, /* @__PURE__ */ new Set());
if (added && Array.isArray(added)) __privateSet(this, _added, new Set(added));
if (updated && Array.isArray(updated)) __privateSet(this, _updated, new Set(updated));
if (removed && Array.isArray(removed)) __privateSet(this, _removed, new Set(removed));
}
get added() {
return __privateGet(this, _added);
}
get removed() {
return __privateGet(this, _removed);
}
get updated() {
return __privateGet(this, _updated);
}
clear() {
__privateGet(this, _added).clear();
__privateGet(this, _removed).clear();
__privateGet(this, _updated).clear();
}
dispose() {
__privateGet(this, _added).clear();
__privateSet(this, _added, null);
__privateGet(this, _removed).clear();
__privateSet(this, _removed, null);
__privateGet(this, _updated).clear();
__privateSet(this, _updated, null);
}
add(node) {
__privateGet(this, _added).add(node);
return this;
}
remove(node) {
__privateGet(this, _removed).add(node);
return this;
}
update(node) {
__privateGet(this, _updated).add(node);
return this;
}
}
_added = new WeakMap();
_removed = new WeakMap();
_updated = new WeakMap();
const SelectionDirection = {
/** The anchorNode is behind the focusNode */
FORWARD: 1,
/** The focusNode and the anchorNode are collapsed */
NONE: 0,
/** The focusNode is behind the anchorNode */
BACKWARD: -1
};
const SAFE_GUARD_TIME = 1e3;
let startTime = Date.now();
function start() {
startTime = Date.now();
}
function update() {
if (Date.now - startTime >= SAFE_GUARD_TIME) {
throw new Error("Safe guard timeout");
}
}
const SafeGuard = {
start,
update
};
class SelectionController extends EventTarget {
/**
* Constructor
*
* @param {TextEditor} textEditor
* @param {Selection} selection
* @param {SelectionControllerOptions} [options]
*/
constructor(textEditor, selection, options) {
super();
__privateAdd(this, _SelectionController_instances);
/**
* Reference to the text editor.
*
* @type {TextEditor}
*/
__privateAdd(this, _textEditor, null);
/**
* Selection.
*
* @type {Selection}
*/
__privateAdd(this, _selection, null);
/**
* Set of ranges (this should always have one)
*
* @type {Set<Range>}
*/
__privateAdd(this, _ranges, /* @__PURE__ */ new Set());
/**
* Current range (.rangeAt 0)
*
* @type {Range}
*/
__privateAdd(this, _range, null);
/**
* @type {Node}
*/
__privateAdd(this, _focusNode, null);
/**
* @type {number}
*/
__privateAdd(this, _focusOffset, 0);
/**
* @type {Node}
*/
__privateAdd(this, _anchorNode, null);
/**
* @type {number}
*/
__privateAdd(this, _anchorOffset, 0);
/**
* Saved selection.
*
* @type {object}
*/
__privateAdd(this, _savedSelection, null);
/**
* TextNodeIterator that allows us to move
* around the root element but only through
* <br> and #text nodes.
*
* @type {TextNodeIterator}
*/
__privateAdd(this, _textNodeIterator, null);
/**
* CSSStyleDeclaration that we can mutate
* to handle style changes.
*
* @type {CSSStyleDeclaration}
*/
__privateAdd(this, _currentStyle, null);
/**
* Element used to have a custom CSSStyleDeclaration
* that we can modify to handle style changes when the
* selection is changed.
*
* @type {HTMLDivElement}
*/
__privateAdd(this, _inertElement, null);
/**
* @type {SelectionControllerDebug}
*/
__privateAdd(this, _debug, null);
/**
* Command Mutations.
*
* @type {CommandMutations}
*/
__privateAdd(this, _mutations, new CommandMutations());
/**
* Style defaults.
*
* @type {Object.<string, *>}
*/
__privateAdd(this, _styleDefaults, null);
/**
* This is called on every `selectionchange` because it is dispatched
* only by the `document` object.
*
* @param {Event} e
*/
__privateAdd(this, _onSelectionChange, (e) => {
if (!this.hasFocus) return;
let focusNodeChanges = false;
if (__privateGet(this, _focusNode) !== __privateGet(this, _selection).focusNode) {
__privateSet(this, _focusNode, __privateGet(this, _selection).focusNode);
focusNodeChanges = true;
}
__privateSet(this, _focusOffset, __privateGet(this, _selection).focusOffset);
if (__privateGet(this, _anchorNode) !== __privateGet(this, _selection).anchorNode) {
__privateSet(this, _anchorNode, __privateGet(this, _selection).anchorNode);
}
__privateSet(this, _anchorOffset, __privateGet(this, _selection).anchorOffset);
if (__privateGet(this, _selection).rangeCount > 1) {
for (let index = 0; index < __privateGet(this, _selection).rangeCount; index++) {
const range = __privateGet(this, _selection).getRangeAt(index);
if (__privateGet(this, _ranges).has(range)) {
__privateGet(this, _ranges).delete(range);
__privateGet(this, _selection).removeRange(range);
} else {
__privateGet(this, _ranges).add(range);
__privateSet(this, _range, range);
}
}
} else if (__privateGet(this, _selection).rangeCount > 0) {
const range = __privateGet(this, _selection).getRangeAt(0);
__privateSet(this, _range, range);
__privateGet(this, _ranges).clear();
__privateGet(this, _ranges).add(range);
} else {
__privateSet(this, _range, null);
__privateGet(this, _ranges).clear();
}
if (focusNodeChanges) {
__privateMethod(this, _SelectionController_instances, notifyStyleChange_fn).call(this);
}
if (__privateGet(this, _debug)) {
__privateGet(this, _debug).update(this);
}
});
__privateSet(this, _debug, options == null ? void 0 : options.debug);
__privateSet(this, _styleDefaults, options == null ? void 0 : options.styleDefaults);
__privateSet(this, _selection, selection);
__privateSet(this, _textEditor, textEditor);
__privateSet(this, _textNodeIterator, new TextNodeIterator(__privateGet(this, _textEditor).element));
__privateMethod(this, _SelectionController_instances, setup_fn).call(this);
}
/**
* Styles of the current inline.
*
* @type {CSSStyleDeclaration}
*/
get currentStyle() {
return __privateGet(this, _currentStyle);
}
/**
* Saves the current selection and returns the client rects.
*
* @returns {boolean}
*/
saveSelection() {
__privateSet(this, _savedSelection, {
isCollapsed: __privateGet(this, _selection).isCollapsed,
focusNode: __privateGet(this, _selection).focusNode,
focusOffset: __privateGet(this, _selection).focusOffset,
anchorNode: __privateGet(this, _selection).anchorNode,
anchorOffset: __privateGet(this, _selection).anchorOffset,
range: __privateMethod(this, _SelectionController_instances, getSavedRange_fn).call(this)
});
return true;
}
/**
* Restores a saved selection if there's any.
*
* @returns {boolean}
*/
restoreSelection() {
if (!__privateGet(this, _savedSelection)) return false;
if (__privateGet(this, _savedSelection).anchorNode && __privateGet(this, _savedSelection).focusNode) {
if (__privateGet(this, _savedSelection).anchorNode === __privateGet(this, _savedSelection).focusNode) {
__privateGet(this, _selection).setPosition(__privateGet(this, _savedSelection).focusNode, __privateGet(this, _savedSelection).focusOffset);
} else {
__privateGet(this, _selection).setBaseAndExtent(
__privateGet(this, _savedSelection).anchorNode,
__privateGet(this, _savedSelection).anchorOffset,
__privateGet(this, _savedSelection).focusNode,
__privateGet(this, _savedSelection).focusOffset
);
}
}
__privateSet(this, _savedSelection, null);
return true;
}
/**
* Marks the start of a mutation.
*
* Clears all the mutations kept in CommandMutations.
*/
startMutation() {
__privateGet(this, _mutations).clear();
if (!__privateGet(this, _focusNode)) return false;
return true;
}
/**
* Marks the end of a mutation.
*
* @returns
*/
endMutation() {
return __privateGet(this, _mutations);
}
/**
* Selects all content.
*/
selectAll() {
__privateGet(this, _selection).selectAllChildren(__privateGet(this, _textEditor).root);
return this;
}
/**
* Moves cursor to end.
*/
cursorToEnd() {
const range = document.createRange();
range.selectNodeContents(__privateGet(this, _textEditor).element);
range.collapse(false);
__privateGet(this, _selection).removeAllRanges();
__privateGet(this, _selection).addRange(range);
return this;
}
/**
* Collapses a selection.
*
* @param {Node} node
* @param {number} offset
*/
collapse(node, offset) {
const nodeOffset = node.nodeType === Node.TEXT_NODE && offset >= node.nodeValue.length ? node.nodeValue.length : offset;
return this.setSelection(
node,
nodeOffset,
node,
nodeOffset
);
}
/**
* Sets base and extent.
*
* @param {Node} anchorNode
* @param {number} anchorOffset
* @param {Node} [focusNode=anchorNode]
* @param {number} [focusOffset=anchorOffset]
*/
setSelection(anchorNode, anchorOffset, focusNode = anchorNode, focusOffset = anchorOffset) {
if (!anchorNode.isConnected) {
throw new Error("Invalid anchorNode");
}
if (!focusNode.isConnected) {
throw new Error("Invalid focusNode");
}
if (__privateGet(this, _savedSelection)) {
__privateGet(this, _savedSelection).isCollapsed = focusNode === anchorNode && anchorOffset === focusOffset;
__privateGet(this, _savedSelection).focusNode = focusNode;
__privateGet(this, _savedSelection).focusOffset = focusOffset;
__privateGet(this, _savedSelection).anchorNode = anchorNode;
__privateGet(this, _savedSelection).anchorOffset = anchorOffset;
__privateGet(this, _savedSelection).range.collapsed = __privateGet(this, _savedSelection).isCollapsed;
const position = focusNode.compareDocumentPosition(anchorNode);
if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
__privateGet(this, _savedSelection).range.startContainer = focusNode;
__privateGet(this, _savedSelection).range.startOffset = focusOffset;
__privateGet(this, _savedSelection).range.endContainer = anchorNode;
__privateGet(this, _savedSelection).range.endOffset = anchorOffset;
} else {
__privateGet(this, _savedSelection).range.startContainer = anchorNode;
__privateGet(this, _savedSelection).range.startOffset = anchorOffset;
__privateGet(this, _savedSelection).range.endContainer = focusNode;
__privateGet(this, _savedSelection).range.endOffset = focusOffset;
}
} else {
__privateSet(this, _anchorNode, anchorNode);
__privateSet(this, _anchorOffset, anchorOffset);
if (anchorNode === focusNode) {
__privateSet(this, _focusNode, __privateGet(this, _anchorNode));
__privateSet(this, _focusOffset, __privateGet(this, _anchorOffset));
__privateGet(this, _selection).setPosition(anchorNode, anchorOffset);
} else {
__privateSet(this, _focusNode, focusNode);
__privateSet(this, _focusOffset, focusOffset);
__privateGet(this, _selection).setBaseAndExtent(
anchorNode,
anchorOffset,
focusNode,
focusOffset
);
}
}
}
/**
* Disposes the current resources.
*/
dispose() {
document.removeEventListener("selectionchange", __privateGet(this, _onSelectionChange));
__privateSet(this, _textEditor, null);
__privateGet(this, _ranges).clear();
__privateSet(this, _ranges, null);
__privateSet(this, _range, null);
__privateSet(this, _selection, null);
__privateSet(this, _focusNode, null);
__privateSet(this, _anchorNode, null);
__privateGet(this, _mutations).dispose();
__privateSet(this, _mutations, null);
}
/**
* Returns the current selection.
*
* @type {Selection}
*/
get selection() {
return __privateGet(this, _selection);
}
/**
* Returns the current range.
*
* @type {Range}
*/
get range() {
return __privateGet(this, _range);
}
/**
* Indicates the direction of the selection
*
* @type {SelectionDirection}
*/
get direction() {
if (this.isCollapsed) {
return SelectionDirection.NONE;
}
if (this.focusNode !== this.anchorNode) {
return this.startContainer === this.focusNode ? SelectionDirection.BACKWARD : SelectionDirection.FORWARD;
}
return this.focusOffset < this.anchorOffset ? SelectionDirection.BACKWARD : SelectionDirection.FORWARD;
}
/**
* Indicates that the editor element has the
* focus.
*
* @type {boolean}
*/
get hasFocus() {
return document.activeElement === __privateGet(this, _textEditor).element;
}
/**
* Returns true if the selection is collapsed (caret)
* or false otherwise.
*
* @type {boolean}
*/
get isCollapsed() {
if (__privateGet(this, _savedSelection)) {
return __privateGet(this, _savedSelection).isCollapsed;
}
return __privateGet(this, _selection).isCollapsed;
}
/**
* Current or saved anchor node.
*
* @type {Node}
*/
get anchorNode() {
if (__privateGet(this, _savedSelection)) {
return __privateGet(this, _savedSelection).anchorNode;
}
return __privateGet(this, _anchorNode);
}
/**
* Current or saved anchor offset.
*
* @type {number}
*/
get anchorOffset() {
if (__privateGet(this, _savedSelection)) {
return __privateGet(this, _savedSelection).anchorOffset;
}
return __privateGet(this, _selection).anchorOffset;
}
/**
* Indicates that the caret is at the start of the node.
*
* @type {boolean}
*/
get anchorAtStart() {
return this.anchorOffset === 0;
}
/**
* Indicates that the caret is at the end of the node.
*
* @type {boolean}
*/
get anchorAtEnd() {
return this.anchorOffset === this.anchorNode.nodeValue.length;
}
/**
* Current or saved focus node.
*
* @type {Node}
*/
get focusNode() {
if (__privateGet(this, _savedSelection)) {
return __privateGet(this, _savedSelection).focusNode;
}
if (!__privateGet(this, _focusNode))
console.trace("focusNode", __privateGet(this, _focusNode));
return __privateGet(this, _focusNode);
}
/**
* Current or saved focus offset.
*
* @type {number}
*/
get focusOffset() {
if (__privateGet(this, _savedSelection)) {
return __privateGet(this, _savedSelection).focusOffset;
}
return __privateGet(this, _focusOffset);
}
/**
* Indicates that the caret is at the start of the node.
*
* @type {boolean}
*/
get focusAtStart() {
return this.focusOffset === 0;
}
/**
* Indicates that the caret is at the end of the node.
*
* @type {boolean}
*/
get focusAtEnd() {
return this.focusOffset === this.focusNode.nodeValue.length;
}
/**
* Returns the paragraph in the focus node
* of the current selection.
*
* @type {HTMLElement|null}
*/
get focusParagraph() {
return getParagraph(this.focusNode);
}
/**
* Returns the inline in the focus node
* of the current selection.
*
* @type {HTMLElement|null}
*/
get focusInline() {
return getInline(this.focusNode);
}
/**
* Returns the current paragraph in the anchor
* node of the current selection.
*
* @type {HTMLElement|null}
*/
get anchorParagraph() {
return getParagraph(this.anchorNode);
}
/**
* Returns the current inline in the anchor
* node of the current selection.
*
* @type {HTMLElement|null}
*/
get anchorInline() {
return getInline(this.anchorNode);
}
/**
* Start container of the current range.
*/
get startContainer() {
var _a, _b, _c;
if (__privateGet(this, _savedSelection)) {
return (_b = (_a = __privateGet(this, _savedSelection)) == null ? void 0 : _a.range) == null ? void 0 : _b.startContainer;
}
return (_c = __privateGet(this, _range)) == null ? void 0 : _c.startContainer;
}
/**
* `startOffset` of the current range.
*
* @type {number|null}
*/
get startOffset() {
var _a, _b, _c;
if (__privateGet(this, _savedSelection)) {
return (_b = (_a = __privateGet(this, _savedSelection)) == null ? void 0 : _a.range) == null ? void 0 : _b.startOffset;
}
return (_c = __privateGet(this, _range)) == null ? void 0 : _c.startOffset;
}
/**
* Start paragraph of the current range.
*
* @type {HTMLElement|null}
*/
get startParagraph() {
const startContainer = this.startContainer;
if (!startContainer) return null;
return getParagraph(startContainer);
}
/**
* Start inline of the current page.
*
* @type {HTMLElement|null}
*/
get startInline() {
const startContainer = this.startContainer;
if (!startContainer) return null;
return getInline(startContainer);
}
/**
* End container of the current range.
*
* @type {Node}
*/
get endContainer() {
var _a, _b, _c;
if (__privateGet(this, _savedSelection)) {
return (_b = (_a = __privateGet(this, _savedSelection)) == null ? void 0 : _a.range) == null ? void 0 : _b.endContainer;
}
return (_c = __privateGet(this, _range)) == null ? void 0 : _c.endContainer;
}
/**
* `endOffset` of the current range
*
* @type {HTMLElement|null}
*/
get endOffset() {
var _a, _b, _c;
if (__privateGet(this, _savedSelection)) {
return (_b = (_a = __privateGet(this, _savedSelection)) == null ? void 0 : _a.range) == null ? void 0 : _b.endOffset;
}
return (_c = __privateGet(this, _range)) == null ? void 0 : _c.endOffset;
}
/**
* Paragraph element of the `endContainer` of
* the current range.
*
* @type {HTMLElement|null}
*/
get endParagraph() {
const endContainer = this.endContainer;
if (!endContainer) return null;
return getParagraph(endContainer);
}
/**
* Inline element of the `endContainer` of
* the current range.
*
* @type {HTMLElement|null}
*/
get endInline() {
const endContainer = this.endContainer;
if (!endContainer) return null;
return getInline(endContainer);
}
/**
* Returns true if the anchor node and the focus
* node are the same text nodes.
*
* @type {boolean}
*/
get isTextSame() {
return this.isTextFocus === this.isTextAnchor && this.focusNode === this.anchorNode;
}
/**
* Indicates that focus node is a text node.
*
* @type {boolean}
*/
get isTextFocus() {
return this.focusNode.nodeType === Node.TEXT_NODE;
}
/**
* Indicates that anchor node is a text node.
*
* @type {boolean}
*/
get isTextAnchor() {
return this.anchorNode.nodeType === Node.TEXT_NODE;
}
/**
* Is true if the current focus node is a inline.
*
* @type {boolean}
*/
get isInlineFocus() {
return isInline(this.focusNode);
}
/**
* Is true if the current anchor node is a inline.
*
* @type {boolean}
*/
get isInlineAnchor() {
return isInline(this.anchorNode);
}
/**
* Is true if the current focus node is a paragraph.
*
* @type {boolean}
*/
get isParagraphFocus() {
return isParagraph(this.focusNode);
}
/**
* Is true if the current anchor node is a paragraph.
*
* @type {boolean}
*/
get isParagraphAnchor() {
return isParagraph(this.anchorNode);
}
/**
* Is true if the current focus node is a line break.
*
* @type {boolean}
*/
get isLineBreakFocus() {
return isLineBreak(this.focusNode) || isInline(this.focusNode) && isLineBreak(this.focusNode.firstChild);
}
/**
* Indicates that we have multiple nodes selected.
*
* @type {boolean}
*/
get isMulti() {
return this.focusNode !== this.anchorNode;
}
/**
* Indicates that we have selected multiple
* paragraph elements.
*
* @type {boolean}
*/
get isMultiParagraph() {
return this.isMulti && this.focusParagraph !== this.anchorParagraph;
}
/**
* Indicates that we have selected multiple
* inline elements.
*
* @type {boolean}
*/
get isMultiInline() {
return this.isMulti && this.focusInline !== this.anchorInline;
}
/**
* Indicates that the caret (only the caret)
* is at the start of an inline.
*
* @type {boolean}
*/
get isInlineStart() {
if (!this.isCollapsed) return false;
return isInlineStart(this.focusNode, this.focusOffset);
}
/**
* Indicates that the caret (only the caret)
* is at the end of an inline. This value doesn't
* matter when dealing with selections.
*
* @type {boolean}
*/
get isInlineEnd() {
if (!this.isCollapsed) return false;
return isInlineEnd(this.focusNode, this.focusOffset);
}
/**
* Indicates that we're in the starting position of a paragraph.
*
* @type {boolean}
*/
get isParagraphStart() {
if (!this.isCollapsed) return false;
return isParagraphStart(this.focusNode, this.focusOffset);
}
/**
* Indicates that we're in the ending position of a paragraph.
*
* @type {boolean}
*/
get isParagraphEnd() {
if (!this.isCollapsed) return false;
return isParagraphEnd(this.focusNode, this.focusOffset);
}
/**
* Insert pasted fragment.
*
* @param {DocumentFragment} fragment
*/
insertPaste(fragment) {
fragment.children.length;
if (this.isParagraphStart) {
this.focusParagraph.before(fragment);
} else if (this.isParagraphEnd) {
this.focusParagraph.after(fragment);
} else {
const newParagraph = splitParagraph(
this.focusParagraph,
this.focusInline,
this.focusOffset
);
this.focusParagraph.after(fragment, newParagraph);
}
}
/**
* Replaces data with pasted fragment
*
* @param {DocumentFragment} fragment
*/
replaceWithPaste(fragment) {
fragment.children.length;
this.removeSelected();
this.insertPaste(fragment);
}
/**
* Replaces the current line break with text
*
* @param {string} text
*/
replaceLineBreak(text) {
const newText = new Text(text);
this.focusInline.replaceChildren(newText);
this.collapse(newText, text.length);
}
/**
* Removes text forward from the current position.
*/
removeForwardText() {
__privateGet(this, _textNodeIterator).currentNode = this.focusNode;
const removedData = removeForward(
this.focusNode.nodeValue,
this.focusOffset
);
if (this.focusNode.nodeValue !== removedData) {
this.focusNode.nodeValue = removedData;
}
const paragraph = this.focusParagraph;
if (!paragraph) throw new Error("Cannot find paragraph");
const inline = this.focusInline;
if (!inline) throw new Error("Cannot find inline");
const nextTextNode = __privateGet(this, _textNodeIterator).nextNode();
if (this.focusNode.nodeValue === "") {
this.focusNode.remove();
}
if (paragraph.childNodes.length === 1 && inline.childNodes.length === 0) {
const lineBreak = createLineBreak();
inline.appendChild(lineBreak);
return this.collapse(lineBreak, 0);
} else if (paragraph.childNodes.length > 1 && inline.childNodes.length === 0) {
inline.remove();
return this.collapse(nextTextNode, 0);
}
return this.collapse(this.focusNode, this.focusOffset);
}
/**
* Removes text backward from the current caret position.
*/
removeBackwardText() {
__privateGet(this, _textNodeIterator).currentNode = this.focusNode;
const removedData = removeBackward(
this.focusNode.nodeValue,
this.focusOffset
);
if (this.focusNode.nodeValue !== removedData) {
this.focusNode.nodeValue = removedData;
}
if (this.focusOffset - 1 > 0) {
return this.collapse(this.focusNode, this.focusOffset - 1);
}
const paragraph = this.focusParagraph;
if (!paragraph) throw new Error("Cannot find paragraph");
const inline = this.focusInline;
if (!inline) throw new Error("Cannot find inline");
const previousTextNode = __privateGet(this, _textNodeIterator).previousNode();
if (this.focusNode.nodeValue === "") {
this.focusNode.remove();
}
if (paragraph.children.length === 1 && inline.childNodes.length === 0) {
const lineBreak = createLineBreak();
inline.appendChild(lineBreak);
return this.collapse(lineBreak, 0);
} else if (paragraph.children.length > 1 && inline.childNodes.length === 0) {
inline.remove();
return this.collapse(previousTextNode, getTextNodeLength(previousTextNode));
}
return this.collapse(this.focusNode, this.focusOffset - 1);
}
/**
* Inserts some text in the caret position.
*
* @param {string} newText
*/
insertText(newText) {
this.focusNode.nodeValue = insertInto(
this.focusNode.nodeValue,
this.focusOffset,
newText
);
__privateGet(this, _mutations).update(this.focusInline);
return this.collapse(this.focusNode, this.focusOffset + newText.length);
}
/**
* Replaces currently selected text.
*
* @param {string} newText
*/
replaceText(newText) {
const startOffset = Math.min(this.anchorOffset, this.focusOffset);
const endOffset = Math.max(this.anchorOffset, this.focusOffset);
this.focusNode.nodeValue = replaceWith(
this.focusNode.nodeValue,
startOffset,
endOffset,
newText
);
__privateGet(this, _mutations).update(this.focusInline);
return this.collapse(this.focusNode, startOffset + newText.length);
}
/**
* Replaces the selected inlines with new text.
*
* @param {string} newText
*/
replaceInlines(newText) {
const currentParagraph = this.focusParagraph;
if (this.startInline === currentParagraph.firstChild && this.startOffset === 0 && this.endInline === currentParagraph.lastChild && this.endOffset === currentParagraph.lastChild.textContent.length) {
const newTextNode = new Text(newText);
currentParagraph.replaceChildren(
createInline(newTextNode, this.anchorInline.style)
);
return this.collapse(newTextNode, newTextNode.nodeValue.length);
}
this.removeSelected();
this.focusNode.nodeValue = insertInto(
this.focusNode.nodeValue,
this.focusOffset,
newText
);
return this.collapse(this.focusNode, this.focusOffset + newText.length);
}
/**
* Replaces paragraphs with text.
*
* @param {string} newText
*/
replaceParagraphs(newText) {
const currentParagraph = this.focusParagraph;
this.removeSelected();
this.focusNode.nodeValue = insertInto(
this.focusNode.nodeValue,
this.focusOffset,
newText
);
for (const child of currentParagraph.children) {
if (child.textContent === "") {
child.remove();
}
}
}
/**
* Inserts a new paragraph after the current paragraph.
*/
insertParagraphAfter() {
const currentParagraph = this.focusParagraph;
const newParagraph = createEmptyParagraph(__privateGet(this, _currentStyle));
currentParagraph.after(newParagraph);
__privateGet(this, _mutations).update(currentParagraph);
__privateGet(this, _mutations).add(newParagraph);
return this.collapse(newParagraph.firstChild.firstChild, 0);
}
/**
* Inserts a new paragraph before the current paragraph.
*/
insertParagraphBefore() {
const currentParagraph = this.focusParagraph;
const newParagraph = createEmptyParagraph(__privateGet(this, _currentStyle));
currentParagraph.before(newParagraph);
__privateGet(this, _mutations).update(currentParagraph);
__privateGet(this, _mutations).add(newParagraph);
return this.collapse(currentParagraph.firstChild.firstChild, 0);
}
/**
* Splits the current paragraph.
*/
splitParagraph() {
const currentParagraph = this.focusParagraph;
const newParagraph = splitParagraph(
this.focusParagraph,
this.focusInline,
__privateGet(this, _focusOffset)
);
this.focusParagraph.after(newParagraph);
__privateGet(this, _mutations).update(currentParagraph);
__privateGet(this, _mutations).add(newParagraph);
return this.collapse(newParagraph.firstChild.firstChild, 0);
}
/**
* Inserts a new paragraph.
*/
insertParagraph() {
if (this.isParagraphEnd) {
return this.insertParagraphAfter();
} else if (this.isParagraphStart) {
return this.insertParagraphBefore();
}
return this.splitParagraph();
}
/**
* Replaces the currently selected content with
* a paragraph.
*/
replaceWithParagraph() {
const currentParagraph = this.focusParagraph;
const currentInline = this.focusInline;
this.removeSelected();
const newParagraph = splitParagraph(
currentParagraph,
currentInline,
this.focusOffset
);
currentParagraph.after(newParagraph);
__privateGet(this, _mutations).update(currentParagraph);
__privateGet(this, _mutations).add(newParagraph);
}
/**
* Removes a paragraph in backward direction.
*/
removeBackwardParagraph() {
const previousParagraph = this.focusParagraph.previousElementSibling;
if (!previousParagraph) {
return;
}
const paragraphToBeRemoved = this.focusParagraph;
paragraphToBeRemoved.remove();
const previousInline = previousParagraph.children.length > 1 ? previousParagraph.lastElementChild : previousParagraph.firstChild;
const previousOffset = isLineBreak(previousInline.firstChild) ? 0 : previousInline.firstChild.nodeValue.length;
__privateGet(this, _mutations).remove(paragraphToBeRemoved);
return this.collapse(previousInline.firstChild, previousOffset);
}
/**
* Merges the previous paragraph with the current paragraph.
*/
mergeBackwardParagraph() {
const currentParagraph = this.focusParagraph;
const previousParagraph = this.focusParagraph.previousElementSibling;
if (!previousParagraph) {
return;
}
let previousInline = previousParagraph.lastChild;
const previousOffset = getInlineLength(previousInline);
if (isEmptyParagraph(previousParagraph)) {
previousParagraph.replaceChildren(...currentParagraph.children);
previousInline = previousParagraph.firstChild;
currentParagraph.remove();
} else {
mergeParagraphs(previousParagraph, currentParagraph);
}
__privateGet(this, _mutations).remove(currentParagraph);
__privateGet(this, _mutations).update(previousParagraph);
return this.collapse(previousInline.firstChild, previousOffset);
}
/**
* Merges the next paragraph with the current paragraph.
*/
mergeForwardParagraph() {
const currentParagraph = this.focusParagraph;
const nextParagraph = this.focusParagraph.nextElementSibling;
if (!nextParagraph) {
return;
}
mergeParagraphs(this.focusParagraph, nextParagraph);
__privateGet(this, _mutations).update(currentParagraph);
__privateGet(this, _mutations).remove(nextParagraph);
}
/**
* Removes the forward paragraph.
*/
removeForwardParagraph() {
const nextParagraph = this.focusParagraph.nextSibling;
if (!nextParagraph) {
return;
}
const paragraphToBeRemoved = this.focusParagraph;
paragraphToBeRemoved.remove();
const nextInline = nextParagraph.firstChild;
const nextOffset = this.focusOffset;
__privateGet(this, _mutations).remove(paragraphToBeRemoved);
return this.collapse(nextInline.firstChild, nextOffset);
}
/**
* Cleans up all the affected paragraphs.
*
* @param {Set<HTMLDivElement>} affectedParagraphs
* @param {Set<HTMLSpanElement>} affectedInlines
*/
cleanUp(affectedParagraphs, affectedInlines) {
for (const inline of affectedInlines) {
if (inline.textContent === "") {
inline.remove();
__privateGet(this, _mutations).remove(inline);
}
}
for (const paragraph of affectedParagraphs) {
if (paragraph.children.length === 0) {
paragraph.remove();
__privateGet(this, _mutations).remove(paragraph);
}
}
}
/**
* Removes the selected content.
*
* @param {RemoveSelectedOptions} [options]
*/
removeSelected(options) {
if (this.isCollapsed) return;
const affectedInlines = /* @__PURE__ */ new Set();
const affectedParagraphs = /* @__PURE__ */ new Set();
const startNode = getClosestTextNode(__privateGet(this, _range).startContainer);
const endNode = getClosestTextNode(__privateGet(this, _range).endContainer);
const startOffset = __privateGet(this, _range).startOffset;
const endOffset = __privateGet(this, _range).endOffset;
if (startNode === endNode) {
__privateGet(this, _textNodeIterator).currentNode = startNode;
__privateGet(this, _textNodeIterator).previousNode();
__privateGet(this, _textNodeIterator).currentNode = startNode;
__privateGet(this, _textNodeIterator).nextNode();
const inline = getInline(startNode);
const paragraph = getParagraph(startNode);
affectedInlines.add(inline);
affectedParagraphs.add(paragraph);
const newNodeValue = removeSlice(
startNode.nodeValue,
startOffset,
endOffset
);
if (newNodeValue === "") {
const lineBreak = createLineBreak();
inline.replaceChildren(lineBreak);
return this.collapse(lineBreak, 0);
}
startNode.nodeValue = newNodeValue;
return this.collapse(startNode, startOffset);
}
__privateGet(this, _textNodeIterator).currentNode = startNode;
const startInline = getInline(startNode);
const startParagraph = getParagraph(startNode);
const endInline = getInline(endNode);
const endParagraph = getParagraph(endNode);
SafeGuard.start();
do {
SafeGuard.update();
const currentNode = __privateGet(this, _textNodeIterator).currentNode;
const inline = getInline(__privateGet(this, _textNodeIterator).currentNode);
const paragraph = getParagraph(__privateGet(this, _textNodeIterator).currentNode);
let shouldRemoveNodeCompletely = false;
if (__privateGet(this, _textNodeIterator).currentNode === startNode) {
if (startOffset === 0) {
shouldRemoveNodeCompletely = true;
} else {
currentNode.nodeValue = currentNode.nodeValue.slice(0, startOffset);
}
} else if (__privateGet(this, _textNodeIterator).currentNode === endNode) {
if (isLineBreak(endNode) || isTextNode(endNode) && endOffset === endNode.nodeValue.length) {
shouldRemoveNodeCompletely = true;
} else {
currentNode.nodeValue = currentNode.nodeValue.slice(endOffset);
}
} else {
shouldRemoveNodeCompletely = true;
}
__privateGet(this, _textNodeIterator).nextNode();
if (shouldRemoveNodeCompletely) {
currentNode.remove();
if (currentNode === startNode) {
continue;
}
if (currentNode === endNode) {
break;
}
if (inline.childNodes.length === 0) {
inline.remove();
}
if (paragraph !== startParagraph && paragraph.children.length === 0) {
paragraph.remove();
}
}
if (currentNode === endNode) {
break;
}
} while (__privateGet(this, _textNodeIterator).currentNode);
if (startParagraph !== endParagraph) {
const mergedParagraph = mergeParagraphs(startParagraph, endParagraph);
if (mergedParagraph.children.length === 0) {
const newEmptyInline = createEmptyInline(__privateGet(this, _currentStyle));
mergedParagraph.appendChild(newEmptyInline);
return this.collapse(newEmptyInline.firstChild, 0);
}
}
if (startInline.childNodes.length === 0 && endInline.childNodes.length > 0) {
startInline.remove();
return this.collapse(endNode, 0);
} else if (startInline.childNodes.length > 0 && endInline.childNodes.length === 0) {
endInline.remove();
return this.collapse(startNode, startOffset);
} else if (startInline.childNodes.length === 0 && endInline.childNodes.length === 0) {
const previousInline = startInline.previousElementSibling;
const nextInline = endInline.nextElementSibling;
startInline.remove();
endInline.remove();
if (previousInline) {
return this.collapse(previousInline.firstChild, previousInline.firstChild.nodeValue.length);
}
if (nextInline) {
return this.collapse(nextInline.firstChild, 0);
}
const newEmptyInline = createEmptyInline(__privateGet(this, _currentStyle));
startParagraph.appendChild(newEmptyInline);
return this.collapse(newEmptyInline.firstChild, 0);
}
return this.collapse(startNode, startOffset);
}
/**
* Applies styles to selection
*
* @param {Object.<string, *>} newStyles
* @returns {void}
*/
applyStyles(newStyles) {
return __privateMethod(this, _SelectionController_instances, applyStylesTo_fn).call(this, this.startContainer, this.startOffset, this.endContainer, this.endOffset, newStyles);
}
}
_textEditor = new WeakMap();
_selection = new WeakMap();
_ranges = new WeakMap();
_range = new WeakMap();
_focusNode = new WeakMap();
_focusOffset = new WeakMap();
_anchorNode = new WeakMap();
_anchorOffset = new WeakMap();
_savedSelection = new WeakMap();
_textNodeIterator = new WeakMap();
_currentStyle = new WeakMap();
_inertElement = new WeakMap();
_debug = new WeakMap();
_mutations = new WeakMap();
_styleDefaults = new WeakMap();
_SelectionController_instances = new WeakSet();
/**
* Applies the default styles to the currentStyle
* CSSStyleDeclaration.
*/
applyDefaultStylesToCurrentStyle_fn = function() {
if (__privateGet(this, _styleDefaults)) {
for (const [name, value] of Object.entries(__privateGet(this, _styleDefaults))) {
__privateGet(this, _currentStyle).setProperty(
name,
value + (name === "font-size" ? "px" : "")
);
}
}
};
/**
* Applies some styles to the currentStyle
* CSSStyleDeclaration
*
* @param {HTMLElement} element
*/
applyStylesToCurrentStyle_fn = function(element) {
for (let index = 0; index < element.style.length; index++) {
const styleName = element.style.item(index);
const styleValue = element.style.getPropertyValue(styleName);
__privateGet(this, _currentStyle).setProperty(styleName, styleValue);
}
};
/**
* Updates current styles based on the currently selected inline.
*
* @param {HTMLSpanElement} inline
* @returns {SelectionController}
*/
updateCurrentStyle_fn = function(inline) {
__privateMethod(this, _SelectionController_instances, applyDefaultStylesToCurrentStyle_fn).call(this);
const root = inline.parentElement.parentElement;
__privateMethod(this, _SelectionController_instances, applyStylesToCurrentStyle_fn).call(this, root);
const paragraph = inline.parentElement;
__privateMethod(this, _SelectionController_instances, applyStylesToCurrentStyle_fn).call(this, paragraph);
__privateMethod(this, _SelectionController_instances, applyStylesToCurrentStyle_fn).call(this, inline);
return this;
};
_onSelectionChange = new WeakMap();
/**
* Notifies that the styles have changed.
*/
notifyStyleChange_fn = function() {
const inline = this.focusInline;
if (inline) {
__privateMethod(this, _SelectionController_instances, updateCurrentStyle_fn).call(this, inline);
this.dispatchEvent(
new CustomEvent("stylechange", {
detail: __privateGet(this, _currentStyle)
})
);
}
};
/**
* Setups
*/
setup_fn = function() {
__privateSet(this, _inertElement, document.createElement("div"));
__privateSet(this, _currentStyle, __privateGet(this, _inertElement).style);
__privateMethod(this, _SelectionController_instances, applyDefaultStylesToCurrentStyle_fn).call(this);
if (__privateGet(this, _selection).rangeCount > 0) {
const range = __privateGet(this, _selection).getRangeAt(0);
__privateSet(this, _range, range);
__privateGet(this, _ranges).add(range);
}
if (__privateGet(this, _selection).rangeCount > 1) {
for (let index = 1; index < __privateGet(this, _selection).rangeCount; index++) {
__privateGet(this, _selection).removeRange(index);
}
}
document.addEventListener("selectionchange", __privateGet(this, _onSelectionChange));
};
/**
* Returns a Range-like object.
*
* @returns {RangeLike}
*/
getSavedRange_fn = function() {
if (!__privateGet(this, _range)) {
return {
collapsed: true,
commonAncestorContainer: null,
startContainer: null,
startOffset: 0,
endContainer: null,
endOffset: 0
};
}
return {
collapsed: __privateGet(this, _range).collapsed,
commonAncestorContainer: __privateGet(this, _range).commonAncestorContainer,
startContainer: __privateGet(this, _range).startContainer,
startOffset: __privateGet(this, _range).startOffset,
endContainer: __privateGet(this, _range).endContainer,
endOffset: __privateGet(this, _range).endOffset
};
};
/**
* Applies styles from the startNode to the endNode.
*
* @param {Node} startNode
* @param {number} startOffset
* @param {Node} endNode
* @param {number} endOffset
* @param {Object.<string,*>|CSSStyleDeclaration} newStyles
* @returns {void}
*/
applyStylesTo_fn = function(startNode, startOffset, endNode, endOffset, newStyles) {
const root = __privateGet(this, _textEditor).root;
setRootStyles(root, newStyles);
if (startNode === endNode && startNode.nodeType === Node.TEXT_NODE) {
if (startOffset === 0 && endOffset === endNode.nodeValue.length) {
const paragraph = this.startParagraph;
const inline = this.startInline;
setParagraphStyles(paragraph, newStyles);
setInlineStyles(inline, newStyles);
} else if (startOffset !== endOffset) {
const paragraph = this.startParagraph;
setParagraphStyles(paragraph, newStyles);
const inline = this.startInline;
const midText = startNode.splitText(startOffset);
const endText = midText.splitText(endOffset - startOffset);
const midInline = createInlineFrom(inline, midText, newStyles);
inline.after(midInline);
if (endText.length > 0) {
const endInline = createInline(endText, inline.style);
midInline.after(endInline);
}
this.setSelection(midText, 0, midText, midText.nodeValue.length);
} else {
const paragraph = this.startParagraph;
setParagraphStyles(paragraph, newStyles);
}
return __privateMethod(this, _SelectionController_instances, notifyStyleChange_fn).call(this);
} else if (startNode !== endNode) {
SafeGuard.start();
const expectedEndNode = getClosestTextNode(endNode);
__privateGet(this, _textNodeIterator).currentNode = getClosestTextNode(startNode);
do {
SafeGuard.update();
const paragraph = getParagraph(__privateGet(this, _textNodeIterator).currentNode);
setParagraphStyles(paragraph, newStyles);
const inline = getInline(__privateGet(this, _textNodeIterator).currentNode);
if (__privateGet(this, _textNodeIterator).currentNode === startNode && startOffset > 0) {
const newInline = splitInline(inline, startOffset);
setInlineStyles(newInline, newStyles);
inline.after(newInline);
} else if (__privateGet(this, _textNodeIterator).currentNode === startNode && startOffset === 0 || __privateGet(this, _textNodeIterator).currentNode !== startNode && __privateGet(this, _textNodeIterator).currentNode !== endNode || __privateGet(this, _textNodeIterator).currentNode === endNode && endOffset === endNode.nodeValue.length) {
setInlineStyles(inline, newStyles);
} else if (__privateGet(this, _textNodeIterator).currentNode === endNode && endOffset < endNode.nodeValue.length) {
const newInline = splitInline(inline, endOffset);
setInlineStyles(inline, newStyles);
inline.after(newInline);
}
if (__privateGet(this, _textNodeIterator).currentNode === expectedEndNode) return;
__privateGet(this, _textNodeIterator).nextNode();
} while (__privateGet(this, _textNodeIterator).currentNode);
}
return __privateMethod(this, _SelectionController_instances, notifyStyleChange_fn).call(this);
};
function createSelectionImposterFromClientRects(referenceRect, clientRects) {
const fragment = document.createDocumentFragment();
for (const rect of clientRects) {
const rectElement = document.createElement("div");
rectElement.className = "selection-imposter-rect";
rectElement.style.left = `${rect.x - referenceRect.x}px`;
rectElement.style.top = `${rect.y - referenceRect.y}px`;
rectElement.style.width = `${rect.width}px`;
rectElement.style.height = `${rect.height}px`;
fragment.appendChild(rectElement);
}
return fragment;
}
function addEventListeners(target, object, options) {
Object.entries(object).forEach(
([type, listener]) => target.addEventListener(type, listener, options)
);
}
function removeEventListeners(target, object) {
Object.entries(object).forEach(
([type, listener]) => target.removeEventListener(type, listener)
);
}
const LayoutType = {
FULL: "full",
PARTIAL: "partial"
};
class TextEditor extends EventTarget {
/**
* Constructor.
*
* @param {HTMLElement} element
*/
constructor(element, options) {
super();
__privateAdd(this, _TextEditor_instances);
/**
* Element content editable to be used by the TextEditor
*
* @type {HTMLElement}
*/
__privateAdd(this, _element, null);
/**
* Map/Dictionary of events.
*
* @type {Object.<string, Function>}
*/
__privateAdd(this, _events, null);
/**
* Root element that will contain the content.
*
* @type {HTMLElement}
*/
__privateAdd(this, _root, null);
/**
* Change controller controls when we should notify changes.
*
* @type {ChangeController}
*/
__privateAdd(this, _changeController, null);
/**
* Selection controller controls the current/saved selection.
*
* @type {SelectionController}
*/
__privateAdd(this, _selectionController, null);
/**
* Selection imposter keeps selection elements.
*
* @type {HTMLElement}
*/
__privateAdd(this, _selectionImposterElement, null);
/**
* Style defaults.
*
* @type {Object.<string, *>}
*/
__privateAdd(this, _styleDefaults2, null);
/**
* Dispatchs a `change` event.
*
* @param {CustomEvent} e
* @returns {void}
*/
__privateAdd(this, _onChange, (e) => this.dispatchEvent(new e.constructor(e.type, e)));
/**
* Dispatchs a `stylechange` event.
*
* @param {CustomEvent} e
* @returns {void}
*/
__privateAdd(this, _onStyleChange, (e) => {
if (__privateGet(this, _selectionImposterElement).children.length > 0) {
__privateMethod(this, _TextEditor_instances, createSelectionImposter_fn).call(this);
}
this.dispatchEvent(new e.constructor(e.type, e));
});
/**
* On blur we create a new FakeSelection if there's any.
*
* @param {FocusEvent} e
*/
__privateAdd(this, _onBlur, (e) => {
__privateGet(this, _changeController).notifyImmediately();
__privateGet(this, _selectionController).saveSelection();
__privateMethod(this, _TextEditor_instances, createSelectionImposter_fn).call(this);
this.dispatchEvent(new FocusEvent(e.type, e));
});
/**
* On focus we should restore the FakeSelection from the current
* selection.
*
* @param {FocusEvent} e
*/
__privateAdd(this, _onFocus, (e) => {
__privateGet(this, _selectionController).restoreSelection();
if (__privateGet(this, _selectionImposterElement)) {
__privateGet(this, _selectionImposterElement).replaceChildren();
}
this.dispatchEvent(new FocusEvent(e.type, e));
});
/**
* Event called when the user pastes some text into the
* editor.
*
* @param {ClipboardEvent} e
*/
__privateAdd(this, _onPaste, (e) => clipboard.paste(e, this, __privateGet(this, _selectionController)));
/**
* Event called when the user cuts some text from the
* editor.
*
* @param {ClipboardEvent} e
*/
__privateAdd(this, _onCut, (e) => clipboard.cut(e, this, __privateGet(this, _selectionController)));
/**
* Event called when the user copies some text from the
* editor.
*
* @param {ClipboardEvent} e
*/
__privateAdd(this, _onCopy, (e) => clipboard.copy(e, this, __privateGet(this, _selectionController)));
/**
* Event called before the DOM is modified.
*
* @param {InputEvent} e
*/
__privateAdd(this, _onBeforeInput, (e) => {
if (e.inputType === "historyUndo" || e.inputType === "historyRedo") {
return;
}
if (!(e.inputType in commands)) {
if (e.inputType !== "insertCompositionText") {
e.preventDefault();
}
return;
}
if (e.inputType in commands) {
const command = commands[e.inputType];
if (!__privateGet(this, _selectionController).startMutation())
return;
command(e, this, __privateGet(this, _selectionController));
const mutations = __privateGet(this, _selectionController).endMutation();
__privateMethod(this, _TextEditor_instances, notifyLayout_fn).call(this, LayoutType.FULL, mutations);
}
});
/**
* Event called after the DOM is modified.
*
* @param {InputEvent} e
*/
__privateAdd(this, _onInput, (e) => {
if (e.inputType === "historyUndo" || e.inputType === "historyRedo") {
return;
}
if (e.inputType === "insertCompositionText") {
__privateMethod(this, _TextEditor_instances, notifyLayout_fn).call(this, LayoutType.FULL, null);
}
});
if (!(element instanceof HTMLElement))
throw new TypeError("Invalid text editor element");
__privateSet(this, _element, element);
__privateSet(this, _selectionImposterElement, options == null ? void 0 : options.selectionImposterElement);
__privateSet(this, _events, {
blur: __privateGet(this, _onBlur),
focus: __privateGet(this, _onFocus),
paste: __privateGet(this, _onPaste),
cut: __privateGet(this, _onCut),
copy: __privateGet(this, _onCopy),
beforeinput: __privateGet(this, _onBeforeInput),
input: __privateGet(this, _onInput)
});
__privateSet(this, _styleDefaults2, options == null ? void 0 : options.styleDefaults);
__privateMethod(this, _TextEditor_instances, setup_fn2).call(this, options);
}
/**
* Root element that contains all the paragraphs.
*
* @type {HTMLDivElement}
*/
get root() {
return __privateGet(this, _root);
}
set root(newRoot) {
const previousRoot = __privateGet(this, _root);
__privateSet(this, _root, newRoot);
previousRoot.replaceWith(newRoot);
}
/**
* Element that contains the root and that has the
* contenteditable attribute.
*
* @type {HTMLElement}
*/
get element() {
return __privateGet(this, _element);
}
/**
* Returns true if the content is in an empty state.
*
* @type {boolean}
*/
get isEmpty() {
return __privateGet(this, _root).children.length === 1 && __privateGet(this, _root).firstElementChild.children.length === 1 && isLineBreak(__privateGet(this, _root).firstElementChild.firstElementChild.firstChild);
}
/**
* Indicates the amount of paragraphs in the current content.
*
* @type {number}
*/
get numParagraphs() {
return __privateGet(this, _root).children.length;
}
/**
* CSS Style declaration for the current inline. From here we
* can infer root, paragraph and inline declarations.
*
* @type {CSSStyleDeclaration}
*/
get currentStyle() {
return __privateGet(this, _selectionController).currentStyle;
}
/**
* Focus the element
*/
focus() {
return __privateGet(this, _element).focus();
}
/**
* Blurs the element
*/
blur() {
return __privateGet(this, _element).blur();
}
/**
* Creates a new root.
*
* @param {...any} args
* @returns {HTMLDivElement}
*/
createRoot(...args) {
return createRoot(...args);
}
/**
* Creates a new paragraph.
*
* @param {...any} args
* @returns {HTMLDivElement}
*/
createParagraph(...args) {
return createParagraph(...args);
}
/**
* Creates a new inline from a string.
*
* @param {string} text
* @param {Object.<string,*>|CSSStyleDeclaration} styles
* @returns {HTMLSpanElement}
*/
createInlineFromString(text, styles) {
if (text === "") {
return createEmptyInline(styles);
}
return createInline(new Text(text), styles);
}
/**
* Creates a new inline.
*
* @param {...any} args
* @returns {HTMLSpanElement}
*/
createInline(...args) {
return createInline(...args);
}
/**
* Applies the current styles to the selection or
* the current DOM node at the caret.
*
* @param {*} styles
*/
applyStylesToSelection(styles) {
__privateGet(this, _selectionController).startMutation();
__privateGet(this, _selectionController).applyStyles(styles);
const mutations = __privateGet(this, _selectionController).endMutation();
__privateMethod(this, _TextEditor_instances, notifyLayout_fn).call(this, LayoutType.FULL, mutations);
__privateGet(this, _changeController).notifyImmediately();
return this;
}
/**
* Selects all content.
*/
selectAll() {
__privateGet(this, _selectionController).selectAll();
return this;
}
/**
* Moves cursor to end.
*
* @returns
*/
cursorToEnd() {
__privateGet(this, _selectionController).cursorToEnd();
return this;
}
/**
* Disposes everything.
*/
dispose() {
__privateGet(this, _changeController).removeEventListener("change", __privateGet(this, _onChange));
__privateGet(this, _changeController).dispose();
__privateSet(this, _changeController, null);
__privateGet(this, _selectionController).removeEventListener(
"stylechange",
__privateGet(this, _onStyleChange)
);
__privateGet(this, _selectionController).dispose();
__privateSet(this, _selectionController, null);
removeEventListeners(__privateGet(this, _element), __privateGet(this, _events));
__privateSet(this, _element, null);
__privateSet(this, _root, null);
}
}
_element = new WeakMap();
_events = new WeakMap();
_root = new WeakMap();
_changeController = new WeakMap();
_selectionController = new WeakMap();
_selectionImposterElement = new WeakMap();
_styleDefaults2 = new WeakMap();
_TextEditor_instances = new WeakSet();
/**
* Setups editor properties.
*/
setupElementProperties_fn = function() {
if (!__privateGet(this, _element).isContentEditable) {
__privateGet(this, _element).contentEditable = "true";
if (!__privateGet(this, _element).isContentEditable) {
__privateGet(this, _element).setAttribute("contenteditable", "true");
}
}
if (__privateGet(this, _element).spellcheck) __privateGet(this, _element).spellcheck = false;
if (__privateGet(this, _element).autocapitalize) __privateGet(this, _element).autocapitalize = false;
if (!__privateGet(this, _element).autofocus) __privateGet(this, _element).autofocus = true;
if (!__privateGet(this, _element).role || __privateGet(this, _element).role !== "textbox")
__privateGet(this, _element).role = "textbox";
if (__privateGet(this, _element).ariaAutoComplete) __privateGet(this, _element).ariaAutoComplete = false;
if (!__privateGet(this, _element).ariaMultiLine) __privateGet(this, _element).ariaMultiLine = true;
__privateGet(this, _element).dataset.itype = "editor";
};
/**
* Setups the root element.
*/
setupRoot_fn = function() {
__privateSet(this, _root, createEmptyRoot(__privateGet(this, _styleDefaults2)));
__privateGet(this, _element).appendChild(__privateGet(this, _root));
};
_onChange = new WeakMap();
_onStyleChange = new WeakMap();
/**
* Setups the elements, the properties and the
* initial content.
*/
setup_fn2 = function(options) {
__privateMethod(this, _TextEditor_instances, setupElementProperties_fn).call(this);
__privateMethod(this, _TextEditor_instances, setupRoot_fn).call(this);
__privateSet(this, _changeController, new ChangeController(this));
__privateGet(this, _changeController).addEventListener("change", __privateGet(this, _onChange));
__privateSet(this, _selectionController, new SelectionController(
this,
document.getSelection(),
options
));
__privateGet(this, _selectionController).addEventListener(
"stylechange",
__privateGet(this, _onStyleChange)
);
addEventListeners(__privateGet(this, _element), __privateGet(this, _events), {
capture: true
});
};
/**
* Creates the selection imposter.
*/
createSelectionImposter_fn = function() {
var _a;
if (__privateGet(this, _selectionImposterElement) && !__privateGet(this, _selectionController).isCollapsed) {
const rects = (_a = __privateGet(this, _selectionController).range) == null ? void 0 : _a.getClientRects();
if (rects) {
const rect = __privateGet(this, _selectionImposterElement).getBoundingClientRect();
__privateGet(this, _selectionImposterElement).replaceChildren(
createSelectionImposterFromClientRects(rect, rects)
);
}
}
};
_onBlur = new WeakMap();
_onFocus = new WeakMap();
_onPaste = new WeakMap();
_onCut = new WeakMap();
_onCopy = new WeakMap();
_onBeforeInput = new WeakMap();
_onInput = new WeakMap();
/**
* Notifies that the edited texts needs layout.
*
* @param {'full'|'partial'} type
* @param {CommandMutations} mutations
*/
notifyLayout_fn = function(type = LayoutType.FULL, mutations) {
this.dispatchEvent(
new CustomEvent("needslayout", {
detail: {
type,
mutations
}
})
);
};
export default TextEditor;
//# sourceMappingURL=TextEditor.js.map