UNPKG

@joshuafcole/fluorine

Version:

A highly reactive, optionally compiled, and strongly typed view layer for TypeScript.

477 lines (475 loc) 18 kB
"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