@joshuafcole/fluorine
Version:
A highly reactive, optionally compiled, and strongly typed view layer for TypeScript.
477 lines (475 loc) • 18 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("efreet/utils");
var PropType;
(function (PropType) {
PropType[PropType["Property"] = 0] = "Property";
PropType[PropType["Attribute"] = 1] = "Attribute";
PropType[PropType["Event"] = 2] = "Event";
PropType[PropType["Lifecycle"] = 3] = "Lifecycle";
})(PropType = exports.PropType || (exports.PropType = {}));
exports.EMPTY_ELEM = {};
const namespace_urls = {
xlink: "http://www.w3.org/1999/xlink"
};
function set_attribute(node, property, value) {
let [ns, prop] = property.split(":");
if (prop === undefined)
[prop, ns] = [ns, prop];
if (ns)
console.log(ns, prop, value);
if (value === false || value === undefined)
ns
? node.removeAttributeNS(namespace_urls[ns], prop)
: node.removeAttribute(property);
else if (value === true)
ns
? node.setAttributeNS(namespace_urls[ns], prop, "")
: node.setAttribute(property, "");
else
ns
? node.setAttributeNS(namespace_urls[ns], prop, value)
: node.setAttribute(property, value);
}
class Renderer {
constructor() {
this.prop_map = {
// DEBUG
_id: PropType.Attribute,
_ix: PropType.Attribute,
// Content
class: PropType.Attribute,
type: PropType.Attribute,
disabled: PropType.Attribute,
spellcheck: PropType.Attribute,
placeholder: PropType.Attribute,
tabindex: PropType.Attribute,
title: PropType.Attribute,
href: PropType.Attribute,
src: PropType.Attribute,
target: PropType.Attribute,
download: PropType.Attribute,
allowfullscreen: PropType.Attribute,
for: PropType.Attribute,
name: PropType.Attribute,
id: PropType.Attribute,
selected: PropType.Attribute,
checked: PropType.Attribute,
// style: PropType.Property,
// Mouse Hooks
click: PropType.Event,
dblclick: PropType.Event,
contextmenu: PropType.Event,
mousedown: PropType.Event,
mouseup: PropType.Event,
mousemove: PropType.Event,
mouseover: PropType.Event,
mouseout: PropType.Event,
mouseenter: PropType.Event,
mouseleave: PropType.Event,
wheel: PropType.Event,
// Touch Hooks
touchstart: PropType.Event,
touchend: PropType.Event,
touchcancel: PropType.Event,
touchmove: PropType.Event,
// Drag-n-Drop Hooks
dragstart: PropType.Event,
dragover: PropType.Event,
drag: PropType.Event,
dragend: PropType.Event,
drop: PropType.Event,
// Keyboard Hooks
keyup: PropType.Event,
keydown: PropType.Event,
// Misc. Hooks
focus: PropType.Event,
blur: PropType.Event,
focusin: PropType.Event,
focusout: PropType.Event,
scroll: PropType.Event,
change: PropType.Event,
input: PropType.Event,
cut: PropType.Event,
copy: PropType.Event,
paste: PropType.Event,
// Animation Hooks
transitionstart: PropType.Event,
transitionrun: PropType.Event,
transitioncancel: PropType.Event,
transitionend: PropType.Event,
animationstart: PropType.Event,
animationiteration: PropType.Event,
animationcancel: PropType.Event,
animationend: PropType.Event,
// Lifecycle
postrender: PropType.Lifecycle
};
this.differs = {
static: this.static_diff,
text: this.text_diff,
dynamic: this.dynamic_diff
};
this.updaters = {
static: this.static_update,
text: this.text_update,
dynamic: this.dynamic_update
};
this.container = document.createElement("div");
this.node_cache = {
__root: this.container,
head: document.head
};
this.tree = {};
this.render_roots = () => [];
this.debug = { tree: false, perf: false, events: false };
this.handle_event = (event) => {
let id = (event.currentTarget || event.target)._id;
let elem = this.tree[id];
if (!elem)
return;
let handler = elem[event.type];
if (handler) {
if (typeof handler === "function")
handler(event, elem);
else
handler.compile()(event, elem);
}
};
this.force_draw = () => {
let start = utils_1.now();
let roots = this.render_roots();
let root_time = utils_1.now();
this.render(roots);
if (this.debug.perf) {
console.info("Render time", utils_1.now() - root_time, "Gen roots time", root_time - start);
}
this.queued = false;
};
this.container.className = "__root";
this.container._id = "__root";
}
reset() {
this.prev_tree = this.tree;
this.tree = {};
this.postrenders = [];
}
prepare(root) {
let start = utils_1.now();
let elem_count = 1;
let { tree } = this;
let elements = [root];
for (let elem_ix = 0; elem_ix < elem_count; elem_ix += 1) {
let cur = elements[elem_ix];
if (cur.parent === undefined)
cur.parent = "__root";
if (cur._id === undefined)
cur._id = "__root__" + elem_ix;
tree[cur._id] = cur;
let { children = utils_1.EMPTY } = cur;
let child_count = 0;
for (let maybe_child of children) {
if (maybe_child === undefined)
continue;
if (typeof maybe_child === "string" ||
typeof maybe_child === "number") {
elements.push(this.prepare_child(cur._id, { kind: "text", textContent: "" + maybe_child }, child_count++));
}
else if (!(maybe_child instanceof Array)) {
elements.push(this.prepare_child(cur._id, maybe_child, child_count++));
}
else {
for (let sub_child of maybe_child) {
if (sub_child === undefined)
continue;
if (typeof sub_child === "string" ||
typeof sub_child === "number") {
elements.push(this.prepare_child(cur._id, { kind: "text", textContent: "" + sub_child }, child_count++));
}
else if (!(sub_child instanceof Array)) {
elements.push(this.prepare_child(cur._id, sub_child, child_count++));
}
else {
throw new Error("Fluorine allows one one layer of array-based child nesting (make sure to flatten any nested maps).");
}
}
}
}
elem_count += child_count;
}
if (this.debug.perf)
console.log(`Prepared (${elem_count}) in ${utils_1.now() - start}`);
return tree;
}
prepare_child(parent_id, child, child_ix) {
if (child._id === undefined)
child._id = `${parent_id}__${child_ix}`;
if (child.ix === undefined)
child.ix = child_ix;
if (child.parent === undefined)
child.parent = parent_id;
return child;
}
domify() {
let { differs, prev_tree, tree } = this;
for (let id of Object.keys(prev_tree)) {
if (!tree[id]) {
// Remove
let node = this.node_cache[id];
if (node.parentNode)
node.parentNode.removeChild(node);
}
}
for (let id of Object.keys(tree)) {
let cur = tree[id];
let prev = prev_tree[id];
if (!prev) {
// Add
let node;
if (cur.svg)
node = document.createElementNS("http://www.w3.org/2000/svg", cur.tagname || "div");
else if (cur.kind === "text")
node = document.createTextNode(cur.textContent);
else
node = document.createElement(cur.tagname || "div");
this.node_cache[id] = node;
node._id = id;
prev = exports.EMPTY_ELEM;
}
else if (cur.tagname !== prev.tagname) {
// Replace
let old_node = this.node_cache[id];
if (old_node.parentNode)
old_node.parentNode.removeChild(old_node);
let new_node;
if (cur.svg)
new_node = document.createElementNS("http://www.w3.org/2000/svg", cur.tagname || "div");
else if (cur.kind === "text")
new_node = document.createTextNode(cur.textContent);
else
new_node = document.createElement(cur.tagname || "div");
this.node_cache[id] = new_node;
new_node._id = id;
// @FIXME: If it isn't correct to always omit old children, we need to topo sort the inputs and go top down.
// while (old_node.firstChild) new_node.appendChild(old_node.firstChild);
let old_kids = prev.children;
prev = exports.EMPTY_ELEM;
prev.children = old_kids;
let child_ix = 0;
let child_count = old_kids ? old_kids.length : 0;
while (old_node.firstChild) {
let child = old_node.firstChild;
let cur_kid = (old_kids || utils_1.EMPTY)[child_ix];
if (child_ix >= child_count || !cur_kid) {
old_node.removeChild(child);
}
else if ((child instanceof Text && child.textContent === cur_kid) ||
child._id === cur_kid._id) {
new_node.appendChild(child);
}
child_ix += 1;
}
}
if (cur.ix !== prev.ix || cur.parent !== prev.parent) {
// Move
let node = this.node_cache[id];
this._move_elem(id, cur, node);
if (this.debug.tree && node.nodeName !== "#text") {
node.setAttribute("_id", cur._id);
node.setAttribute("ix", "" + cur.ix);
}
}
if (differs[cur.kind](cur, prev)) {
// Update
let node = this.node_cache[id];
this.updaters[cur.kind](cur, prev, node, this);
if (cur.postrender)
this.postrenders.push(id);
}
if (cur.focused && cur.focused !== prev.focused) {
// Focus
this.to_focus = id;
}
}
}
_move_elem(id, cur, node) {
let parent_node = this.node_cache[cur.parent];
if (parent_node) {
if (cur.ix >= parent_node.children.length) {
parent_node.appendChild(node);
}
else {
parent_node.insertBefore(node, parent_node.children[cur.ix]);
}
}
if (cur.postrender)
this.postrenders.push(id);
}
postdomify() {
for (let id of this.postrenders) {
let node = this.node_cache[id];
let cur = this.tree[id];
cur.postrender(cur, node);
}
if (this.to_focus) {
let id = this.to_focus;
this.to_focus = undefined;
let node = this.node_cache[id];
if (node.focus) {
setTimeout(() => {
node.focus();
if ("value" in node) {
let input = node;
input.selectionStart = input.selectionEnd = (input.value || "").length;
}
// if (typeof window.getSelection != "undefined"
// && typeof document.createRange != "undefined") {
// let range = document.createRange();
// range.selectNodeContents(node);
// range.collapse(false);
// let sel = window.getSelection();
// sel.removeAllRanges();
// sel.addRange(range);
// }
}, 0);
}
}
}
redraw() {
if (!this.queued) {
this.queued = true;
requestAnimationFrame(this.force_draw);
}
}
render(elems) {
this.reset();
// We sort elements by depth to allow them to be self referential.
elems.sort((a, b) => (a.parent ? a.parent.split("__").length : 0) -
(b.parent ? b.parent.split("__").length : 0));
let start = utils_1.now();
for (let elem of elems)
this.prepare(elem);
let prepare = utils_1.now();
this.domify();
let domify = utils_1.now();
this.postdomify();
let postDomify = utils_1.now();
let time = utils_1.now() - start;
if (time > 15) {
console.info("slow render (> 15ms): ", time, {
prepare: prepare - start,
domify: domify - prepare,
postDomify: postDomify - domify
});
}
}
static_diff(cur, old) {
return !!cur.dirty;
}
static_update(cur, old, node) { }
text_diff(cur, old) {
return cur.textContent !== old.textContent;
}
text_update(cur, old, node) {
if (cur.textContent !== old.textContent)
node.textContent = cur.textContent;
}
dynamic_diff(cur, old) {
for (let prop in cur) {
if (cur[prop] !== old[prop])
return true;
}
for (let prop in old) {
if (cur[prop] !== old[prop])
return true;
}
return false;
}
dynamic_update(cur, old, node, lib) {
for (let prop in cur) {
if (prop === "children" ||
prop === "ix" ||
prop === "parent" ||
prop === "tagname")
continue;
let type = lib.prop_map[prop];
if (cur[prop] !== old[prop]) {
if (type === PropType.Attribute || (cur.svg && type === undefined)) {
set_attribute(node, prop, cur[prop]);
}
else if (type === PropType.Event) {
// node["on" + prop] = lib.handle_event;
node.addEventListener(prop, lib.handle_event);
}
else {
node[prop] = cur[prop];
}
}
}
for (let prop in old) {
if (cur[prop] !== undefined)
continue;
let type = lib.prop_map[prop];
if (type === PropType.Attribute) {
node.removeAttribute(prop);
}
else if (type === PropType.Event) {
// node["on" + prop] = undefined;
node.removeEventListener(prop, old[prop]);
}
else if (prop !== "children") {
node[prop] = undefined;
}
}
}
specialize(kind, properties) {
if (this.differs[kind])
return;
let start = utils_1.now();
if (properties.length === 0) {
this.differs[kind] = this.static_diff;
this.updaters[kind] = this.static_update;
return;
}
let is_svg = !!properties.filter(prop => prop === "svg").length;
let prop_checks = properties.map(prop => `cur["${prop}"] !== old["${prop}"]`);
let diff = new Function("cur", "old", utils_1.pad_chunk(2) `
if(cur.dirty ||
${prop_checks.join(" ||\n")}) {
return true;
}
return false;
`);
let prop_updaters = properties.map(prop => {
let update = `node["${prop}"] = cur["${prop}"];`;
let type = this.prop_map[prop];
if (type === PropType.Attribute || (type === undefined && is_svg)) {
update = utils_1.inline_chunk `
if(cur["${prop}"] === undefined || cur["${prop}"] === false) node.removeAttribute("${prop}");
else node.setAttribute("${prop}", cur["${prop}"] === true ? "" : cur["${prop}"]);
`;
}
else if (type === PropType.Event) {
update = utils_1.inline_chunk `
node["${"on" +
prop}"] = cur["${prop}"] ? lib.handle_event : undefined;
`;
}
else if (type === PropType.Lifecycle) {
// This is a no-op, lifecycle events are managed using the element, not the node.
}
return utils_1.inline_chunk `
if(cur["${prop}"] !== old["${prop}"]) {
${update}
}
`;
});
let update = new Function("cur", "old", "node", "lib", utils_1.pad_chunk(2) `
${prop_updaters.join("\n")}
`);
this.differs[kind] = diff;
this.updaters[kind] = update;
}
}
exports.Renderer = Renderer;
//# sourceMappingURL=renderer.js.map