typewriter-editor
Version:
A rich text editor using the Delta format with decorations and rendered with a tiny virtual dom
223 lines (222 loc) • 8.92 kB
JavaScript
// Expose to allow debugging of keys on line elements
export const options = {
renderKeys: false,
};
const EMPTY_ARR = [];
const SVG_NS = 'http://www.w3.org/2000/svg';
const KEY_ATTR = 'data-key';
const domProps = new Set(['value', 'selected', 'checked', 'contentEditable']);
const getKey = (vdom) => (vdom == null ? vdom : vdom.key);
const setKey = (dom, key) => {
if (key && key !== dom.key) {
dom.key = key;
options.renderKeys && dom.setAttribute(KEY_ATTR, key);
}
if (!key && dom.key) {
delete dom.key;
options.renderKeys && dom.removeAttribute(KEY_ATTR);
}
};
const listener = (event) => {
event.currentTarget.events[event.type](event);
};
const patchProp = (dom, key, oldVal, newVal, isSvg) => {
if (key === 'key') {
}
else if (key[0] === 'o' && key[1] === 'n') {
if (!((dom.events || (dom.events = {}))[(key = key.slice(2))] = newVal)) {
dom.removeEventListener(key, listener);
}
else if (!oldVal) {
dom.addEventListener(key, listener);
}
}
else if (newVal == null) {
dom.removeAttribute(key);
}
else if (!isSvg && key !== 'list' && key !== 'form' && key in dom) {
dom[key] = newVal == null ? '' : newVal;
}
else {
dom.setAttribute(key, newVal);
}
};
const createNode = (vdom, isSvg) => {
if (typeof vdom === 'string') {
return document.createTextNode(vdom);
}
var props = vdom.props;
var dom = (isSvg = isSvg || vdom.type === 'svg')
? document.createElementNS(SVG_NS, vdom.type, { is: props.is })
: document.createElement(vdom.type, { is: props.is });
for (var k in props)
patchProp(dom, k, null, props[k], isSvg);
setKey(dom, getKey(vdom));
vdom.children.forEach(kid => dom.appendChild(createNode(vdomify(kid), isSvg)));
return dom;
};
const getDomProps = (dom, isSvg) => {
const props = {};
for (let i = 0; i < dom.attributes.length; i++) {
const { name, value } = dom.attributes[i];
if (name in dom && name !== 'list' && !isSvg) {
props[name] = dom[name];
}
else if (!options.renderKeys || name !== KEY_ATTR) {
props[name] = value === '' ? true : value;
}
}
return props;
};
const patchDom = (parent, dom, oldDom, newVdom, isSvg) => {
if (typeof newVdom === 'string') {
if (oldDom != null && oldDom.nodeType === Node.TEXT_NODE) {
if (oldDom.nodeValue !== newVdom)
dom.nodeValue = newVdom;
}
else {
dom = parent.insertBefore(createNode(newVdom, isSvg), dom);
if (oldDom != null) {
parent.removeChild(oldDom);
}
}
}
else if (oldDom == null || oldDom.nodeName.toLowerCase() !== newVdom.type) {
dom = parent.insertBefore(createNode(vdomify(newVdom), isSvg), dom);
if (oldDom != null) {
parent.removeChild(oldDom);
}
}
else {
var oldProps = getDomProps(oldDom, isSvg), newProps = newVdom.props;
isSvg = isSvg || newVdom.type === 'svg';
for (var i in { ...oldProps, ...newProps }) {
if ((domProps.has(i) ? dom[i] : oldProps[i]) !== newProps[i]) {
patchProp(dom, i, oldProps[i], newProps[i], isSvg);
}
}
setKey(dom, newVdom.key);
patchChildren(dom, newVdom.children, isSvg);
}
return dom;
};
const patchChildren = (dom, newVKids, isSvg, oldKids = Array.from(dom.childNodes)) => {
var tmpKid, oldKid, oldKey, newKey, oldHead = 0, newHead = 0, oldTail = oldKids.length - 1, newTail = newVKids.length - 1;
// Patch children with the same key from the beginning until they diverge
while (newHead <= newTail && oldHead <= oldTail) {
if ((oldKey = getKey(oldKids[oldHead])) == null || oldKey !== getKey(newVKids[newHead])) {
break;
}
patchDom(dom, oldKids[oldHead], oldKids[oldHead++], (newVKids[newHead] = vdomify(newVKids[newHead++])), isSvg);
}
// Patch children with the same key backwards from the end until they diverge
while (newHead <= newTail && oldHead <= oldTail) {
if ((oldKey = getKey(oldKids[oldTail])) == null || oldKey !== getKey(newVKids[newTail])) {
break;
}
// Update the reference for insertBefore references
oldKids[oldTail] = patchDom(dom, oldKids[oldTail], oldKids[oldTail--], (newVKids[newTail] = vdomify(newVKids[newTail--])), isSvg);
}
if (oldHead > oldTail) {
// All old matched, so new nodes were inserted
const insertBefore = oldKids[oldHead] || (oldKids[oldHead - 1] && oldKids[oldHead - 1].nextSibling) || null;
while (newHead <= newTail) {
dom.insertBefore(createNode((newVKids[newHead] = vdomify(newVKids[newHead++])), isSvg), insertBefore);
}
}
else if (newHead > newTail) {
// All new matched, so extra old nodes needing to be removed
while (oldHead <= oldTail) {
dom.removeChild(oldKids[oldHead++]);
}
}
else {
// 1 or more from old and new need to be removed/added
// cache old keys to their dom
const oldKeyed = new Map();
const newKeyed = new Set();
for (let i = oldHead; i <= oldTail; i++) {
if ((oldKey = getKey(oldKids[i])) != null) {
oldKeyed.set(oldKey, oldKids[i]);
}
}
// Go through the rest of the new to add/update them
while (newHead <= newTail) {
oldKey = getKey((oldKid = oldKids[oldHead]));
newKey = getKey((newVKids[newHead] = vdomify(newVKids[newHead])));
// If the old key was placed somewhere else already, or the new key is after this old one, remove it
if (newKeyed.has(oldKey) || (newKey != null && newKey === getKey(oldKids[oldHead + 1]))) {
if (oldKey == null) {
dom.removeChild(oldKid);
}
oldHead++;
continue;
}
if (newKey == null) {
if (oldKey == null) {
// Both keys are null, just patch it
patchDom(dom, oldKid, oldKid, newVKids[newHead], isSvg);
newHead++;
}
// otherwise move on, we'll remove this old one below when we iterate through oldKeyed
oldHead++;
}
else {
if (oldKey === newKey) {
// They match, just patch them (incr newHead below)
patchDom(dom, oldKid, oldKid, newVKids[newHead], isSvg);
newKeyed.add(newKey);
oldHead++;
}
else {
if ((tmpKid = oldKeyed.get(newKey)) != null) {
// If the matching old node is in the dom already, pull it into this location and patch it
patchDom(dom, dom.insertBefore(tmpKid, oldKid), tmpKid, newVKids[newHead], isSvg);
newKeyed.add(newKey);
}
else {
// This is a new item and there is no old or the old no longer belongs, patch it in, leaving oldKid
patchDom(dom, oldKid, null, newVKids[newHead], isSvg);
}
}
newHead++;
}
}
while (oldHead <= oldTail) {
if (getKey((oldKid = oldKids[oldHead++])) == null) {
dom.removeChild(oldKid);
}
}
for (const [key, node] of oldKeyed) {
if (!newKeyed.has(key)) {
dom.removeChild(node);
}
}
}
return dom;
};
const vdomify = (vdom) => (vdom !== true && vdom !== false && vdom ? vdom : '');
const createVdom = (type, props, children, key) => ({
type,
props,
children,
key,
});
export const recycleNode = (dom) => dom.nodeType === Node.TEXT_NODE
? dom.nodeValue
: createVdom(dom.nodeName.toLowerCase(), getDomProps(dom), EMPTY_ARR.map.call(dom.childNodes, recycleNode), getKey(dom));
export const h = (type, props, ch) => typeof type === 'function'
? type(props || {}, ch)
: createVdom(type, props || {}, Array.isArray(ch) ? ch : ch == null ? [] : [ch], props?.key);
// Helper for Typescript JSX which translates JSX into React.createElement() calls.
// Add `import { React } from 'typewriter-editor';` at the top of any .tsx page to use JSX.
export const React = { createElement: h };
export const patch = (dom, vdom, oldKids) => {
if (Array.isArray(vdom)) {
dom = patchChildren(dom, vdom, dom instanceof window.SVGElement, oldKids);
}
else {
dom = patchDom(dom.parentNode, dom, dom, vdom);
}
return dom;
};