diff --git a/.gitignore b/.gitignore index 6c739de..79fbad0 100644 --- a/.gitignore +++ b/.gitignore @@ -29,5 +29,29 @@ node_modules # Vim *.swp +# Generated JavaScript test/browserified.js browserified.js +h.d.ts +h.js +h.js.map +htmldomapi.d.ts +htmldomapi.js +htmldomapi.js.map +is.d.ts +is.js +is.js.map +snabbdom.bundle.d.ts +snabbdom.bundle.js +snabbdom.bundle.js.map +snabbdom.d.ts +snabbdom.js +snabbdom.js.map +thunk.d.ts +thunk.js +thunk.js.map +vnode.d.ts +vnode.js +vnode.js.map +/modules +/helpers diff --git a/h.js b/h.js deleted file mode 100644 index ac07850..0000000 --- a/h.js +++ /dev/null @@ -1,34 +0,0 @@ -var VNode = require('./vnode'); -var is = require('./is'); - -function addNS(data, children, sel) { - data.ns = 'http://www.w3.org/2000/svg'; - - if (sel !== 'foreignObject' && children !== undefined) { - for (var i = 0; i < children.length; ++i) { - addNS(children[i].data, children[i].children, children[i].sel); - } - } -} - -module.exports = function h(sel, b, c) { - var data = {}, children, text, i; - if (c !== undefined) { - data = b; - if (is.array(c)) { children = c; } - else if (is.primitive(c)) { text = c; } - } else if (b !== undefined) { - if (is.array(b)) { children = b; } - else if (is.primitive(b)) { text = b; } - else { data = b; } - } - if (is.array(children)) { - for (i = 0; i < children.length; ++i) { - if (is.primitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i]); - } - } - if (sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g') { - addNS(data, children, sel); - } - return VNode(sel, data, children, text, undefined); -}; diff --git a/htmldomapi.js b/htmldomapi.js deleted file mode 100644 index c73f29a..0000000 --- a/htmldomapi.js +++ /dev/null @@ -1,54 +0,0 @@ -function createElement(tagName){ - return document.createElement(tagName); -} - -function createElementNS(namespaceURI, qualifiedName){ - return document.createElementNS(namespaceURI, qualifiedName); -} - -function createTextNode(text){ - return document.createTextNode(text); -} - - -function insertBefore(parentNode, newNode, referenceNode){ - parentNode.insertBefore(newNode, referenceNode); -} - - -function removeChild(node, child){ - node.removeChild(child); -} - -function appendChild(node, child){ - node.appendChild(child); -} - -function parentNode(node){ - return node.parentElement; -} - -function nextSibling(node){ - return node.nextSibling; -} - -function tagName(node){ - return node.tagName; -} - -function setTextContent(node, text){ - node.textContent = text; -} - -module.exports = { - createElement: createElement, - createElementNS: createElementNS, - createTextNode: createTextNode, - appendChild: appendChild, - removeChild: removeChild, - insertBefore: insertBefore, - parentNode: parentNode, - nextSibling: nextSibling, - tagName: tagName, - setTextContent: setTextContent -}; diff --git a/is.js b/is.js deleted file mode 100644 index c228508..0000000 --- a/is.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - array: Array.isArray, - primitive: function(s) { return typeof s === 'string' || typeof s === 'number'; }, -}; diff --git a/modules/class.js b/modules/class.js deleted file mode 100644 index 35e19de..0000000 --- a/modules/class.js +++ /dev/null @@ -1,23 +0,0 @@ -function updateClass(oldVnode, vnode) { - var cur, name, elm = vnode.elm, - oldClass = oldVnode.data.class, - klass = vnode.data.class; - - if (!oldClass && !klass) return; - oldClass = oldClass || {}; - klass = klass || {}; - - for (name in oldClass) { - if (!klass[name]) { - elm.classList.remove(name); - } - } - for (name in klass) { - cur = klass[name]; - if (cur !== oldClass[name]) { - elm.classList[cur ? 'add' : 'remove'](name); - } - } -} - -module.exports = {create: updateClass, update: updateClass}; diff --git a/modules/dataset.js b/modules/dataset.js deleted file mode 100644 index 37e573b..0000000 --- a/modules/dataset.js +++ /dev/null @@ -1,23 +0,0 @@ -function updateDataset(oldVnode, vnode) { - var elm = vnode.elm, - oldDataset = oldVnode.data.dataset, - dataset = vnode.data.dataset, - key - - if (!oldDataset && !dataset) return; - oldDataset = oldDataset || {}; - dataset = dataset || {}; - - for (key in oldDataset) { - if (!dataset[key]) { - delete elm.dataset[key]; - } - } - for (key in dataset) { - if (oldDataset[key] !== dataset[key]) { - elm.dataset[key] = dataset[key]; - } - } -} - -module.exports = {create: updateDataset, update: updateDataset} diff --git a/modules/props.js b/modules/props.js deleted file mode 100644 index 843c8a8..0000000 --- a/modules/props.js +++ /dev/null @@ -1,23 +0,0 @@ -function updateProps(oldVnode, vnode) { - var key, cur, old, elm = vnode.elm, - oldProps = oldVnode.data.props, props = vnode.data.props; - - if (!oldProps && !props) return; - oldProps = oldProps || {}; - props = props || {}; - - for (key in oldProps) { - if (!props[key]) { - delete elm[key]; - } - } - for (key in props) { - cur = props[key]; - old = oldProps[key]; - if (old !== cur && (key !== 'value' || elm[key] !== cur)) { - elm[key] = cur; - } - } -} - -module.exports = {create: updateProps, update: updateProps}; diff --git a/modules/style.js b/modules/style.js deleted file mode 100644 index e03b633..0000000 --- a/modules/style.js +++ /dev/null @@ -1,69 +0,0 @@ -var raf = (typeof window !== 'undefined' && window.requestAnimationFrame) || setTimeout; -var nextFrame = function(fn) { raf(function() { raf(fn); }); }; - -function setNextFrame(obj, prop, val) { - nextFrame(function() { obj[prop] = val; }); -} - -function updateStyle(oldVnode, vnode) { - var cur, name, elm = vnode.elm, - oldStyle = oldVnode.data.style, - style = vnode.data.style; - - if (!oldStyle && !style) return; - oldStyle = oldStyle || {}; - style = style || {}; - var oldHasDel = 'delayed' in oldStyle; - - for (name in oldStyle) { - if (!style[name]) { - elm.style[name] = ''; - } - } - for (name in style) { - cur = style[name]; - if (name === 'delayed') { - for (name in style.delayed) { - cur = style.delayed[name]; - if (!oldHasDel || cur !== oldStyle.delayed[name]) { - setNextFrame(elm.style, name, cur); - } - } - } else if (name !== 'remove' && cur !== oldStyle[name]) { - elm.style[name] = cur; - } - } -} - -function applyDestroyStyle(vnode) { - var style, name, elm = vnode.elm, s = vnode.data.style; - if (!s || !(style = s.destroy)) return; - for (name in style) { - elm.style[name] = style[name]; - } -} - -function applyRemoveStyle(vnode, rm) { - var s = vnode.data.style; - if (!s || !s.remove) { - rm(); - return; - } - var name, elm = vnode.elm, idx, i = 0, maxDur = 0, - compStyle, style = s.remove, amount = 0, applied = []; - for (name in style) { - applied.push(name); - elm.style[name] = style[name]; - } - compStyle = getComputedStyle(elm); - var props = compStyle['transition-property'].split(', '); - for (; i < props.length; ++i) { - if(applied.indexOf(props[i]) !== -1) amount++; - } - elm.addEventListener('transitionend', function(ev) { - if (ev.target === elm) --amount; - if (amount === 0) rm(); - }); -} - -module.exports = {create: updateStyle, update: updateStyle, destroy: applyDestroyStyle, remove: applyRemoveStyle}; diff --git a/package.json b/package.json index 1aceea4..269e48f 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.5.4", "description": "A virtual DOM library with focus on simplicity, modularity, powerful features and performance.", "main": "snabbdom.js", - "typings": "type-definitions/snabbdom.d.ts", + "typings": "snabbdom.d.ts", "directories": { "example": "examples", "test": "test" @@ -20,10 +20,14 @@ "gulp-uglify": "^1.5.3", "knuth-shuffle": "^1.0.1", "testem": "^1.0.2", + "typescript": "^2.0.6", "xyz": "0.5.x" }, "scripts": { + "pretest": "npm run compile", "test": "testem", + "compile": "tsc", + "prepublish": "npm run compile", "release-major": "xyz --repo git@github.com:paldepind/snabbdom.git --increment major", "release-minor": "xyz --repo git@github.com:paldepind/snabbdom.git --increment minor", "release-patch": "xyz --repo git@github.com:paldepind/snabbdom.git --increment patch" diff --git a/snabbdom.bundle.js b/snabbdom.bundle.js deleted file mode 100644 index 2f26dec..0000000 --- a/snabbdom.bundle.js +++ /dev/null @@ -1,11 +0,0 @@ -var snabbdom = require('./snabbdom'); -var patch = snabbdom.init([ // Init patch function with choosen modules - require('./modules/attributes'), // makes it easy to toggle classes - require('./modules/class'), // makes it easy to toggle classes - require('./modules/props'), // for setting properties on DOM elements - require('./modules/style'), // handles styling on elements with support for animations - require('./modules/eventlisteners'), // attaches event listeners -]); -var h = require('./h'); // helper function for creating vnodes - -module.exports = { patch: patch, h: h } diff --git a/snabbdom.js b/snabbdom.js deleted file mode 100644 index 1fcc2ed..0000000 --- a/snabbdom.js +++ /dev/null @@ -1,260 +0,0 @@ -// jshint newcap: false -/* global require, module, document, Node */ -'use strict'; - -var VNode = require('./vnode'); -var is = require('./is'); -var domApi = require('./htmldomapi'); - -function isUndef(s) { return s === undefined; } -function isDef(s) { return s !== undefined; } - -var emptyNode = VNode('', {}, [], undefined, undefined); - -function sameVnode(vnode1, vnode2) { - return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; -} - -function createKeyToOldIdx(children, beginIdx, endIdx) { - var i, map = {}, key; - for (i = beginIdx; i <= endIdx; ++i) { - key = children[i].key; - if (isDef(key)) map[key] = i; - } - return map; -} - -var hooks = ['create', 'update', 'remove', 'destroy', 'pre', 'post']; - -function init(modules, api) { - var i, j, cbs = {}; - - if (isUndef(api)) api = domApi; - - for (i = 0; i < hooks.length; ++i) { - cbs[hooks[i]] = []; - for (j = 0; j < modules.length; ++j) { - if (modules[j][hooks[i]] !== undefined) cbs[hooks[i]].push(modules[j][hooks[i]]); - } - } - - function emptyNodeAt(elm) { - var id = elm.id ? '#' + elm.id : ''; - var c = elm.className ? '.' + elm.className.split(' ').join('.') : ''; - return VNode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm); - } - - function createRmCb(childElm, listeners) { - return function() { - if (--listeners === 0) { - var parent = api.parentNode(childElm); - api.removeChild(parent, childElm); - } - }; - } - - function createElm(vnode, insertedVnodeQueue) { - var i, data = vnode.data; - if (isDef(data)) { - if (isDef(i = data.hook) && isDef(i = i.init)) { - i(vnode); - data = vnode.data; - } - } - var elm, children = vnode.children, sel = vnode.sel; - if (isDef(sel)) { - // Parse selector - var hashIdx = sel.indexOf('#'); - var dotIdx = sel.indexOf('.', hashIdx); - var hash = hashIdx > 0 ? hashIdx : sel.length; - var dot = dotIdx > 0 ? dotIdx : sel.length; - var tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel; - elm = vnode.elm = isDef(data) && isDef(i = data.ns) ? api.createElementNS(i, tag) - : api.createElement(tag); - if (hash < dot) elm.id = sel.slice(hash + 1, dot); - if (dotIdx > 0) elm.className = sel.slice(dot + 1).replace(/\./g, ' '); - if (is.array(children)) { - for (i = 0; i < children.length; ++i) { - api.appendChild(elm, createElm(children[i], insertedVnodeQueue)); - } - } else if (is.primitive(vnode.text)) { - api.appendChild(elm, api.createTextNode(vnode.text)); - } - for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode); - i = vnode.data.hook; // Reuse variable - if (isDef(i)) { - if (i.create) i.create(emptyNode, vnode); - if (i.insert) insertedVnodeQueue.push(vnode); - } - } else { - elm = vnode.elm = api.createTextNode(vnode.text); - } - return vnode.elm; - } - - function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) { - for (; startIdx <= endIdx; ++startIdx) { - api.insertBefore(parentElm, createElm(vnodes[startIdx], insertedVnodeQueue), before); - } - } - - function invokeDestroyHook(vnode) { - var i, j, data = vnode.data; - if (isDef(data)) { - if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode); - for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode); - if (isDef(i = vnode.children)) { - for (j = 0; j < vnode.children.length; ++j) { - invokeDestroyHook(vnode.children[j]); - } - } - } - } - - function removeVnodes(parentElm, vnodes, startIdx, endIdx) { - for (; startIdx <= endIdx; ++startIdx) { - var i, listeners, rm, ch = vnodes[startIdx]; - if (isDef(ch)) { - if (isDef(ch.sel)) { - invokeDestroyHook(ch); - listeners = cbs.remove.length + 1; - rm = createRmCb(ch.elm, listeners); - for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm); - if (isDef(i = ch.data) && isDef(i = i.hook) && isDef(i = i.remove)) { - i(ch, rm); - } else { - rm(); - } - } else { // Text node - api.removeChild(parentElm, ch.elm); - } - } - } - } - - function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) { - var oldStartIdx = 0, newStartIdx = 0; - var oldEndIdx = oldCh.length - 1; - var oldStartVnode = oldCh[0]; - var oldEndVnode = oldCh[oldEndIdx]; - var newEndIdx = newCh.length - 1; - var newStartVnode = newCh[0]; - var newEndVnode = newCh[newEndIdx]; - var oldKeyToIdx, idxInOld, elmToMove, before; - - while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { - if (isUndef(oldStartVnode)) { - oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left - } else if (isUndef(oldEndVnode)) { - oldEndVnode = oldCh[--oldEndIdx]; - } else if (sameVnode(oldStartVnode, newStartVnode)) { - patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); - oldStartVnode = oldCh[++oldStartIdx]; - newStartVnode = newCh[++newStartIdx]; - } else if (sameVnode(oldEndVnode, newEndVnode)) { - patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); - oldEndVnode = oldCh[--oldEndIdx]; - newEndVnode = newCh[--newEndIdx]; - } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right - patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); - api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm)); - oldStartVnode = oldCh[++oldStartIdx]; - newEndVnode = newCh[--newEndIdx]; - } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left - patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); - api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); - oldEndVnode = oldCh[--oldEndIdx]; - newStartVnode = newCh[++newStartIdx]; - } else { - if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); - idxInOld = oldKeyToIdx[newStartVnode.key]; - if (isUndef(idxInOld)) { // New element - api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); - newStartVnode = newCh[++newStartIdx]; - } else { - elmToMove = oldCh[idxInOld]; - patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); - oldCh[idxInOld] = undefined; - api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm); - newStartVnode = newCh[++newStartIdx]; - } - } - } - if (oldStartIdx > oldEndIdx) { - before = isUndef(newCh[newEndIdx+1]) ? null : newCh[newEndIdx+1].elm; - addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); - } else if (newStartIdx > newEndIdx) { - removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); - } - } - - function patchVnode(oldVnode, vnode, insertedVnodeQueue) { - var i, hook; - if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) { - i(oldVnode, vnode); - } - var elm = vnode.elm = oldVnode.elm, oldCh = oldVnode.children, ch = vnode.children; - if (oldVnode === vnode) return; - if (!sameVnode(oldVnode, vnode)) { - var parentElm = api.parentNode(oldVnode.elm); - elm = createElm(vnode, insertedVnodeQueue); - api.insertBefore(parentElm, elm, oldVnode.elm); - removeVnodes(parentElm, [oldVnode], 0, 0); - return; - } - if (isDef(vnode.data)) { - for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); - i = vnode.data.hook; - if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode); - } - if (isUndef(vnode.text)) { - if (isDef(oldCh) && isDef(ch)) { - if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue); - } else if (isDef(ch)) { - if (isDef(oldVnode.text)) api.setTextContent(elm, ''); - addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); - } else if (isDef(oldCh)) { - removeVnodes(elm, oldCh, 0, oldCh.length - 1); - } else if (isDef(oldVnode.text)) { - api.setTextContent(elm, ''); - } - } else if (oldVnode.text !== vnode.text) { - api.setTextContent(elm, vnode.text); - } - if (isDef(hook) && isDef(i = hook.postpatch)) { - i(oldVnode, vnode); - } - } - - return function(oldVnode, vnode) { - var i, elm, parent; - var insertedVnodeQueue = []; - for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); - - if (isUndef(oldVnode.sel)) { - oldVnode = emptyNodeAt(oldVnode); - } - - if (sameVnode(oldVnode, vnode)) { - patchVnode(oldVnode, vnode, insertedVnodeQueue); - } else { - elm = oldVnode.elm; - parent = api.parentNode(elm); - - createElm(vnode, insertedVnodeQueue); - - if (parent !== null) { - api.insertBefore(parent, vnode.elm, api.nextSibling(elm)); - removeVnodes(parent, [oldVnode], 0, 0); - } - } - - for (i = 0; i < insertedVnodeQueue.length; ++i) { - insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]); - } - for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); - return vnode; - }; -} - -module.exports = {init: init}; diff --git a/src/h.ts b/src/h.ts new file mode 100644 index 0000000..7d387fb --- /dev/null +++ b/src/h.ts @@ -0,0 +1,42 @@ +import {VNode} from './interfaces'; +import vnode = require('./vnode'); +import is = require('./is'); + +function addNS(data: any, children: Array | undefined, sel: string | undefined): void { + data.ns = 'http://www.w3.org/2000/svg'; + if (sel !== 'foreignObject' && children !== undefined) { + for (let i = 0; i < children.length; ++i) { + addNS(children[i].data, (children[i] as VNode).children as Array, children[i].sel); + } + } +} + +function h(sel: string): VNode; +function h(sel: string, data: any): VNode; +function h(sel: string, text: string): VNode; +function h(sel: string, children: Array): VNode; +function h(sel: string, data: any, text: string): VNode; +function h(sel: string, data: any, children: Array): VNode; +function h(sel: any, b?: any, c?: any): VNode { + var data = {}, children: any, text: any, i: number; + if (c !== undefined) { + data = b; + if (is.array(c)) { children = c; } + else if (is.primitive(c)) { text = c; } + } else if (b !== undefined) { + if (is.array(b)) { children = b; } + else if (is.primitive(b)) { text = b; } + else { data = b; } + } + if (is.array(children)) { + for (i = 0; i < children.length; ++i) { + if (is.primitive(children[i])) children[i] = (vnode as any)(undefined, undefined, undefined, children[i]); + } + } + if (sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g') { + addNS(data, children, sel); + } + return vnode(sel, data, children, text, undefined); +}; + +export = h; \ No newline at end of file diff --git a/helpers/attachto.js b/src/helpers/attachto.ts old mode 100644 new mode 100755 similarity index 53% rename from helpers/attachto.js rename to src/helpers/attachto.ts index 24c8d48..29e6feb --- a/helpers/attachto.js +++ b/src/helpers/attachto.ts @@ -1,26 +1,28 @@ -function pre(vnode, newVnode) { - var attachData = vnode.data.attachData; +import {VNode, VNodeData} from '../interfaces'; + +function pre(vnode: VNode, newVnode: VNode): void { + var attachData = (vnode.data as VNodeData).attachData; // Copy created placeholder and real element from old vnode - newVnode.data.attachData.placeholder = attachData.placeholder; - newVnode.data.attachData.real = attachData.real; + (newVnode.data as VNodeData).attachData.placeholder = attachData.placeholder; + (newVnode.data as VNodeData).attachData.real = attachData.real; // Mount real element in vnode so the patch process operates on it - vnode.elm = vnode.data.attachData.real; + vnode.elm = (vnode.data as VNodeData).attachData.real; } -function post(_, vnode) { +function post(_: any, vnode: VNode): void { // Mount dummy placeholder in vnode so potential reorders use it - vnode.elm = vnode.data.attachData.placeholder; + vnode.elm = (vnode.data as VNodeData).attachData.placeholder; } -function destroy(vnode) { +function destroy(vnode: VNode): void { // Remove placeholder - vnode.elm.parentElement.removeChild(vnode.elm); + vnode.elm && vnode.elm.parentElement.removeChild(vnode.elm); // Remove real element from where it was inserted - vnode.elm = vnode.data.attachData.real; + vnode.elm = (vnode.data as VNodeData).attachData.real; } -function create(_, vnode) { - var real = vnode.elm, attachData = vnode.data.attachData; +function create(_: any, vnode: VNode): void { + var real = vnode.elm, attachData = (vnode.data as VNodeData).attachData; var placeholder = document.createElement('span'); // Replace actual element with dummy placeholder // Snabbdom will then insert placeholder instead @@ -30,7 +32,7 @@ function create(_, vnode) { attachData.placeholder = placeholder; } -module.exports = function(target, vnode) { +export = function attachTo(target: Element, vnode: VNode): VNode { if (vnode.data === undefined) vnode.data = {}; if (vnode.data.hook === undefined) vnode.data.hook = {}; var data = vnode.data; diff --git a/src/htmldomapi.ts b/src/htmldomapi.ts new file mode 100644 index 0000000..feb1d9a --- /dev/null +++ b/src/htmldomapi.ts @@ -0,0 +1,54 @@ +import {DOMAPI} from './interfaces'; + +function createElement(tagName: any): HTMLElement { + return document.createElement(tagName); +} + +function createElementNS(namespaceURI: string, qualifiedName: string): Element { + return document.createElementNS(namespaceURI, qualifiedName); +} + +function createTextNode(text: string): Text { + return document.createTextNode(text); +} + +function insertBefore(parentNode: Node, newNode: Node, referenceNode: Node | null): void { + parentNode.insertBefore(newNode, referenceNode); +} + +function removeChild(node: Node, child: Node): void { + node.removeChild(child); +} + +function appendChild(node: Node, child: Node): void { + node.appendChild(child); +} + +function parentNode(node: Node): HTMLElement { + return node.parentElement; +} + +function nextSibling(node: Node): Node { + return node.nextSibling; +} + +function tagName(elm: Element): string { + return elm.tagName; +} + +function setTextContent(node: Node, text: string | null): void { + node.textContent = text; +} + +export = { + createElement, + createElementNS, + createTextNode, + insertBefore, + removeChild, + appendChild, + parentNode, + nextSibling, + tagName, + setTextContent, +} as DOMAPI; \ No newline at end of file diff --git a/src/interfaces.d.ts b/src/interfaces.d.ts new file mode 100644 index 0000000..b3a6a15 --- /dev/null +++ b/src/interfaces.d.ts @@ -0,0 +1,87 @@ +export interface VNodeData { + // modules - use any because Object type is useless + props?: any; + attrs?: any; + class?: any; + style?: any; + dataset?: any; + on?: any; + hero?: any; + attachData?: any; + [key: string]: any; // for any other 3rd party module + // end of modules + hook?: Hooks; + key?: string | number; + ns?: string; // for SVGs + fn?: () => VNode; // for thunks + args?: Array; // for thunks +} + +export interface VNode { + sel: string | undefined; + data: VNodeData | undefined; + children: Array | undefined; + elm: Element | Text | undefined; + text: string | undefined; + key: string | number | undefined; +} + +export interface ThunkData extends VNodeData { + fn: () => VNode; + args: Array; +} + +export interface Thunk extends VNode { + data: ThunkData; +} + +export interface ThunkFn { + (sel: string, fn: Function, args: Array): Thunk; + (sel: string, key: any, fn: Function, args: Array): Thunk; +} + +export type PreHook = () => any; +export type InitHook = (vNode: VNode) => any; +export type CreateHook = (emptyVNode: VNode, vNode: VNode) => any; +export type InsertHook = (vNode: VNode) => any; +export type PrePatchHook = (oldVNode: VNode, vNode: VNode) => any; +export type UpdateHook = (oldVNode: VNode, vNode: VNode) => any; +export type PostPatchHook = (oldVNode: VNode, vNode: VNode) => any; +export type DestroyHook = (vNode: VNode) => any; +export type RemoveHook = (vNode: VNode, removeCallback: () => void) => any; +export type PostHook = () => any; + +export interface Hooks { + pre?: PreHook; + init?: InitHook; + create?: CreateHook; + insert?: InsertHook; + prepatch?: PrePatchHook; + update?: UpdateHook; + postpatch?: PostPatchHook; + destroy?: DestroyHook; + remove?: RemoveHook; + post?: PostHook; +} + +export interface Module { + pre?: PreHook; + create?: CreateHook; + update?: UpdateHook; + destroy?: DestroyHook; + remove?: RemoveHook; + post?: PostHook; +} + +export interface DOMAPI { + createElement: (tagName: any) => HTMLElement; + createElementNS: (namespaceURI: string, qualifiedName: string) => Element; + createTextNode: (text: string) => Text; + insertBefore: (parentNode: Node, newNode: Node, referenceNode: Node | null) => void; + removeChild: (node: Node, child: Node) => void; + appendChild: (node: Node, child: Node) => void; + parentNode: (node: Node) => HTMLElement; + nextSibling: (node: Node) => Node; + tagName: (elm: Element) => string; + setTextContent: (node: Node, text: string | null) => void; +} \ No newline at end of file diff --git a/src/is.ts b/src/is.ts new file mode 100644 index 0000000..d392e71 --- /dev/null +++ b/src/is.ts @@ -0,0 +1,6 @@ +export = { + array: Array.isArray, + primitive: function primitive(s: any): boolean { + return typeof s === 'string' || typeof s === 'number'; + }, +}; diff --git a/modules/attributes.js b/src/modules/attributes.ts old mode 100644 new mode 100755 similarity index 60% rename from modules/attributes.js rename to src/modules/attributes.ts index e486bc7..ea3931c --- a/modules/attributes.js +++ b/src/modules/attributes.ts @@ -1,22 +1,25 @@ -var NamespaceURIs = { +import {VNode, VNodeData, Module} from '../interfaces'; + +const NamespaceURIs = { "xlink": "http://www.w3.org/1999/xlink" }; -var booleanAttrs = ["allowfullscreen", "async", "autofocus", "autoplay", "checked", "compact", "controls", "declare", +const booleanAttrs = ["allowfullscreen", "async", "autofocus", "autoplay", "checked", "compact", "controls", "declare", "default", "defaultchecked", "defaultmuted", "defaultselected", "defer", "disabled", "draggable", "enabled", "formnovalidate", "hidden", "indeterminate", "inert", "ismap", "itemscope", "loop", "multiple", "muted", "nohref", "noresize", "noshade", "novalidate", "nowrap", "open", "pauseonexit", "readonly", "required", "reversed", "scoped", "seamless", "selected", "sortable", "spellcheck", "translate", "truespeed", "typemustmatch", "visible"]; -var booleanAttrsDict = Object.create(null); -for(var i=0, len = booleanAttrs.length; i < len; i++) { +const booleanAttrsDict = Object.create(null); +for (let i = 0, len = booleanAttrs.length; i < len; i++) { booleanAttrsDict[booleanAttrs[i]] = true; } -function updateAttrs(oldVnode, vnode) { - var key, cur, old, elm = vnode.elm, - oldAttrs = oldVnode.data.attrs, attrs = vnode.data.attrs, namespaceSplit; +function updateAttrs(oldVnode: VNode, vnode: VNode): void { + var key: string, cur: any, old: any, elm: Element = vnode.elm as Element, + oldAttrs = (oldVnode.data as VNodeData).attrs, + attrs = (vnode.data as VNodeData).attrs, namespaceSplit: Array; if (!oldAttrs && !attrs) return; oldAttrs = oldAttrs || {}; @@ -27,12 +30,12 @@ function updateAttrs(oldVnode, vnode) { cur = attrs[key]; old = oldAttrs[key]; if (old !== cur) { - if(!cur && booleanAttrsDict[key]) + if (!cur && booleanAttrsDict[key]) elm.removeAttribute(key); else { namespaceSplit = key.split(":"); - if(namespaceSplit.length > 1 && NamespaceURIs.hasOwnProperty(namespaceSplit[0])) - elm.setAttributeNS(NamespaceURIs[namespaceSplit[0]], key, cur); + if (namespaceSplit.length > 1 && NamespaceURIs.hasOwnProperty(namespaceSplit[0])) + elm.setAttributeNS((NamespaceURIs as any)[namespaceSplit[0]], key, cur); else elm.setAttribute(key, cur); } @@ -48,4 +51,4 @@ function updateAttrs(oldVnode, vnode) { } } -module.exports = {create: updateAttrs, update: updateAttrs}; +export = {create: updateAttrs, update: updateAttrs} as Module; diff --git a/src/modules/class.ts b/src/modules/class.ts new file mode 100755 index 0000000..64399e8 --- /dev/null +++ b/src/modules/class.ts @@ -0,0 +1,25 @@ +import {VNode, VNodeData, Module} from '../interfaces'; + +function updateClass(oldVnode: VNode, vnode: VNode): void { + var cur: any, name: string, elm: Element = vnode.elm as Element, + oldClass = (oldVnode.data as VNodeData).class, + klass = (vnode.data as VNodeData).class; + + if (!oldClass && !klass) return; + oldClass = oldClass || {}; + klass = klass || {}; + + for (name in oldClass) { + if (!klass[name]) { + elm.classList.remove(name); + } + } + for (name in klass) { + cur = klass[name]; + if (cur !== oldClass[name]) { + (elm.classList as any)[cur ? 'add' : 'remove'](name); + } + } +} + +export = {create: updateClass, update: updateClass} as Module; diff --git a/src/modules/dataset.ts b/src/modules/dataset.ts new file mode 100755 index 0000000..6867d7e --- /dev/null +++ b/src/modules/dataset.ts @@ -0,0 +1,25 @@ +import {VNode, VNodeData, Module} from '../interfaces'; + +function updateDataset(oldVnode: VNode, vnode: VNode): void { + var elm: HTMLElement = vnode.elm as HTMLElement, + oldDataset = (oldVnode.data as VNodeData).dataset, + dataset = (vnode.data as VNodeData).dataset, + key: string; + + if (!oldDataset && !dataset) return; + oldDataset = oldDataset || {}; + dataset = dataset || {}; + + for (key in oldDataset) { + if (!dataset[key]) { + delete elm.dataset[key]; + } + } + for (key in dataset) { + if (oldDataset[key] !== dataset[key]) { + elm.dataset[key] = dataset[key]; + } + } +} + +export = {create: updateDataset, update: updateDataset} as Module; diff --git a/modules/eventlisteners.js b/src/modules/eventlisteners.ts old mode 100644 new mode 100755 similarity index 74% rename from modules/eventlisteners.js rename to src/modules/eventlisteners.ts index 34ad98d..036ac79 --- a/modules/eventlisteners.js +++ b/src/modules/eventlisteners.ts @@ -1,4 +1,6 @@ -function invokeHandler(handler, vnode, event) { +import {VNode, VNodeData, Module} from '../interfaces'; + +function invokeHandler(handler: any, vnode?: VNode, event?: Event): void { if (typeof handler === "function") { // call function handler handler.call(vnode, event, vnode); @@ -23,9 +25,9 @@ function invokeHandler(handler, vnode, event) { } } -function handleEvent(event, vnode) { +function handleEvent(event: Event, vnode: VNode) { var name = event.type, - on = vnode.data.on; + on = (vnode.data as VNodeData).on; // call event handler(s) if exists if (on && on[name]) { @@ -34,18 +36,18 @@ function handleEvent(event, vnode) { } function createListener() { - return function handler(event) { - handleEvent(event, handler.vnode); + return function handler(event: Event) { + handleEvent(event, (handler as any).vnode); } } -function updateEventListeners(oldVnode, vnode) { - var oldOn = oldVnode.data.on, - oldListener = oldVnode.listener, - oldElm = oldVnode.elm, - on = vnode && vnode.data.on, - elm = vnode && vnode.elm, - name; +function updateEventListeners(oldVnode: VNode, vnode?: VNode): void { + var oldOn = (oldVnode.data as VNodeData).on, + oldListener = (oldVnode as any).listener, + oldElm: Element = oldVnode.elm as Element, + on = vnode && (vnode.data as VNodeData).on, + elm: Element = (vnode && vnode.elm) as Element, + name: string; // optimization for reused immutable handlers if (oldOn === on) { @@ -73,7 +75,7 @@ function updateEventListeners(oldVnode, vnode) { // add new listeners which has not already attached if (on) { // reuse existing listener or create new - var listener = vnode.listener = oldVnode.listener || createListener(); + var listener = (vnode as any).listener = (oldVnode as any).listener || createListener(); // update vnode for listener listener.vnode = vnode; @@ -94,8 +96,8 @@ function updateEventListeners(oldVnode, vnode) { } } -module.exports = { +export = { create: updateEventListeners, update: updateEventListeners, destroy: updateEventListeners -}; +} as Module; diff --git a/modules/hero.js b/src/modules/hero.ts old mode 100644 new mode 100755 similarity index 55% rename from modules/hero.js rename to src/modules/hero.ts index b8b3159..ad9f7c3 --- a/modules/hero.js +++ b/src/modules/hero.ts @@ -1,12 +1,14 @@ +import {VNode, VNodeData, Module} from '../interfaces'; + var raf = (typeof window !== 'undefined' && window.requestAnimationFrame) || setTimeout; -var nextFrame = function(fn) { raf(function() { raf(fn); }); }; +var nextFrame = function(fn: any) { raf(function() { raf(fn); }); }; -function setNextFrame(obj, prop, val) { +function setNextFrame(obj: any, prop: string, val: any): void { nextFrame(function() { obj[prop] = val; }); } -function getTextNodeRect(textNode) { - var rect; +function getTextNodeRect(textNode: Text): ClientRect | undefined { + var rect: ClientRect | undefined; if (document.createRange) { var range = document.createRange(); range.selectNodeContents(textNode); @@ -17,7 +19,9 @@ function getTextNodeRect(textNode) { return rect; } -function calcTransformOrigin(isTextNode, textRect, boundingRect) { +function calcTransformOrigin(isTextNode: boolean, + textRect: ClientRect | undefined, + boundingRect: ClientRect): string { if (isTextNode) { if (textRect) { //calculate pixels to center of text from left edge of bounding box @@ -29,73 +33,78 @@ function calcTransformOrigin(isTextNode, textRect, boundingRect) { return '0 0'; //top left } -function getTextDx(oldTextRect, newTextRect) { +function getTextDx(oldTextRect: ClientRect | undefined, + newTextRect: ClientRect | undefined): number { if (oldTextRect && newTextRect) { return ((oldTextRect.left + oldTextRect.width/2) - (newTextRect.left + newTextRect.width/2)); } return 0; } -function getTextDy(oldTextRect, newTextRect) { +function getTextDy(oldTextRect: ClientRect | undefined, + newTextRect: ClientRect | undefined): number { if (oldTextRect && newTextRect) { return ((oldTextRect.top + oldTextRect.height/2) - (newTextRect.top + newTextRect.height/2)); } return 0; } -function isTextElement(elm) { +function isTextElement(elm: Element | Text): elm is Text { return elm.childNodes.length === 1 && elm.childNodes[0].nodeType === 3; } -var removed, created; +var removed: any, created: any; -function pre(oldVnode, vnode) { +function pre() { removed = {}; created = []; } -function create(oldVnode, vnode) { - var hero = vnode.data.hero; +function create(oldVnode: VNode, vnode: VNode): void { + var hero = (vnode.data as VNodeData).hero; if (hero && hero.id) { created.push(hero.id); created.push(vnode); } } -function destroy(vnode) { - var hero = vnode.data.hero; +function destroy(vnode: VNode): void { + var hero = (vnode.data as VNodeData).hero; if (hero && hero.id) { var elm = vnode.elm; - vnode.isTextNode = isTextElement(elm); //is this a text node? - vnode.boundingRect = elm.getBoundingClientRect(); //save the bounding rectangle to a new property on the vnode - vnode.textRect = vnode.isTextNode ? getTextNodeRect(elm.childNodes[0]) : null; //save bounding rect of inner text node - var computedStyle = window.getComputedStyle(elm, null); //get current styles (includes inherited properties) - vnode.savedStyle = JSON.parse(JSON.stringify(computedStyle)); //save a copy of computed style values + (vnode as any).isTextNode = isTextElement(elm as Element | Text); //is this a text node? + (vnode as any).boundingRect = (elm as Element).getBoundingClientRect(); //save the bounding rectangle to a new property on the vnode + (vnode as any).textRect = (vnode as any).isTextNode ? getTextNodeRect((elm as Element).childNodes[0] as Text) : null; //save bounding rect of inner text node + var computedStyle = window.getComputedStyle(elm as Element, void 0); //get current styles (includes inherited properties) + (vnode as any).savedStyle = JSON.parse(JSON.stringify(computedStyle)); //save a copy of computed style values removed[hero.id] = vnode; } } function post() { - var i, id, newElm, oldVnode, oldElm, hRatio, wRatio, - oldRect, newRect, dx, dy, origTransform, origTransition, - newStyle, oldStyle, newComputedStyle, isTextNode, - newTextRect, oldTextRect; + var i: number, id: any, newElm: Element, oldVnode: VNode, oldElm: Element, + hRatio: number, wRatio: number, + oldRect: ClientRect, newRect: ClientRect, dx: number, dy: number, + origTransform: string | null, origTransition: string | null, + newStyle: CSSStyleDeclaration, oldStyle: CSSStyleDeclaration, + newComputedStyle: CSSStyleDeclaration, isTextNode: boolean, + newTextRect: ClientRect | undefined, oldTextRect: ClientRect | undefined; for (i = 0; i < created.length; i += 2) { id = created[i]; newElm = created[i+1].elm; oldVnode = removed[id]; if (oldVnode) { - isTextNode = oldVnode.isTextNode && isTextElement(newElm); //Are old & new both text? - newStyle = newElm.style; - newComputedStyle = window.getComputedStyle(newElm, null); //get full computed style for new element - oldElm = oldVnode.elm; - oldStyle = oldElm.style; + isTextNode = (oldVnode as any).isTextNode && isTextElement(newElm); //Are old & new both text? + newStyle = (newElm as HTMLElement).style; + newComputedStyle = window.getComputedStyle(newElm, void 0); //get full computed style for new element + oldElm = oldVnode.elm as Element; + oldStyle = (oldElm as HTMLElement).style; //Overall element bounding boxes newRect = newElm.getBoundingClientRect(); - oldRect = oldVnode.boundingRect; //previously saved bounding rect + oldRect = (oldVnode as any).boundingRect; //previously saved bounding rect //Text node bounding boxes & distances if (isTextNode) { - newTextRect = getTextNodeRect(newElm.childNodes[0]); - oldTextRect = oldVnode.textRect; + newTextRect = getTextNodeRect(newElm.childNodes[0] as Text); + oldTextRect = (oldVnode as any).textRect; dx = getTextDx(oldTextRect, newTextRect); dy = getTextDy(oldTextRect, newTextRect); } else { @@ -119,13 +128,13 @@ function post() { setNextFrame(newStyle, 'transform', origTransform); setNextFrame(newStyle, 'opacity', '1'); // Animate old element - for (var key in oldVnode.savedStyle) { //re-apply saved inherited properties - if (parseInt(key) != key) { + for (var key in (oldVnode as any).savedStyle) { //re-apply saved inherited properties + if (parseInt(key) != key as any as number) { var ms = key.substring(0,2) === 'ms'; var moz = key.substring(0,3) === 'moz'; var webkit = key.substring(0,6) === 'webkit'; - if (!ms && !moz && !webkit) //ignore prefixed style properties - oldStyle[key] = oldVnode.savedStyle[key]; + if (!ms && !moz && !webkit) //ignore prefixed style properties + (oldStyle as any)[key] = (oldVnode as any).savedStyle[key]; } } oldStyle.position = 'absolute'; @@ -133,20 +142,20 @@ function post() { oldStyle.left = oldRect.left + 'px'; oldStyle.width = oldRect.width + 'px'; //Needed for elements who were sized relative to their parents oldStyle.height = oldRect.height + 'px'; //Needed for elements who were sized relative to their parents - oldStyle.margin = 0; //Margin on hero element leads to incorrect positioning + oldStyle.margin = '0'; //Margin on hero element leads to incorrect positioning oldStyle.transformOrigin = calcTransformOrigin(isTextNode, oldTextRect, oldRect); oldStyle.transform = ''; oldStyle.opacity = '1'; document.body.appendChild(oldElm); setNextFrame(oldStyle, 'transform', 'translate('+ -dx +'px, '+ -dy +'px) scale('+wRatio+', '+hRatio+')'); //scale must be on far right for translate to be correct setNextFrame(oldStyle, 'opacity', '0'); - oldElm.addEventListener('transitionend', function(ev) { + oldElm.addEventListener('transitionend', function (ev: TransitionEvent) { if (ev.propertyName === 'transform') - document.body.removeChild(ev.target); + document.body.removeChild(ev.target as Node); }); } } removed = created = undefined; } -module.exports = {pre: pre, create: create, destroy: destroy, post: post}; +export = {pre: pre, create: create, destroy: destroy, post: post} as Module; diff --git a/src/modules/props.ts b/src/modules/props.ts new file mode 100755 index 0000000..f6b3a89 --- /dev/null +++ b/src/modules/props.ts @@ -0,0 +1,26 @@ +import {VNode, VNodeData, Module} from '../interfaces'; + +function updateProps(oldVnode: VNode, vnode: VNode): void { + var key: string, cur: any, old: any, elm = vnode.elm, + oldProps = (oldVnode.data as VNodeData).props, + props = (vnode.data as VNodeData).props; + + if (!oldProps && !props) return; + oldProps = oldProps || {}; + props = props || {}; + + for (key in oldProps) { + if (!props[key]) { + delete (elm as any)[key]; + } + } + for (key in props) { + cur = props[key]; + old = oldProps[key]; + if (old !== cur && (key !== 'value' || (elm as any)[key] !== cur)) { + (elm as any)[key] = cur; + } + } +} + +export = {create: updateProps, update: updateProps} as Module; diff --git a/src/modules/style.ts b/src/modules/style.ts new file mode 100755 index 0000000..fc601f3 --- /dev/null +++ b/src/modules/style.ts @@ -0,0 +1,76 @@ +import {VNode, VNodeData, Module} from '../interfaces'; + +var raf = (typeof window !== 'undefined' && window.requestAnimationFrame) || setTimeout; +var nextFrame = function(fn: any) { raf(function() { raf(fn); }); }; + +function setNextFrame(obj: any, prop: string, val: any): void { + nextFrame(function() { obj[prop] = val; }); +} + +function updateStyle(oldVnode: VNode, vnode: VNode): void { + var cur: any, name: string, elm = vnode.elm, + oldStyle = (oldVnode.data as VNodeData).style, + style = (vnode.data as VNodeData).style; + + if (!oldStyle && !style) return; + oldStyle = oldStyle || {}; + style = style || {}; + var oldHasDel = 'delayed' in oldStyle; + + for (name in oldStyle) { + if (!style[name]) { + (elm as any).style[name] = ''; + } + } + for (name in style) { + cur = style[name]; + if (name === 'delayed') { + for (name in style.delayed) { + cur = style.delayed[name]; + if (!oldHasDel || cur !== oldStyle.delayed[name]) { + setNextFrame((elm as any).style, name, cur); + } + } + } else if (name !== 'remove' && cur !== oldStyle[name]) { + (elm as any).style[name] = cur; + } + } +} + +function applyDestroyStyle(vnode: VNode): void { + var style: any, name: string, elm = vnode.elm, s = (vnode.data as VNodeData).style; + if (!s || !(style = s.destroy)) return; + for (name in style) { + (elm as any).style[name] = style[name]; + } +} + +function applyRemoveStyle(vnode: VNode, rm: () => void): void { + var s = (vnode.data as VNodeData).style; + if (!s || !s.remove) { + rm(); + return; + } + var name: string, elm = vnode.elm, i = 0, compStyle: CSSStyleDeclaration, + style = s.remove, amount = 0, applied: Array = []; + for (name in style) { + applied.push(name); + (elm as any).style[name] = style[name]; + } + compStyle = getComputedStyle(elm as Element); + var props = (compStyle as any)['transition-property'].split(', '); + for (; i < props.length; ++i) { + if(applied.indexOf(props[i]) !== -1) amount++; + } + (elm as Element).addEventListener('transitionend', function (ev: TransitionEvent) { + if (ev.target === elm) --amount; + if (amount === 0) rm(); + }); +} + +export = { + create: updateStyle, + update: updateStyle, + destroy: applyDestroyStyle, + remove: applyRemoveStyle +} as Module; diff --git a/src/snabbdom.bundle.ts b/src/snabbdom.bundle.ts new file mode 100644 index 0000000..c7cd1ff --- /dev/null +++ b/src/snabbdom.bundle.ts @@ -0,0 +1,16 @@ +import snabbdom = require('./snabbdom'); +import attributesModule = require('./modules/attributes'); // for setting attributes on DOM elements +import classModule = require('./modules/class'); // makes it easy to toggle classes +import propsModule = require('./modules/props'); // for setting properties on DOM elements +import styleModule = require('./modules/style'); // handles styling on elements with support for animations +import eventListenersModule = require('./modules/eventlisteners'); // attaches event listeners +import h = require('./h'); // helper function for creating vnodes +var patch = snabbdom.init([ // Init patch function with choosen modules + attributesModule, + classModule, + propsModule, + styleModule, + eventListenersModule +]) as (oldVNode: any, vnode: any) => any; + +export = { patch, h: h as any }; \ No newline at end of file diff --git a/src/snabbdom.ts b/src/snabbdom.ts new file mode 100644 index 0000000..f268b03 --- /dev/null +++ b/src/snabbdom.ts @@ -0,0 +1,272 @@ +/* global require, module, document, Node */ +import {VNode, VNodeData, Hooks, DOMAPI} from './interfaces'; +import vnode = require('./vnode'); +import is = require('./is'); +import htmlDomApi = require('./htmldomapi'); + +function isUndef(s: any): boolean { return s === undefined; } +function isDef(s: any): boolean { return s !== undefined; } + +type VNodeQueue = Array; + +const emptyNode = vnode('', {}, [], undefined, undefined); + +function sameVnode(vnode1: VNode, vnode2: VNode): boolean { + return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; +} + +function createKeyToOldIdx(children: Array, beginIdx: number, endIdx: number): any { + let i: number, map: any = {}, key: any; + for (i = beginIdx; i <= endIdx; ++i) { + key = children[i].key; + if (isDef(key)) map[key] = i; + } + return map; +} + +const hooks = ['create', 'update', 'remove', 'destroy', 'pre', 'post']; + +function init(modules: Array, domApi?: DOMAPI) { + let i: number, j: number, cbs: any = {}; + let api: DOMAPI = domApi as DOMAPI; + + if (isUndef(domApi)) api = htmlDomApi; + + for (i = 0; i < hooks.length; ++i) { + cbs[hooks[i]] = []; + for (j = 0; j < modules.length; ++j) { + if ((modules[j] as any)[hooks[i]] !== undefined) cbs[hooks[i]].push((modules[j] as any)[hooks[i]]); + } + } + + function emptyNodeAt(elm: Element) { + const id = elm.id ? '#' + elm.id : ''; + const c = elm.className ? '.' + elm.className.split(' ').join('.') : ''; + return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm); + } + + function createRmCb(childElm: Element | Text, listeners: number) { + return function rmCb() { + if (--listeners === 0) { + const parent = api.parentNode(childElm); + api.removeChild(parent, childElm); + } + }; + } + + function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Element | Text { + let i: any, data = vnode.data; + if (isDef(data)) { + if (isDef(i = (data as VNodeData).hook) && isDef(i = i.init)) { + i(vnode); + data = vnode.data; + } + } + let elm: Element | Text, children = vnode.children, sel = vnode.sel; + if (isDef(sel)) { + // Parse selector + const hashIdx = (sel as string).indexOf('#'); + const dotIdx = (sel as string).indexOf('.', hashIdx); + const hash = hashIdx > 0 ? hashIdx : (sel as string).length; + const dot = dotIdx > 0 ? dotIdx : (sel as string).length; + const tag = hashIdx !== -1 || dotIdx !== -1 ? (sel as string).slice(0, Math.min(hash, dot)) : sel; + elm = vnode.elm = isDef(data) && isDef(i = (data as VNodeData).ns) ? api.createElementNS(i, tag as string) + : api.createElement(tag); + if (hash < dot) elm.id = (sel as string).slice(hash + 1, dot); + if (dotIdx > 0) elm.className = (sel as string).slice(dot + 1).replace(/\./g, ' '); + if (is.array(children)) { + for (i = 0; i < children.length; ++i) { + api.appendChild(elm, createElm(children[i] as VNode, insertedVnodeQueue)); + } + } else if (is.primitive(vnode.text)) { + api.appendChild(elm, api.createTextNode(vnode.text as string)); + } + for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode); + i = (vnode.data as VNodeData).hook; // Reuse variable + if (isDef(i)) { + if (i.create) i.create(emptyNode, vnode); + if (i.insert) insertedVnodeQueue.push(vnode); + } + } else { + elm = vnode.elm = api.createTextNode(vnode.text as string); + } + return vnode.elm; + } + + function addVnodes(parentElm: Node, + before: Node | null, + vnodes: Array, + startIdx: number, + endIdx: number, + insertedVnodeQueue: VNodeQueue) { + for (; startIdx <= endIdx; ++startIdx) { + api.insertBefore(parentElm, createElm(vnodes[startIdx], insertedVnodeQueue), before); + } + } + + function invokeDestroyHook(vnode: VNode) { + let i: any, j: number, data = vnode.data; + if (isDef(data)) { + if (isDef(i = (data as VNodeData).hook) && isDef(i = i.destroy)) i(vnode); + for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode); + if (isDef(i = vnode.children)) { + for (j = 0; j < (vnode.children as Array).length; ++j) { + invokeDestroyHook((vnode.children as Array)[j]); + } + } + } + } + + function removeVnodes(parentElm: Element, + vnodes: Array, + startIdx: number, + endIdx: number): void { + for (; startIdx <= endIdx; ++startIdx) { + let i: any, listeners: number, rm: () => void, ch = vnodes[startIdx]; + if (isDef(ch)) { + if (isDef(ch.sel)) { + invokeDestroyHook(ch); + listeners = cbs.remove.length + 1; + rm = createRmCb(ch.elm as Element | Text, listeners); + for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm); + if (isDef(i = ch.data) && isDef(i = i.hook) && isDef(i = i.remove)) { + i(ch, rm); + } else { + rm(); + } + } else { // Text node + api.removeChild(parentElm, ch.elm as Element | Text); + } + } + } + } + + function updateChildren(parentElm: Element, + oldCh: Array, + newCh: Array, + insertedVnodeQueue: VNodeQueue) { + let oldStartIdx = 0, newStartIdx = 0; + let oldEndIdx = oldCh.length - 1; + let oldStartVnode = oldCh[0]; + let oldEndVnode = oldCh[oldEndIdx]; + let newEndIdx = newCh.length - 1; + let newStartVnode = newCh[0]; + let newEndVnode = newCh[newEndIdx]; + let oldKeyToIdx: any, idxInOld: number, elmToMove: any, before: any; + + while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { + if (isUndef(oldStartVnode)) { + oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left + } else if (isUndef(oldEndVnode)) { + oldEndVnode = oldCh[--oldEndIdx]; + } else if (sameVnode(oldStartVnode, newStartVnode)) { + patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); + oldStartVnode = oldCh[++oldStartIdx]; + newStartVnode = newCh[++newStartIdx]; + } else if (sameVnode(oldEndVnode, newEndVnode)) { + patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); + oldEndVnode = oldCh[--oldEndIdx]; + newEndVnode = newCh[--newEndIdx]; + } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right + patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); + api.insertBefore(parentElm, oldStartVnode.elm as Element, api.nextSibling(oldEndVnode.elm as Element)); + oldStartVnode = oldCh[++oldStartIdx]; + newEndVnode = newCh[--newEndIdx]; + } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left + patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); + api.insertBefore(parentElm, oldEndVnode.elm as Element, oldStartVnode.elm as Element); + oldEndVnode = oldCh[--oldEndIdx]; + newStartVnode = newCh[++newStartIdx]; + } else { + if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); + idxInOld = oldKeyToIdx[newStartVnode.key as string | number]; + if (isUndef(idxInOld)) { // New element + api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Element); + newStartVnode = newCh[++newStartIdx]; + } else { + elmToMove = oldCh[idxInOld]; + patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); + oldCh[idxInOld] = undefined as any; + api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm as Element); + newStartVnode = newCh[++newStartIdx]; + } + } + } + if (oldStartIdx > oldEndIdx) { + before = isUndef(newCh[newEndIdx+1]) ? null : newCh[newEndIdx+1].elm; + addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); + } else if (newStartIdx > newEndIdx) { + removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); + } + } + + function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) { + let i: any, hook: any; + if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) { + i(oldVnode, vnode); + } + let elm = vnode.elm = oldVnode.elm, oldCh = oldVnode.children, ch = vnode.children; + if (oldVnode === vnode) return; + if (!sameVnode(oldVnode, vnode)) { + const parentElm = api.parentNode(oldVnode.elm as Element); + elm = createElm(vnode, insertedVnodeQueue); + api.insertBefore(parentElm, elm, oldVnode.elm as Element); + removeVnodes(parentElm, [oldVnode], 0, 0); + return; + } + if (isDef(vnode.data)) { + for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); + i = (vnode.data as VNodeData).hook; + if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode); + } + if (isUndef(vnode.text)) { + if (isDef(oldCh) && isDef(ch)) { + if (oldCh !== ch) updateChildren(elm as Element, oldCh as Array, ch as Array, insertedVnodeQueue); + } else if (isDef(ch)) { + if (isDef(oldVnode.text)) api.setTextContent(elm as Element, ''); + addVnodes(elm as Element, null, ch as Array, 0, (ch as Array).length - 1, insertedVnodeQueue); + } else if (isDef(oldCh)) { + removeVnodes(elm as Element, oldCh as Array, 0, (oldCh as Array).length - 1); + } else if (isDef(oldVnode.text)) { + api.setTextContent(elm as Element, ''); + } + } else if (oldVnode.text !== vnode.text) { + api.setTextContent(elm as Element, vnode.text as string); + } + if (isDef(hook) && isDef(i = hook.postpatch)) { + i(oldVnode, vnode); + } + } + + return function patch(oldVnode: VNode | Element, vnode: VNode): VNode { + let i: number, elm: Element, parent: Element; + const insertedVnodeQueue: VNodeQueue = []; + for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); + + if (isUndef((oldVnode as VNode).sel)) { + oldVnode = emptyNodeAt(oldVnode as Element); + } + + if (sameVnode(oldVnode as VNode, vnode)) { + patchVnode(oldVnode as VNode, vnode, insertedVnodeQueue); + } else { + elm = (oldVnode as VNode).elm as Element; + parent = api.parentNode(elm); + + createElm(vnode, insertedVnodeQueue); + + if (parent !== null) { + api.insertBefore(parent, vnode.elm as Element, api.nextSibling(elm)); + removeVnodes(parent, [oldVnode as VNode], 0, 0); + } + } + + for (i = 0; i < insertedVnodeQueue.length; ++i) { + (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]); + } + for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); + return vnode; + }; +} + +export = {init: init}; \ No newline at end of file diff --git a/src/thunk.ts b/src/thunk.ts new file mode 100644 index 0000000..ce7edff --- /dev/null +++ b/src/thunk.ts @@ -0,0 +1,49 @@ +import {VNode, VNodeData, ThunkFn} from './interfaces'; +import h = require('./h'); + +function copyToThunk(vnode: VNode, thunk: VNode): void { + thunk.elm = vnode.elm; + (vnode.data as VNodeData).fn = (thunk.data as VNodeData).fn; + (vnode.data as VNodeData).args = (thunk.data as VNodeData).args; + thunk.data = vnode.data; + thunk.children = vnode.children; + thunk.text = vnode.text; + thunk.elm = vnode.elm; +} + +function init(thunk: VNode): void { + const cur = thunk.data as VNodeData; + const vnode = (cur.fn as any).apply(undefined, cur.args); + copyToThunk(vnode, thunk); +} + +function prepatch(oldVnode: VNode, thunk: VNode): void { + let i: number, old = oldVnode.data as VNodeData, cur = thunk.data as VNodeData; + const oldArgs = old.args, args = cur.args; + if (old.fn !== cur.fn || (oldArgs as any).length !== (args as any).length) { + copyToThunk((cur.fn as any).apply(undefined, args), thunk); + } + for (i = 0; i < (args as any).length; ++i) { + if ((oldArgs as any)[i] !== (args as any)[i]) { + copyToThunk((cur.fn as any).apply(undefined, args), thunk); + return; + } + } + copyToThunk(oldVnode, thunk); +} + +function thunk(sel: string, key?: any, fn?: any, args?: any): VNode { + if (args === undefined) { + args = fn; + fn = key; + key = undefined; + } + return h(sel, { + key: key, + hook: {init: init, prepatch: prepatch}, + fn: fn, + args: args + }); +}; + +export = thunk as ThunkFn; \ No newline at end of file diff --git a/src/vnode.ts b/src/vnode.ts new file mode 100644 index 0000000..1362ac9 --- /dev/null +++ b/src/vnode.ts @@ -0,0 +1,13 @@ +import {VNode} from './interfaces'; + +function vnode(sel: string, + data: any | undefined, + children: Array | undefined, + text: string | undefined, + elm: Element | Text | undefined): VNode { + let key = data === undefined ? undefined : data.key; + return {sel: sel, data: data, children: children, + text: text, elm: elm, key: key}; +} + +export = vnode; diff --git a/thunk.js b/thunk.js deleted file mode 100644 index 21bf7b1..0000000 --- a/thunk.js +++ /dev/null @@ -1,46 +0,0 @@ -var h = require('./h'); - -function copyToThunk(vnode, thunk) { - thunk.elm = vnode.elm; - vnode.data.fn = thunk.data.fn; - vnode.data.args = thunk.data.args; - thunk.data = vnode.data; - thunk.children = vnode.children; - thunk.text = vnode.text; - thunk.elm = vnode.elm; -} - -function init(thunk) { - var cur = thunk.data; - var vnode = cur.fn.apply(undefined, cur.args); - copyToThunk(vnode, thunk); -} - -function prepatch(oldVnode, thunk) { - var i, old = oldVnode.data, cur = thunk.data, vnode; - var oldArgs = old.args, args = cur.args; - if (old.fn !== cur.fn || oldArgs.length !== args.length) { - copyToThunk(cur.fn.apply(undefined, args), thunk); - } - for (i = 0; i < args.length; ++i) { - if (oldArgs[i] !== args[i]) { - copyToThunk(cur.fn.apply(undefined, args), thunk); - return; - } - } - copyToThunk(oldVnode, thunk); -} - -module.exports = function(sel, key, fn, args) { - if (args === undefined) { - args = fn; - fn = key; - key = undefined; - } - return h(sel, { - key: key, - hook: {init: init, prepatch: prepatch}, - fn: fn, - args: args - }); -}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e0ea1e9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES5", + "outDir": "./", + "noImplicitAny": true, + "sourceMap": true, + "strictNullChecks": true, + "declaration": true, + "removeComments": false, + "noUnusedLocals": true, + "lib": [ + "dom", + "es5" + ] + }, + "files": [ + "src/helpers/attachto.ts", + "src/modules/attributes.ts", + "src/modules/class.ts", + "src/modules/dataset.ts", + "src/modules/eventlisteners.ts", + "src/modules/hero.ts", + "src/modules/props.ts", + "src/modules/style.ts", + "src/h.ts", + "src/htmldomapi.ts", + "src/interfaces.d.ts", + "src/is.ts", + "src/snabbdom.bundle.ts", + "src/snabbdom.ts", + "src/thunk.ts", + "src/vnode.ts" + ] +} diff --git a/type-definitions/snabbdom.d.ts b/type-definitions/snabbdom.d.ts deleted file mode 100644 index da51aa8..0000000 --- a/type-definitions/snabbdom.d.ts +++ /dev/null @@ -1,148 +0,0 @@ -export interface VNodeData { - // modules - use any because Object type is useless - props?: any; - attrs?: any; - class?: any; - style?: any; - dataset?: any; - on?: any; - hero?: any; - // end of modules - hook?: Hooks; - key?: string | number; - ns?: string; // for SVGs - fn?: () => VNode; // for thunks - args?: Array; // for thunks -} - -export interface VNode { - sel: string; - data?: VNodeData; - children?: Array; - elm?: Element | Text; - text?: string; - key?: string | number; -} - -export interface ThunkData extends VNodeData { - fn: () => VNode; - args: Array; -} - -export interface Thunk extends VNode { - data: ThunkData; -} - -export type PreHook = () => any; -export type InitHook = (vNode: VNode) => any; -export type CreateHook = (emptyVNode: VNode, vNode: VNode) => any; -export type InsertHook = (vNode: VNode) => any; -export type PrePatchHook = (oldVNode: VNode, vNode: VNode) => any; -export type UpdateHook = (oldVNode: VNode, vNode: VNode) => any; -export type PostPatchHook = (oldVNode: VNode, vNode: VNode) => any; -export type DestroyHook = (vNode: VNode) => any; -export type RemoveHook = (vNode: VNode, removeCallback: () => void) => any; -export type PostHook = () => any; - -export interface Hooks { - pre?: PreHook; - init?: InitHook; - create?: CreateHook; - insert?: InsertHook; - prepatch?: PrePatchHook; - update?: UpdateHook; - postpatch?: PostPatchHook; - destroy?: DestroyHook; - remove?: RemoveHook; - post?: PostHook; -} - -export interface Module { - pre?: PreHook; - create?: CreateHook; - update?: UpdateHook; - destroy?: DestroyHook; - remove?: RemoveHook; - post?: PostHook; -} - -export interface SnabbdomAPI { - createElement(tagName: string): T; - createElementNS(namespaceURI: string, qualifiedName: string): T; - createTextNode(text: string): T; - insertBefore(parentNode: T, newNode: T, referenceNode: T): void; - removeChild(node: T, child: T): void; - appendChild(node: T, child: T): void; - parentNode(node: T): T; - nextSibling(node: T): T; - tagName(node: T): string; - setTextContent(node: T, text: string): void; -} - -declare module "snabbdom" { - export interface PatchFunction { - (oldVNode: VNode, vnode: VNode): VNode; - } - - export function init(modules: Object, api?: SnabbdomAPI): PatchFunction; -} - -declare module "snabbdom/vnode" { - export default function vnode(sel: string, - data: VNodeData, - children: Array, - text: string, - elm: any): VNode; -} - -declare module "snabbdom/is" { - export function array(x: any): boolean; - export function primitive(x: any): boolean; -} - -declare module "snabbdom/thunk" { - export default function thunk(sel: string, - key: string, - render: (...state: Array) => VNode, - ...state: Array): Thunk; -} - -declare module "snabbdom/htmldomapi" { - let api: SnabbdomAPI; - export = api; -} - -declare module "snabbdom/modules/class" { - let ClassModule: Module; - export = ClassModule; -} - -declare module "snabbdom/modules/props" { - let PropsModule: Module; - export = PropsModule; -} - -declare module "snabbdom/modules/attributes" { - let AttrsModule: Module; - export = AttrsModule; -} - -declare module "snabbdom/modules/eventlisteners" { - let EventsModule: Module; - export = EventsModule; -} - -declare module "snabbdom/modules/hero" { - let HeroModule: Module; - export = HeroModule; -} - -declare module "snabbdom/modules/style" { - let StyleModule: Module; - export = StyleModule; -} - -declare module "snabbdom/modules/dataset" { - let DatasetModule: Module; - export = DatasetModule; -} diff --git a/vnode.js b/vnode.js deleted file mode 100644 index 7057afd..0000000 --- a/vnode.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = function(sel, data, children, text, elm) { - var key = data === undefined ? undefined : data.key; - return {sel: sel, data: data, children: children, - text: text, elm: elm, key: key}; -};