Pull in undom. We'll be modifying it.
This commit is contained in:
parent
8a5d746412
commit
8bd0937988
212
packages/projector/src/document.js
Normal file
212
packages/projector/src/document.js
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
// Originally Undom - https://github.com/developit/undom
|
||||||
|
// Copyright (c) 2016 Jason Miller
|
||||||
|
// License: MIT
|
||||||
|
|
||||||
|
/*
|
||||||
|
const NODE_TYPES = {
|
||||||
|
ELEMENT_NODE: 1,
|
||||||
|
ATTRIBUTE_NODE: 2,
|
||||||
|
TEXT_NODE: 3,
|
||||||
|
CDATA_SECTION_NODE: 4,
|
||||||
|
ENTITY_REFERENCE_NODE: 5,
|
||||||
|
COMMENT_NODE: 6,
|
||||||
|
PROCESSING_INSTRUCTION_NODE: 7,
|
||||||
|
DOCUMENT_NODE: 9
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
function toLower(str) {
|
||||||
|
return String(str).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function splice(arr, item, add, byValueOnly) {
|
||||||
|
let i = arr ? findWhere(arr, item, true, byValueOnly) : -1;
|
||||||
|
if (~i) add ? arr.splice(i, 0, add) : arr.splice(i, 1);
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findWhere(arr, fn, returnIndex, byValueOnly) {
|
||||||
|
let i = arr.length;
|
||||||
|
while (i--) if (typeof fn === 'function' && !byValueOnly ? fn(arr[i]) : arr[i] === fn) break;
|
||||||
|
return returnIndex ? i : arr[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAttributeFilter(ns, name) {
|
||||||
|
return o => o.ns === ns && toLower(o.name) === toLower(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a minimally viable DOM Document
|
||||||
|
* @returns {Document} document
|
||||||
|
*/
|
||||||
|
export function CreateDocument() {
|
||||||
|
function isElement(node) {
|
||||||
|
return node.nodeType === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Node {
|
||||||
|
constructor(nodeType, nodeName) {
|
||||||
|
this.nodeType = nodeType;
|
||||||
|
this.nodeName = nodeName;
|
||||||
|
this.childNodes = [];
|
||||||
|
}
|
||||||
|
get nextSibling() {
|
||||||
|
let p = this.parentNode;
|
||||||
|
if (p) return p.childNodes[findWhere(p.childNodes, this, true) + 1];
|
||||||
|
}
|
||||||
|
get previousSibling() {
|
||||||
|
let p = this.parentNode;
|
||||||
|
if (p) return p.childNodes[findWhere(p.childNodes, this, true) - 1];
|
||||||
|
}
|
||||||
|
get firstChild() {
|
||||||
|
return this.childNodes[0];
|
||||||
|
}
|
||||||
|
get lastChild() {
|
||||||
|
return this.childNodes[this.childNodes.length - 1];
|
||||||
|
}
|
||||||
|
appendChild(child) {
|
||||||
|
this.insertBefore(child);
|
||||||
|
}
|
||||||
|
insertBefore(child, ref) {
|
||||||
|
child.remove();
|
||||||
|
child.parentNode = this;
|
||||||
|
if (!ref) this.childNodes.push(child);
|
||||||
|
else splice(this.childNodes, ref, child);
|
||||||
|
}
|
||||||
|
replaceChild(child, ref) {
|
||||||
|
if (ref.parentNode === this) {
|
||||||
|
this.insertBefore(child, ref);
|
||||||
|
ref.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
removeChild(child) {
|
||||||
|
splice(this.childNodes, child);
|
||||||
|
}
|
||||||
|
remove() {
|
||||||
|
if (this.parentNode) this.parentNode.removeChild(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Text extends Node {
|
||||||
|
constructor(text) {
|
||||||
|
super(3, '#text'); // TEXT_NODE
|
||||||
|
this.nodeValue = text;
|
||||||
|
}
|
||||||
|
set textContent(text) {
|
||||||
|
this.nodeValue = text;
|
||||||
|
}
|
||||||
|
get textContent() {
|
||||||
|
return this.nodeValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Element extends Node {
|
||||||
|
constructor(nodeType, nodeName) {
|
||||||
|
super(nodeType || 1, nodeName); // ELEMENT_NODE
|
||||||
|
this.attributes = [];
|
||||||
|
this.__handlers = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
get children() {
|
||||||
|
return this.childNodes.filter(isElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAttribute(key, value) {
|
||||||
|
this.setAttributeNS(null, key, value);
|
||||||
|
}
|
||||||
|
getAttribute(key) {
|
||||||
|
return this.getAttributeNS(null, key);
|
||||||
|
}
|
||||||
|
removeAttribute(key) {
|
||||||
|
this.removeAttributeNS(null, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAttributeNS(ns, name, value) {
|
||||||
|
let attr = findWhere(this.attributes, createAttributeFilter(ns, name));
|
||||||
|
if (!attr) this.attributes.push((attr = { ns, name }));
|
||||||
|
attr.value = String(value);
|
||||||
|
}
|
||||||
|
getAttributeNS(ns, name) {
|
||||||
|
let attr = findWhere(this.attributes, createAttributeFilter(ns, name));
|
||||||
|
return attr && attr.value;
|
||||||
|
}
|
||||||
|
removeAttributeNS(ns, name) {
|
||||||
|
splice(this.attributes, createAttributeFilter(ns, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener(type, handler) {
|
||||||
|
(this.__handlers[toLower(type)] || (this.__handlers[toLower(type)] = [])).push(handler);
|
||||||
|
}
|
||||||
|
removeEventListener(type, handler) {
|
||||||
|
splice(this.__handlers[toLower(type)], handler, 0, true);
|
||||||
|
}
|
||||||
|
dispatchEvent(event) {
|
||||||
|
let t = (event.currentTarget = this),
|
||||||
|
c = event.cancelable,
|
||||||
|
l,
|
||||||
|
i;
|
||||||
|
do {
|
||||||
|
l = t.__handlers[toLower(event.type)];
|
||||||
|
if (l)
|
||||||
|
for (i = l.length; i--; ) {
|
||||||
|
if ((l[i](event) === false || event._end) && c) break;
|
||||||
|
}
|
||||||
|
} while (event.bubbles && !(c && event._stop) && (event.target = t = t.parentNode));
|
||||||
|
return !event.defaultPrevented;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Document extends Element {
|
||||||
|
constructor() {
|
||||||
|
super(9, '#document'); // DOCUMENT_NODE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Event {
|
||||||
|
constructor(type, opts) {
|
||||||
|
this.type = type;
|
||||||
|
this.bubbles = !!opts.bubbles;
|
||||||
|
this.cancelable = !!opts.cancelable;
|
||||||
|
}
|
||||||
|
stopPropagation() {
|
||||||
|
this._stop = true;
|
||||||
|
}
|
||||||
|
stopImmediatePropagation() {
|
||||||
|
this._end = this._stop = true;
|
||||||
|
}
|
||||||
|
preventDefault() {
|
||||||
|
this.defaultPrevented = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createElement(type) {
|
||||||
|
return new Element(null, String(type).toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function createElementNS(ns, type) {
|
||||||
|
let element = createElement(type);
|
||||||
|
element.namespace = ns;
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTextNode(text) {
|
||||||
|
return new Text(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDocument() {
|
||||||
|
let document = new Document();
|
||||||
|
Object.assign(
|
||||||
|
document,
|
||||||
|
(document.defaultView = { document, Document, Node, Text, Element, SVGElement: Element, Event })
|
||||||
|
);
|
||||||
|
Object.assign(document, {
|
||||||
|
documentElement: document,
|
||||||
|
createElement,
|
||||||
|
createElementNS,
|
||||||
|
createTextNode
|
||||||
|
});
|
||||||
|
document.appendChild((document.body = createElement('body')));
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
return createDocument();
|
||||||
|
}
|
||||||
@ -1,158 +1,2 @@
|
|||||||
import { isFunction } from 'trimkit';
|
export { Projector } from './projector.js';
|
||||||
|
export { Scanner } from './scanner.js';
|
||||||
import { supportsPassive } from './utils.js';
|
|
||||||
|
|
||||||
const OVERRIDING_EVENTS = ['contextmenu', 'dragover', 'drop'];
|
|
||||||
function getEventList(element) {
|
|
||||||
const evtString = element.getAttribute('evl');
|
|
||||||
return evtString ? evtString.split(';') : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Projector(domRoot) {
|
|
||||||
const elementMap = new Map();
|
|
||||||
const pendingFrames = [];
|
|
||||||
const eventCallbacks = [];
|
|
||||||
const eventMap = new Map();
|
|
||||||
let runningNextFrame;
|
|
||||||
|
|
||||||
function eventHandler(evt) {
|
|
||||||
const eventName = evt.type;
|
|
||||||
const eventSet = eventMap.get(eventName);
|
|
||||||
if (!eventSet || (evt.target && !eventSet.has(evt.target._id))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (OVERRIDING_EVENTS.includes(eventName)) {
|
|
||||||
evt.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
evt.stopPropagation();
|
|
||||||
|
|
||||||
eventCallbacks.forEach(cb => cb(evt));
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeEvent(eventSet, id, eventName) {
|
|
||||||
eventSet.delete(id);
|
|
||||||
if (!eventSet.size) {
|
|
||||||
domRoot.removeEventListener(eventName, eventHandler);
|
|
||||||
// Probably unnecessary to remove the eventSet from the map.
|
|
||||||
// eventMap.delete(eventName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setAttributes(element, props) {
|
|
||||||
Object.entries(props).forEach(([name, value]) => {
|
|
||||||
if (name in element) {
|
|
||||||
if (name.startsWith('on')) {
|
|
||||||
const eventName = name.substr(2);
|
|
||||||
const eventSet = eventMap.get(eventName) || new Set();
|
|
||||||
const eventList = getEventList(element);
|
|
||||||
if (value === null) {
|
|
||||||
// remove event
|
|
||||||
eventList.splice(eventList.indexOf(eventName), 1);
|
|
||||||
removeEvent(eventSet, element._id, eventName);
|
|
||||||
} else {
|
|
||||||
// add event
|
|
||||||
if (!eventSet.size) {
|
|
||||||
domRoot.addEventListener(
|
|
||||||
eventName,
|
|
||||||
eventHandler,
|
|
||||||
supportsPassive && !OVERRIDING_EVENTS.includes(eventName)
|
|
||||||
? { passive: true, capture: false }
|
|
||||||
: false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
eventList.push(eventName);
|
|
||||||
eventSet.add(element._id);
|
|
||||||
if (!eventMap.has(eventName)) {
|
|
||||||
eventMap.set(eventName, eventSet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
element.setAttribute('evl', eventList.join(';'));
|
|
||||||
} else {
|
|
||||||
element[name] = value;
|
|
||||||
}
|
|
||||||
} else if (value === null) {
|
|
||||||
element.removeAttribute(name);
|
|
||||||
} else {
|
|
||||||
element.setAttribute(name, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createElement({ t: type, n: name, p: props, i: id, c: children }) {
|
|
||||||
let element;
|
|
||||||
if (type === 3) {
|
|
||||||
element = document.createTextNode(props.textContent);
|
|
||||||
} else if (type === 1) {
|
|
||||||
element = document.createElement(name);
|
|
||||||
}
|
|
||||||
elementMap.set((element._id = id), element);
|
|
||||||
setAttributes(element, props);
|
|
||||||
|
|
||||||
for (let i = 0; i < children.length; i++) {
|
|
||||||
element.appendChild(createElement(children[i]));
|
|
||||||
}
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeElement(element) {
|
|
||||||
Array.from(element.childNodes).forEach(removeElement);
|
|
||||||
|
|
||||||
getEventList(element).forEach(eventName => {
|
|
||||||
removeEvent(eventMap.get(eventName), element._id, eventName);
|
|
||||||
});
|
|
||||||
|
|
||||||
element.parentNode.removeChild(element);
|
|
||||||
elementMap.delete(element._id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ACTION_METHODS = [
|
|
||||||
function addElement(parent, data, nextSiblingId) {
|
|
||||||
parent.insertBefore(createElement(data), getElement(nextSiblingId));
|
|
||||||
},
|
|
||||||
setAttributes,
|
|
||||||
removeElement
|
|
||||||
];
|
|
||||||
|
|
||||||
const queueFrame = patchFrame => {
|
|
||||||
if (!patchFrame || !patchFrame.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pendingFrames.unshift(patchFrame);
|
|
||||||
if (!runningNextFrame) {
|
|
||||||
updateFrame();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateFrame = () => {
|
|
||||||
const patches = pendingFrames.pop();
|
|
||||||
if (!patches) {
|
|
||||||
runningNextFrame = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// console.group('PatchSet');
|
|
||||||
let patch;
|
|
||||||
while ((patch = patches.shift())) {
|
|
||||||
// console.log(ACTION_METHODS[patch[0]].name, JSON.stringify(patch));
|
|
||||||
ACTION_METHODS[patch[0]](getElement(patch[1]), patch[2], patch[3]);
|
|
||||||
}
|
|
||||||
// console.groupEnd('PatchSet');
|
|
||||||
|
|
||||||
runningNextFrame = requestAnimationFrame(updateFrame, domRoot);
|
|
||||||
};
|
|
||||||
|
|
||||||
function getElement(id) {
|
|
||||||
return id === null ? domRoot : elementMap.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function subscribe(fn) {
|
|
||||||
eventCallbacks.push(fn);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
queueFrame,
|
|
||||||
getElement,
|
|
||||||
subscribe
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
158
packages/projector/src/projector.js
Normal file
158
packages/projector/src/projector.js
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import { isFunction } from 'trimkit';
|
||||||
|
|
||||||
|
import { supportsPassive } from './utils.js';
|
||||||
|
|
||||||
|
const OVERRIDING_EVENTS = ['contextmenu', 'dragover', 'drop'];
|
||||||
|
function getEventList(element) {
|
||||||
|
const evtString = element.getAttribute('evl');
|
||||||
|
return evtString ? evtString.split(';') : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Projector(domRoot) {
|
||||||
|
const elementMap = new Map();
|
||||||
|
const pendingFrames = [];
|
||||||
|
const eventCallbacks = [];
|
||||||
|
const eventMap = new Map();
|
||||||
|
let runningNextFrame;
|
||||||
|
|
||||||
|
function eventHandler(evt) {
|
||||||
|
const eventName = evt.type;
|
||||||
|
const eventSet = eventMap.get(eventName);
|
||||||
|
if (!eventSet || (evt.target && !eventSet.has(evt.target._id))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OVERRIDING_EVENTS.includes(eventName)) {
|
||||||
|
evt.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
evt.stopPropagation();
|
||||||
|
|
||||||
|
eventCallbacks.forEach(cb => cb(evt));
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeEvent(eventSet, id, eventName) {
|
||||||
|
eventSet.delete(id);
|
||||||
|
if (!eventSet.size) {
|
||||||
|
domRoot.removeEventListener(eventName, eventHandler);
|
||||||
|
// Probably unnecessary to remove the eventSet from the map.
|
||||||
|
// eventMap.delete(eventName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAttributes(element, props) {
|
||||||
|
Object.entries(props).forEach(([name, value]) => {
|
||||||
|
if (name in element) {
|
||||||
|
if (name.startsWith('on')) {
|
||||||
|
const eventName = name.substr(2);
|
||||||
|
const eventSet = eventMap.get(eventName) || new Set();
|
||||||
|
const eventList = getEventList(element);
|
||||||
|
if (value === null) {
|
||||||
|
// remove event
|
||||||
|
eventList.splice(eventList.indexOf(eventName), 1);
|
||||||
|
removeEvent(eventSet, element._id, eventName);
|
||||||
|
} else {
|
||||||
|
// add event
|
||||||
|
if (!eventSet.size) {
|
||||||
|
domRoot.addEventListener(
|
||||||
|
eventName,
|
||||||
|
eventHandler,
|
||||||
|
supportsPassive && !OVERRIDING_EVENTS.includes(eventName)
|
||||||
|
? { passive: true, capture: false }
|
||||||
|
: false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
eventList.push(eventName);
|
||||||
|
eventSet.add(element._id);
|
||||||
|
if (!eventMap.has(eventName)) {
|
||||||
|
eventMap.set(eventName, eventSet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
element.setAttribute('evl', eventList.join(';'));
|
||||||
|
} else {
|
||||||
|
element[name] = value;
|
||||||
|
}
|
||||||
|
} else if (value === null) {
|
||||||
|
element.removeAttribute(name);
|
||||||
|
} else {
|
||||||
|
element.setAttribute(name, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createElement({ t: type, n: name, p: props, i: id, c: children }) {
|
||||||
|
let element;
|
||||||
|
if (type === 3) {
|
||||||
|
element = document.createTextNode(props.textContent);
|
||||||
|
} else if (type === 1) {
|
||||||
|
element = document.createElement(name);
|
||||||
|
}
|
||||||
|
elementMap.set((element._id = id), element);
|
||||||
|
setAttributes(element, props);
|
||||||
|
|
||||||
|
for (let i = 0; i < children.length; i++) {
|
||||||
|
element.appendChild(createElement(children[i]));
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeElement(element) {
|
||||||
|
Array.from(element.childNodes).forEach(removeElement);
|
||||||
|
|
||||||
|
getEventList(element).forEach(eventName => {
|
||||||
|
removeEvent(eventMap.get(eventName), element._id, eventName);
|
||||||
|
});
|
||||||
|
|
||||||
|
element.parentNode.removeChild(element);
|
||||||
|
elementMap.delete(element._id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTION_METHODS = [
|
||||||
|
function addElement(parent, data, nextSiblingId) {
|
||||||
|
parent.insertBefore(createElement(data), getElement(nextSiblingId));
|
||||||
|
},
|
||||||
|
setAttributes,
|
||||||
|
removeElement
|
||||||
|
];
|
||||||
|
|
||||||
|
const queueFrame = patchFrame => {
|
||||||
|
if (!patchFrame || !patchFrame.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingFrames.unshift(patchFrame);
|
||||||
|
if (!runningNextFrame) {
|
||||||
|
updateFrame();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFrame = () => {
|
||||||
|
const patches = pendingFrames.pop();
|
||||||
|
if (!patches) {
|
||||||
|
runningNextFrame = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// console.group('PatchSet');
|
||||||
|
let patch;
|
||||||
|
while ((patch = patches.shift())) {
|
||||||
|
// console.log(ACTION_METHODS[patch[0]].name, JSON.stringify(patch));
|
||||||
|
ACTION_METHODS[patch[0]](getElement(patch[1]), patch[2], patch[3]);
|
||||||
|
}
|
||||||
|
// console.groupEnd('PatchSet');
|
||||||
|
|
||||||
|
runningNextFrame = requestAnimationFrame(updateFrame, domRoot);
|
||||||
|
};
|
||||||
|
|
||||||
|
function getElement(id) {
|
||||||
|
return id === null ? domRoot : elementMap.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribe(fn) {
|
||||||
|
eventCallbacks.push(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
queueFrame,
|
||||||
|
getElement,
|
||||||
|
subscribe
|
||||||
|
};
|
||||||
|
}
|
||||||
1
packages/projector/src/scanner.js
Normal file
1
packages/projector/src/scanner.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { CreateDocument } from './document.js';
|
||||||
Loading…
x
Reference in New Issue
Block a user