Newer
Older
wolf.js / ui / wolf.ui.js
@Iván Dominguez Iván Dominguez on 12 Apr 2022 25 KB More IE support
/*  wolf.ui 0.7, UI components for wolf, a lightweight framework for web page creation.
 *  Copyright 2020 XWolfOverride (under lockdown)
 *
 *  Licensed under the MIT License
 * 
 *  Permission is hereby granted, free of charge, to any person obtaining a copy of this software
 *  and associated documentation files (the "Software"), to deal in the Software without restriction,
 *  including without limitation the rights to use, copy, modify, merge, publish, distribute,
 *  sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 *  furnished to do so, subject to the following conditions:
 * 
 *  The above copyright notice and this permission notice shall be included in all copies or
 *  substantial portions of the Software.
 * 
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
 *  BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 *  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 *  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

(function () {
    'use strict';
    wolf.wolfExtension(function (K, D, UI, TOOLS) {

        /**
         * Common dialog controller
         */

        function dialogControl(modal, element, controller) {
            var modalWall; //Modal wall DOM instance
            var base; //Base dialog DOM
            var dialog; //Dialog DOM element
            var body; //Dialog body DOM element
            var buttons;// Dialog buttons footer DOM element
            var onClose; //Callback for on close
            // Process modaling
            if (modal) {
                modalWall = UI.instanceTemplate(new UI.Template("div", { "class": "wolf-dialog-modal-wall", $controller: controller }), { parent: element })[0];
                element.appendChild(modalWall);
                base = modalWall;
            } else
                base = element;

            // Create dialog row
            var dialog_row=UI.instanceTemplate(new UI.Template("div", { "class": "wolf-dialog-row", $controller: controller }), { parent: base })[0];
            base.appendChild(dialog_row);
            // Create dialog frame
            var dialog = UI.instanceTemplate(new UI.Template("div", { "class": "wolf-dialog", $controller: controller }), { parent: base })[0];
            dialog_row.appendChild(dialog);

            dialog.close = function () {
                element.removeChild((modal ? modalWall : dialog_row));
                controller && controller.close && controller.close();
                onClose && onClose(dialog.dialogController.result);
            };

            // Controller logic
            /**
             * Adds a UI template to the dialog and instences it
             * @param {*} ui UI template object or array
             */
            function append(ui) {
                if (!body) {
                    body = UI.instanceTemplate(new UI.Template("div", { "class": "wolf-dialog-body" }), { parent: dialog })[0];
                    if (buttons)
                        body.classList.add("with-footer");
                    dialog.appendChild(body);
                }
                if (ui.insertTo) //Assuming fragment
                    ui.insertTo(body);
                else {
                    if (!Array.isArray(ui))
                        ui = [ui];
                    ui.forEach(function (uielem) {
                        var uiDOM = UI.instanceTemplate(uielem, { parent: body });
                        uiDOM.forEach(function (dom) { body.appendChild(dom) });
                    })
                }
            }

            /**
             * Adds buttons definitions or UI template to the dialog foooter
             * @param {*} buttonsDef Button definition or UI template object or array
             */
            function appendButtons(buttonsDef) {
                if (body)
                    body.classList.add("with-footer");
                if (!buttons) {
                    buttons = UI.instanceTemplate(new UI.Template("div", { "class": "wolf-dialog-buttons" }), { parent: dialog })[0];
                    dialog.appendChild(buttons);
                }
                if (!Array.isArray(buttonsDef))
                    buttonsDef = [buttonsDef];
                buttonsDef.forEach(function (uidef) {
                    if (uidef.type || uidef.value) {
                        var uiDOM = UI.instanceTemplate(uidef);
                        uiDOM.forEach(function (dom) { buttons.appendChild(dom) });
                    } else
                        for (var k in uidef) {
                            var def = uidef[k];
                            if (!def)
                                continue; //def can be null in order to cancel a button in json strucutres
                            var button;
                            if (def.t)
                                button = UI.instanceTemplate(def.t, { parent: dialog })[0];
                            else {
                                var btcontent = [];
                                if (def.icon)
                                    btcontent.push(new UI.Template("i", { class: "icon" + (def.text ? " with-text" : "") }, { value: def.icon }));
                                if (def.text)
                                    btcontent.push(new UI.Template(null, null, def.text));
                                button = UI.instanceTemplate(new UI.Template("button", null, btcontent), { parent: dialog })[0];
                                if (def.class)
                                    button.className = def.class;
                                if (def.default)
                                    button.classList.add("default");
                                if (def.cancel)
                                    button.classList.add("cancel");
                            }
                            installEvent(button, k, def);
                            buttons.appendChild(button);
                        }
                    function installEvent(button, id, def) {
                        button.addEventListener("click", function (evt) {
                            var evtMethod;
                            if (controller)
                                evtMethod = controller[id];
                            if (!evtMethod)
                                evtMethod = def.click;
                            if (!evtMethod)
                                evtMethod = function () {
                                    dialog.dialogController.result = id;
                                    dialog.close();
                                }
                            evtMethod(element, evt, dialog);
                        });
                    }
                });
            }

            /**
             * Raises the callback of the dialog
             * @param {*} cb callback
             * @param {*} close callback for on close
             */
            function callback(cb, close) {
                cb && cb(dialog, element, controller);
                onClose = close;
            }

            // Return controller
            return dialog.dialogController = {
                append: append,
                appendButtons: appendButtons,
                callback: callback,
            }
        }

        // ====================
        // ====================
        // ==            Public
        // ====================

        /**
         * Shows a non blocking message to the user
         * @param {string} text Text to show
         * @param {string} type (error|warning|default)
         */
        function toast(text, type) {
            var div = document.createElement("div");
            div.className = "wolf-toast wolf-toast-" + type;
            div.innerText = text;
            document.body.appendChild(div);
            setTimeout(function () {
                div.style.opacity = 0;
                setTimeout(function () {
                    document.body.removeChild(div);
                }, 1000);
            }, 1500);
        }

        /**
         * Show a dialog embedding a fragment.
         * @param {element} element Destination element, application element recommended
         * @param {boolean} modal True to show the dialog in modal way
         * @param {string} url Url of the fragment
         * @param {*} [buttons] Button definition
         * @param {*} [controller] Controller for the dialogs events, if none the parent element one will be used
         * @param {function} [callback] Callback when the dialog is shown
         * @param {function} [onclose] Callback when dialog close, passing the dialog result
         */
        function dialog(element, modal, url, buttons, controller, callback, onclose) {
            var dc = dialogControl(modal, element, controller);

            UI.loadFragment(url, function (fragment) {
                dc.append(fragment);
                if (buttons)
                    dc.appendButtons(buttons);
                dc.callback(callback, onclose);
            });
        }

        /**
         * Show a dialog asking or showing information.
         * @param {element} element Destination element, application element recommended
         * @param {boolean} modal True to show the dialog in modal way
         * @param {string} text Text to show
         * @param {*} [buttons] Button definition
         * @param {*} [controller] Controller for the dialogs events, if none the parent element one will be used
         * @param {function} [callback] Callback when the dialog is shown
         * @param {function} [onclose] Callback when dialog close, passing the dialog result
         */
        function messageDialog(element, modal, text, buttons, controller, callback, onclose) {
            if (!buttons)
                buttons = { close: { icon: "close", cancel: true } };
            uiDialog(new UI.Template(null, null, text), buttons, element, modal, controller, callback, onclose)
        }

        /**
         * Show a dialog asking or showing information.
         * @param {element} element Destination element, application element recommended
         * @param {boolean} modal True to show the dialog in modal way
         * @param {*} ui UI body definition
         * @param {*} [buttons] Button definition
         * @param {*} [controller] Controller for the dialogs events, if none the parent element one will be used
         * @param {function} [callback] Callback when the dialog is shown
         * @param {function} [onclose] Callback when dialog close, passing the dialog result
         */
        function uiDialog(ui, buttons, element, modal, controller, callback, onclose) {
            var dc = dialogControl(modal, element, controller);
            dc.append(ui);
            if (buttons)
                dc.appendButtons(buttons);
            dc.callback(callback, onclose);
        }

        /**
         * Loads and initializes a UI library
         * @param {string} path Path of the UI library to load
         */
        function loadLibrary(path) {
            UI.fetchFragment(path, function (dom) {
                UI.readTemplate(dom, null, {
                });
            });
        }

        // ====================
        // ====================
        // ==           Private
        // ====================

        /**
         * Create the base element needed for the extensible UI and control building system
         */
        function InitControlDefinitions() {
            var controlGlobal = {}; //Global control space

            /**
             * Return the control instance attribute values
             * @param {*} controller controller object of control definition
             * @param {*} template template of control usage
             */
            function getControlAttributesTable(controller, template) {
                var values = {};
                // Control definition defaults
                for (var k in controller) {
                    var attr = controller[k];
                    if (attr && attr.default)
                        values[k] = attr.default;
                }
                // UI usage values
                for (var k in template) {
                    if (k[0] == "$")
                        continue;
                    values[k] = template[k];
                }
                return values;
            }

            /**
             * Generates the DOM of the control based on the script render method
             * @param {*} ext ctor extended info
             */
            function renderControlDOM(ext) {
                var tux = ext.customController.build();
                if (!tux)
                    return [];
                if (!Array.isArray(tux))
                    tux = [tux];
                var ux = [];
                for (var i in tux)
                    ux = ux.concat(UI.instanceTemplate(tux[i], ext));
                ext.customController.processDOM && ext.customController.processDOM(ux);
                return ux;
            }

            /**
             * Create event proxy for linked event call
             * @param {string} method Controller method name
             * @param {string} ename Event name
             */
            function eventProxy(method, ename, ext) {
                return function (element, event) {
                    var ctrl = ext.parentCustom || element.getController(); //TODO: This hide the controller if there is a custom
                    var evtMethod = ctrl[method];
                    if (!evtMethod)
                        throw new Error("Method '" + method + "' not found for event '" + ename + "'.");
                    evtMethod(element, event);
                }
            }

            /**
             * Register wolf:control as control definition structure
             */
            UI.registerElement("control", {
                $init: function (template) {
                    var id = template.id;
                    var allowChildren = template.allowchildren == "true";
                    var controller = {};
                    var ui = {};
                    var scriptFactory;

                    if (!id)
                        throw new Error("Can't define a control without id");
                    if (!template.$) {
                        console.warn("wolf:" + id + " empty definition");
                        return null;
                    }
                    // Read definition
                    template.$.forEach(function (c) {
                        switch (c.$t) {
                            case null:
                                break;
                            case "attr": {
                                if (!c.$ || c.$.length != 1 || c.$[0].$t)
                                    throw new Error("Control definition wolf:" + id + " attribute name missing or type error");
                                var name = c.$[0].$;
                                if (name[0] == "$" || name == "ui")
                                    throw new Error("Control definition wolf:" + id + " attribute '" + name + "' not valid or reserved.");
                                if (name.indexOf(":") >= 0)
                                    throw new Error("Control definition wolf:" + id + " attribute '" + name + "' can not have namespaces.");
                                if (controller[name])
                                    throw new Error("Control definition wolf:" + id + " attribute '" + name + "' already defined.");
                                var attr = controller[name] = {};
                                attr.bindable = c.bindable !== "false";
                                attr.mandatory = c.mandatory == "true";
                                attr.default = c.default;
                                break;
                            }
                            case "event": {
                                if (!c.$ || c.$.length != 1 || c.$[0].$t)
                                    throw new Error("Control definition wolf:" + id + " event name missing or type error");
                                var name = c.$[0].$;
                                if (controller[name])
                                    throw new Error("Control definition wolf:" + id + " events and values can not share the same name  (" + name + ").");
                                if (controller["event:" + name])
                                    throw new Error("Control definition wolf:" + id + " event '" + name + "' already defined.");
                                controller["event:" + name] = { bindable: false, mandatory: false };
                                break;
                            }
                            case "ui": {
                                var uid = c.id || "";
                                if (ui[uid])
                                    throw new Error("Control definition wolf:" + id + " ui " + (uid == "" ? "[default]" : "'" + uid + "'") + " already defined.");
                                ui[uid] = c.$;
                                break;
                            }
                            case "script": {
                                if (scriptFactory)
                                    throw new Error("Control definition wolf:" + id + " script already defined.");
                                var script = c.$[0].$;
                                if (script && (typeof script != "string" || script.indexOf("use strict") < 1))
                                    throw new Error("Control definition wolf:" + id + " script 'use strict'; mandatory");
                                scriptFactory = new Function('control', 'K', 'D', 'UI', 'TOOLS', script + ";\n//# sourceURL=wolf:" + id);
                                break;
                            }
                            default:
                                throw new Error("Control definition wolf:" + id + " " + c.$t + " not allowed here.");
                        }
                    });

                    if (scriptFactory) {//TODO: use this single instance of script
                        var script = new scriptFactory(null, K, D, UI, TOOLS);
                        if (script.define)
                            controller = script.define(controller);
                    }

                    // Controller logics (new definition rendering) =============
                    controller.$init = function (template) {
                        if (template.$ && template.$.length && !allowChildren)
                            throw new Error("wolf:" + id + " does not allow child nodes");

                        var values = getControlAttributesTable(controller, template);
                        var API = {
                            ui: function (name, clone) {
                                var result = name ? ui[name] : ui[""];
                                return clone !== false ? UI.cloneTemplate(result) : result;
                            },
                            value: function (name) {
                                var data = values[name];
                                if (data instanceof D.Binding)
                                    data = data.getValue(ext.parent);
                                return data;
                            },
                            binding: function (name) {
                                var data = values[name];
                                if (data instanceof D.Binding)
                                    return data;
                                return null;
                            },
                            childs: function (name) {
                                //ID for usage on future with multiple chilnodes block definitions
                                return template.$;
                            },
                            global: controlGlobal,
                            values: values,
                        }
                        var script = scriptFactory ? new scriptFactory(API, K, D, UI, TOOLS) : {};
                        API.controller = script;
                        API.instanceTemplate = function (templ, ext2) { UI.instanceTemplate(templ, ext2 ? ext2 : { parent: API.parent, customController: script }) };
                        if (!script.build)
                            script.build = function () {
                                return ui[""];
                            }

                        template.$api = API;
                        template.$c = script;

                        //TODO: Create control defined custom properties for attributes that change the attribute values
                        script.init && script.init();
                    };

                    controller.$ctor = function (template, ext) {
                        var parentCustom = ext.customController;
                        ext = {}.merge(ext); //Copy of ext instance
                        // Initialize attributes and script

                        var values = template.$api.values;
                        template.$api.parent = ext.parent; //TODO: Not here, thsi is shared between renderings of same template
                        template.$api.context = ext;

                        // Event mirror and attribute hook
                        // TODO MOVE TO $init (now needs the ext instance)
                        for (var k in values)
                            if (k.startsWith("event:")) {
                                var name = k.substring(6);
                                template.$c["$" + name] = eventProxy(values["event:" + name], name, ext);
                            }
                        for (var k in controller)
                            if (k.startsWith("event:") && !template.$c["$" + k.substring(6)])
                                template.$c["$" + k.substring(6)] = function () { }; //Empty event (avoid errors)

                        ext.getChildNodes = template.$api.childs;
                        ext.customController = template.$c;
                        ext.parentCustom = parentCustom;
                        //TODO: Replace with a local binding (bindings to values object only, creating a local model only for this control)
                        //      - access this local binding with {$name}
                        ext.onRender = function (element, template) {
                            if (!template.$t && template.$ && template.$[0] == "$") {
                                var data = values[template.$.substr(1)];
                                if (data instanceof D.Binding)
                                    data.bindExecutor(element,
                                        null,
                                        null,
                                        function (read) { element.nodeValue = read({ element: element }) }
                                    );
                                else
                                    element.nodeValue = data;
                            }
                            for (var k in template) {
                                if (k[0] == "$")
                                    continue;
                                var attr = template[k];
                                if (attr && attr[0] == "$") {
                                    var data = values[attr.substr(1)];
                                    if (data instanceof D.Binding)
                                        data.bindExecutor(element,
                                            null,
                                            null,
                                            function (read) { element.setAttribute(k, read({ element: element })) }
                                        );
                                    else
                                        element.setAttribute(k, data);
                                }
                            }
                        }
                        // Generate DOM
                        return renderControlDOM(ext);
                    };
                    UI.registerElement(id, controller);
                },
                id: { bindable: false, mandatory: true },
                allowchildren: { bindable: false },
                childs: { bindable: false }
            });

            UI.registerElement("children", {
                $ctor: function (template, ext) {
                    if (!ext.getChildNodes)
                        throw new Error("wolf:children can only be used inside a wolf:control UI definition");
                    var childTemplate = ext.getChildNodes(template.id);
                    var dom = [];
                    // Clear custom control private controller and API
                    ext = { parent: ext.parent }
                    for (var i in childTemplate)
                        dom = dom.concat(UI.instanceTemplate(childTemplate[i], ext));
                    return dom;
                },
                id: { bindable: false }
            })
        }

        // ====================
        // ====================
        // ==         Injection
        // ====================
        InitControlDefinitions();

        wolf.merge({
            // UI
            toast: toast,
            dialog: dialog,
            messageDialog: messageDialog,
            uiDialog: uiDialog,
            loadLibrary: loadLibrary,
        });
    });
})();