Pull in undom. We'll be modifying it.

This commit is contained in:
Timothy Farrell 2017-01-31 16:17:08 -06:00
parent 8a5d746412
commit 8bd0937988
4 changed files with 373 additions and 158 deletions

View 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();
}

View File

@ -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
};
}

View 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
};
}

View File

@ -0,0 +1 @@
export { CreateDocument } from './document.js';