infinnie / infinnie.github.io

https://infinnie.github.io/
Apache License 2.0
0 stars 1 forks source link

HyperApp. #12

Open infinnie opened 6 years ago

infinnie commented 6 years ago
/// <reference path="/node_modules/jquery/dist/jquery.min.js"/>
define("vendors/app", ["jquery"], function ($) {
    "use strict";
    var globalInvokeLaterStack = [], raf = "requestAnimationFrame" in window ? requestAnimationFrame.bind(window) : function (f) {
        return setTimeout(f, 1);
    }, _guid = 0, lookUp = {}, eventLookUp = {};

    return function app(props) {
        var appState, appView = props.view, appActions = {},
            appEvents = {}, appMixins = props.mixins || [],
            appRoot = props.root || document.body, element, oldNode, renderLock;

        $.map(appMixins.concat(props), function (mixin) {
            mixin = typeof mixin === "function" ? mixin(emit) : mixin;

            $.map(mixin.events || [], function (event, key) {
                appEvents[key] = (appEvents[key] || []).concat(event);
            });

            appState = merge(appState, mixin.state);
            initialize(appActions, mixin.actions);
        });

        requestRender(
            (oldNode = emit("load", (element = appRoot.children[0]))) === element &&
            (oldNode = element = null)
        );

        function initialize(actions, withActions, lastName) {
            $.map(withActions || [], function (action, key) {
                var name = lastName ? lastName + "." + key : key

                if (typeof action === "function") {
                    actions[key] = function (data) {
                        emit("action", { name: name, data: data });

                        var result = emit("resolve", action(appState, appActions, data));

                        return typeof result === "function" ? result(update) : update(result);
                    }
                } else {
                    initialize(actions[key] || (actions[key] = {}), action, name);
                }
            });
        }

        function render(cb) {
            element = patch(
                appRoot,
                element,
                oldNode,
                (oldNode = emit("render", appView)(appState, appActions)),
                (renderLock = !renderLock)
            );
            while ((cb = globalInvokeLaterStack.pop())) { cb(); }
        }

        function requestRender() {
            if (appView && !renderLock) {
                raf(render, (renderLock = !renderLock));
            }
        }

        function update(withState) {
            if (typeof withState === "function") {
                return update(withState(appState));
            }
            if (withState && (withState = emit("update", merge(appState, withState)))) {
                requestRender((appState = withState));
            }
            return appState
        }

        function emit(name, data) {
            return (
                $.map(appEvents[name] || [], function (cb) {
                    var result = cb(appState, appActions, data);
                    if (result != null) {
                        data = result;
                    }
                }),
                data
            );
        }

        function merge(a, b) {
            var obj = {}, i;

            for (i in a) {
                obj[i] = a[i]
            }

            for (i in b) {
                obj[i] = b[i]
            }

            return obj
        }

        function getKey(node) {
            if (node && (node = node.data)) {
                return node.key
            }
        }

        function createElement(node, isSVG) {
            var element;
            if (typeof node === "string") {
                element = document.createTextNode(node);
            } else {
                element = (isSVG = isSVG || node.tag === "svg")
                    ? document.createElementNS("http://www.w3.org/2000/svg", node.tag)
                    : document.createElement(node.tag);

                if (node.data && node.data.oncreate) {
                    globalInvokeLaterStack.push(function () {
                        node.data.oncreate(element)
                    });
                }

                for (var i in node.data) {
                    setData(element, i, node.data[i]);
                }

                for (var i = 0; i < node.children.length;) {
                    element.appendChild(createElement(node.children[i++], isSVG));
                }
            }

            return element;
        }

        // TODO
        function setData(element, name, value, oldValue) {
            var prefix = /^\$/, tempName, guid;
            if (name === "key") {
            } else if (name === "style") {
                for (var i in merge(oldValue, (value = value || {}))) {
                    element.style[i] = value[i] || "";
                }
            } else if (prefix.test(name)) {
                tempName = name.replace(prefix, "") + ".app";
                // do something
                // https://api.jquery.com/attr/
                guid = $(element).attr("data-unique-id");
                if (typeof guid === "undefined") {
                    guid = _guid;
                    $(element).attr("data-unique-id", guid);
                    _guid++;
                }
                lookUp[guid] = lookUp[guid] || {};
                lookUp[guid][tempName] = value;

                if (name === "$input" && typeof element.addEventListener === "undefined") {
                    $(element).off("propertychange.app").on("propertychange.app", function () {
                        $(this).trigger("input.app");
                    });
                }
                if (!eventLookUp[tempName]) {
                    eventLookUp[tempName] = true;
                    $(document).on(tempName, "[data-unique-id]", function () {
                        var guid = $(this).attr("data-unique-id");
                        if (lookUp[guid] && typeof lookUp[guid][tempName] === "function") {
                            return lookUp[guid][tempName].apply(this, arguments);
                        }
                    });
                }
            } else {
                try {
                    element[name] = value
                } catch (_) { }

                if (typeof value !== "function") {
                    if (value || 0 === value) {
                        try {
                            element.setAttribute(name, value);
                        } catch (someException) { }
                    } else {
                        element.removeAttribute(name)
                    }
                }
            }
        }

        function updateElement(element, oldData, data) {
            for (var i in merge(oldData, data)) {
                var value = data[i]
                var oldValue = i === "value" || i === "checked" ? element[i] : oldData[i]

                if (value !== oldValue) {
                    setData(element, i, value, oldValue)
                }
            }

            if (data && data.onupdate) {
                globalInvokeLaterStack.push(function () {
                    data.onupdate(element, oldData)
                })
            }
        }

        function removeElement(element, data) {
            if (data && data.onremove) {
                data.onremove(element)
            } else {
                // do something
                $(element).find("[data-unique-id]").andSelf().each(function () {
                    var guid = $(this).attr("data-unique-id");
                    if (lookUp[guid]) {
                        delete lookUp[guid];
                    }
                });
                $(element).remove();
            }
        }

        function patch(parent, element, oldNode, node, isSVG, nextSibling) {
            /// <param name="element" type="Array"/>
            if ($.isArray(node)) {
                if ($.isArray(element) && $.isArray(oldNode)) {
                    var elementCopy = element.slice(0), oldCopy = oldNode.slice(0);
                    var ret = $.map(node, function (t, i) {
                        return patch(parent, elementCopy.shift(), oldCopy.shift(), t, isSVG, nextSibling); // ...
                    });
                    $(elementCopy).each(function (i) {
                        removeElement(this, oldNode[i] && oldNode[i].data);
                    });
                    return ret;
                }
                return $.map(node, function (t, i) {
                    return patch(parent, i === 0 ? element : null, i === 0 ? oldNode : null, t, isSVG, nextSibling);
                });
            }

            if ($.isArray(element)) {
                var remaining = element.slice(1), remainingNodes;
                element = element[0];
                if ($.isArray(oldNode)) {
                    remainingNodes = oldNode.slice(1);
                    oldNode = oldNode[0];
                }
                $(remaining).each(function (i) {
                    removeElement(this, remainingNodes && remainingNodes[i] && remainingNodes[i].data);
                });
            }

            if (oldNode == null) {
                element = parent.insertBefore(createElement(node, isSVG), element || null);
            } else if (node.tag != null && node.tag === oldNode.tag && node.tag !== "img") {
                updateElement(element, oldNode.data, node.data)

                isSVG = isSVG || node.tag === "svg"

                var len = node.children.length
                var oldLen = oldNode.children.length
                var oldKeyed = {}
                var oldElements = []
                var keyed = {}, i, j;

                for (i = 0; i < oldLen; i++) {
                    var oldElement = (oldElements[i] = element.childNodes[i])
                    var oldChild = oldNode.children[i]
                    var oldKey = getKey(oldChild)

                    if (null != oldKey) {
                        oldKeyed[oldKey] = [oldElement, oldChild]
                    }
                }

                i = 0;
                j = 0;

                while (j < len) {
                    var oldElement = oldElements[i]
                    var oldChild = oldNode.children[i]
                    var newChild = node.children[j]

                    var oldKey = getKey(oldChild)
                    if (keyed[oldKey]) {
                        i++
                        continue
                    }

                    var newKey = getKey(newChild)

                    var keyedNode = oldKeyed[newKey] || []

                    if (null == newKey) {
                        if (null == oldKey) {
                            patch(element, oldElement, oldChild, newChild, isSVG)
                            j++
                        }
                        i++
                    } else {
                        if (oldKey === newKey) {
                            patch(element, keyedNode[0], keyedNode[1], newChild, isSVG)
                            i++
                        } else if (keyedNode[0]) {
                            element.insertBefore(keyedNode[0], oldElement)
                            patch(element, keyedNode[0], keyedNode[1], newChild, isSVG)
                        } else {
                            patch(element, oldElement, null, newChild, isSVG)
                        }

                        j++
                        keyed[newKey] = newChild
                    }
                }

                while (i < oldLen) {
                    var oldChild = oldNode.children[i]
                    var oldKey = getKey(oldChild)
                    if (null == oldKey) {
                        removeElement(oldElements[i], oldChild.data)
                    }
                    i++
                }

                for (i in oldKeyed) {
                    keyedNode = oldKeyed[i]
                    var reusableNode = keyedNode[1]
                    if (!keyed[reusableNode.data.key]) {
                        removeElement(keyedNode[0], reusableNode.data)
                    }
                }
            } else if (element && node !== element.nodeValue) {
                if (typeof node === "string" && typeof oldNode === "string") {
                    element.nodeValue = node
                } else {
                    element = parent.insertBefore(
                        createElement(node, isSVG),
                        (nextSibling = element)
                    )
                    removeElement(nextSibling, oldNode.data)
                }
            }
            return element;
        }
        return emit;
    };
});