214 lines
6.6 KiB
JavaScript
214 lines
6.6 KiB
JavaScript
/**
|
|
* @param {Node} node
|
|
* @param {Object} options
|
|
* @param {boolean} [options.sortAttrs]
|
|
* @returns {String}
|
|
*/
|
|
export function getContent(node, options = {}) {
|
|
const selection = node.ownerDocument.getSelection();
|
|
return _getElemContent(node, selection, options);
|
|
}
|
|
|
|
function _getContent(node, selection, options) {
|
|
switch (node.nodeType) {
|
|
case Node.TEXT_NODE:
|
|
return getTextContent(node, selection, options);
|
|
case Node.ELEMENT_NODE:
|
|
return getElemContent(node, selection, options);
|
|
case Node.COMMENT_NODE:
|
|
return `<!--${node.textContent}-->`;
|
|
default:
|
|
throw new Error("boom");
|
|
}
|
|
}
|
|
|
|
function getTextContent(node, selection, options) {
|
|
let text = node.textContent;
|
|
if (selection.focusNode === node) {
|
|
text = text.slice(0, selection.focusOffset) + "]" + text.slice(selection.focusOffset);
|
|
}
|
|
if (selection.anchorNode === node) {
|
|
const isAfterFocus =
|
|
selection.focusNode === selection.anchorNode &&
|
|
selection.focusOffset < selection.anchorOffset;
|
|
const idx = selection.anchorOffset + (isAfterFocus ? 1 : 0);
|
|
text = text.slice(0, idx) + "[" + text.slice(idx);
|
|
}
|
|
return text.replace(/\u00a0/g, " ");
|
|
}
|
|
|
|
const VOID_ELEMS = new Set(["BR", "IMG", "INPUT", "HR"]);
|
|
|
|
function _getElemContent(el, selection, options) {
|
|
let result = "";
|
|
function addTextSelection() {
|
|
if (selection.anchorNode === el && index === selection.anchorOffset) {
|
|
result += "[";
|
|
}
|
|
if (selection.focusNode === el && index === selection.focusOffset) {
|
|
result += "]";
|
|
}
|
|
}
|
|
let index = 0;
|
|
for (const child of el.childNodes) {
|
|
addTextSelection();
|
|
result += _getContent(child, selection, options);
|
|
index++;
|
|
}
|
|
addTextSelection();
|
|
return result;
|
|
}
|
|
|
|
function getElemContent(el, selection, options) {
|
|
const tag = el.tagName.toLowerCase();
|
|
const attributes = [...el.attributes];
|
|
if (options.sortAttrs) {
|
|
attributes.sort((attr1, attr2) => {
|
|
if (attr1.name === attr2.name) {
|
|
return 0;
|
|
}
|
|
return attr1.name > attr2.name ? 1 : -1;
|
|
});
|
|
}
|
|
const attrs = [];
|
|
for (const attr of attributes) {
|
|
const valueLimiter = attr.value.includes('"') ? "'" : '"';
|
|
attrs.push(`${attr.name}=${valueLimiter}${attr.value}${valueLimiter}`);
|
|
}
|
|
const attrStr = (attrs.length ? " " : "") + attrs.join(" ");
|
|
return VOID_ELEMS.has(el.tagName)
|
|
? `<${tag + attrStr}>`
|
|
: `<${tag + attrStr}>${_getElemContent(el, selection, options)}</${tag}>`;
|
|
}
|
|
|
|
function getTextNodesIterator(el) {
|
|
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
walker[Symbol.iterator] = () => ({
|
|
next() {
|
|
const value = walker.nextNode();
|
|
return { value, done: !value };
|
|
},
|
|
});
|
|
return walker;
|
|
}
|
|
|
|
export function setContent(el, content) {
|
|
// build a temp div element to first remove all [] characters in text nodes
|
|
const div = document.createElement("div");
|
|
div.innerHTML = content;
|
|
for (const textNode of getTextNodesIterator(div)) {
|
|
textNode.textContent = textNode.textContent.replace("[", "").replace("]", "");
|
|
}
|
|
// remove extra empty text nodes
|
|
const innerHTML = div.innerHTML;
|
|
if (el.innerHTML !== innerHTML) {
|
|
el.innerHTML = innerHTML;
|
|
}
|
|
|
|
const configSelection = getSelection(el, content);
|
|
if (configSelection) {
|
|
setSelection(configSelection);
|
|
}
|
|
if (getContent(el) !== content) {
|
|
throw new Error("error in setContent/getContent helpers");
|
|
}
|
|
}
|
|
|
|
export function setSelection({
|
|
anchorNode,
|
|
anchorOffset,
|
|
focusNode = anchorNode,
|
|
focusOffset = anchorOffset,
|
|
}) {
|
|
const selection = anchorNode.ownerDocument.getSelection();
|
|
selection.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
|
|
}
|
|
|
|
export function moveSelectionOutsideEditor() {
|
|
setSelection({
|
|
anchorNode: document.body,
|
|
anchorOffset: 0,
|
|
focusNode: document.body,
|
|
focusOffset: 0,
|
|
});
|
|
}
|
|
|
|
export function getSelection(el, content) {
|
|
if (content.indexOf("[") === -1 || content.indexOf("]") === -1) {
|
|
return;
|
|
}
|
|
|
|
const elRef = document.createElement(el.tagName);
|
|
elRef.innerHTML = content;
|
|
|
|
const configSelection = {};
|
|
visitAndSetRange(el, elRef, configSelection);
|
|
|
|
if (configSelection.anchorNode === undefined || configSelection.focusNode === undefined) {
|
|
return;
|
|
}
|
|
return configSelection;
|
|
}
|
|
|
|
export function setRange(el, content) {
|
|
// sanity check
|
|
const rawContent = content.replace("[", "").replace("]", "");
|
|
if (el.innerHTML !== rawContent) {
|
|
throw new Error("setRange requires the same html content");
|
|
}
|
|
|
|
// create range
|
|
const range = document.createRange();
|
|
const elRef = document.createElement(el.tagName);
|
|
elRef.innerHTML = content;
|
|
|
|
visitAndSetRange(el, elRef, range);
|
|
|
|
// set selection range
|
|
const selection = window.getSelection();
|
|
selection.removeAllRanges();
|
|
selection.addRange(range);
|
|
}
|
|
|
|
function visitAndSetRange(target, ref, configSelection) {
|
|
function applyRange(target, ref, idx) {
|
|
const text = ref.textContent;
|
|
if (text.includes("[")) {
|
|
const index = idx === undefined ? text.replace("]", "").indexOf("[") : idx;
|
|
configSelection.anchorNode = target;
|
|
configSelection.anchorOffset = index;
|
|
}
|
|
if (text.includes("]")) {
|
|
const index = idx === undefined ? text.replace("[", "").indexOf("]") : idx;
|
|
configSelection.focusNode = target;
|
|
configSelection.focusOffset = index;
|
|
}
|
|
}
|
|
|
|
if (target.nodeType === Node.TEXT_NODE) {
|
|
applyRange(target, ref);
|
|
} else {
|
|
const targetChildren = [...target.childNodes];
|
|
const refChildren = [...ref.childNodes];
|
|
let i = 0;
|
|
let j = 0;
|
|
while (i !== targetChildren.length || j !== refChildren.length) {
|
|
const targetChild = targetChildren[i] || targetChildren[i - 1];
|
|
const refChild = refChildren[j];
|
|
if (!targetChild) {
|
|
applyRange(target, refChild, 0);
|
|
return;
|
|
}
|
|
if (refChild.nodeType === targetChild.nodeType) {
|
|
visitAndSetRange(targetChild, refChild, configSelection);
|
|
i++;
|
|
j++;
|
|
} else {
|
|
// refchild is a textnode reduced to "[" or "]"
|
|
j++;
|
|
applyRange(target, refChild, i);
|
|
}
|
|
}
|
|
}
|
|
}
|