window.ls = window.ls || {}; window.ls.container = (function () { let stock = {}; let listeners = {}; let namespaces = {}; let set = function (name, object, singleton, watch = true) { if (typeof name !== "string") { throw new Error("var name must be of type string"); } if (typeof singleton !== "boolean") { throw new Error( 'var singleton "' + singleton + '" of service "' + name + '" must be of type boolean' ); } stock[name] = { name: name, object: object, singleton: singleton, instance: null, watch: watch, }; if (!watch) { return this; } let binds = listeners[name] || {}; for (let key in binds) { if (binds.hasOwnProperty(key)) { document.dispatchEvent(new CustomEvent(key)); } } return this; }; let get = function (name) { let service = undefined !== stock[name] ? stock[name] : null; if (null == service) { return null; } if (service.instance) { return service.instance; } let instance = typeof service.object === "function" ? this.resolve(service.object) : service.object; let skip = false; if ( service.watch && name !== "window" && name !== "document" && name !== "element" && typeof instance === "object" && instance !== null ) { let handler = { name: service.name, watch: function () {}, get: function (target, key) { if (key === "__name") { return this.name; } if (key === "__watch") { return this.watch; } if (key === "__proxy") { return true; } if ( typeof target[key] === "object" && target[key] !== null && !target[key].__proxy ) { let handler = Object.assign({}, this); handler.name = handler.name + "." + key; return new Proxy(target[key], handler); } else { return target[key]; } }, set: function (target, key, value, receiver) { if (key === "__name") { return (this.name = value); } if (key === "__watch") { return (this.watch = value); } target[key] = value; let path = receiver.__name + "." + key; document.dispatchEvent(new CustomEvent(path + ".changed")); if (skip) { return true; } skip = true; container.set("$prop", key, true); container.set("$value", value, true); container.resolve(this.watch); container.set("$key", null, true); container.set("$value", null, true); skip = false; return true; }, }; instance = new Proxy(instance, handler); } if (service.singleton) { service.instance = instance; } return instance; }; let resolve = function (target) { if (!target) { return () => {}; } let self = this; const REGEX_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/gm; const REGEX_FUNCTION_PARAMS = /(?:\s*(?:function\s*[^(]*)?\s*)((?:[^'"]|(?:(?:(['"])(?:(?:.*?[^\\]\2)|\2))))*?)\s*(?=(?:=>)|{)/m; const REGEX_PARAMETERS_VALUES = /\s*([\w\\$]+)\s*(?:=\s*((?:(?:(['"])(?:\3|(?:.*?[^\\]\3)))((\s*\+\s*)(?:(?:(['"])(?:\6|(?:.*?[^\\]\6)))|(?:[\w$]*)))*)|.*?))?\s*(?:,|$)/gm; function getParams(func) { let functionAsString = func.toString(); let params = []; let match; functionAsString = functionAsString.replace(REGEX_COMMENTS, ""); functionAsString = functionAsString.match(REGEX_FUNCTION_PARAMS)[1]; if (functionAsString.charAt(0) === "(") { functionAsString = functionAsString.slice(1, -1); } while ((match = REGEX_PARAMETERS_VALUES.exec(functionAsString))) { params.push(match[1]); } return params; } let args = getParams(target); return target.apply( target, args.map(function (value) { return self.get(value.trim()); }) ); }; let path = function (path, value, type) { type = type ? type : "assign"; path = container.scope(path).split("."); let name = path.shift(); let object = container.get(name); let result = null; while (path.length > 1) { if (!object) { return null; } object = object[path.shift()]; } let shift = path.shift(); if ( value !== null && value !== undefined && object && shift && (object[shift] !== undefined || object[shift] !== null) ) { switch (type) { case "append": if (!Array.isArray(object[shift])) { object[shift] = []; } object[shift].push(value); break; case "prepend": if (!Array.isArray(object[shift])) { object[shift] = []; } object[shift].unshift(value); break; case "splice": if (!Array.isArray(object[shift])) { object[shift] = []; } object[shift].splice(value, 1); break; default: object[shift] = value; } return true; } if (!object) { return null; } if (!shift) { result = object; } else { return object[shift]; } return result; }; let bind = function (element, path, callback) { let event = container.scope(path) + ".changed"; let service = event.split(".").slice(0, 1).pop(); let debug = element.getAttribute("data-debug") || false; listeners[service] = listeners[service] || {}; listeners[service][event] = true; let printer = (function (x) { return function () { if (!document.body.contains(element)) { element = null; document.removeEventListener(event, printer, false); return false; } let oldNamespaces = namespaces; namespaces = x; callback(); namespaces = oldNamespaces; }; })(Object.assign({}, namespaces)); document.addEventListener(event, printer); }; let addNamespace = function (key, scope) { namespaces[key] = scope; return this; }; let removeNamespace = function (key) { delete namespaces[key]; return this; }; let scope = function (path) { for (let [key, value] of Object.entries(namespaces)) { path = path.indexOf(".") > -1 ? path.replace(key + ".", value + ".") : path.replace(key, value); } return path; }; let container = { set: set, get: get, resolve: resolve, path: path, bind: bind, scope: scope, addNamespace: addNamespace, removeNamespace: removeNamespace, stock: stock, listeners: listeners, namespaces: namespaces, }; set("container", container, true, false); return container; })(); window.ls.container.set( "http", function (document) { let globalParams = [], globalHeaders = []; let addParam = function (url, param, value) { param = encodeURIComponent(param); let a = document.createElement("a"); param += value ? "=" + encodeURIComponent(value) : ""; a.href = url; a.search += (a.search ? "&" : "") + param; return a.href; }; let request = function (method, url, headers, payload, progress) { let i; if ( -1 === [ "GET", "POST", "PUT", "DELETE", "TRACE", "HEAD", "OPTIONS", "CONNECT", "PATCH", ].indexOf(method) ) { throw new Error("var method must contain a valid HTTP method name"); } if (typeof url !== "string") { throw new Error("var url must be of type string"); } if (typeof headers !== "object") { throw new Error("var headers must be of type object"); } if (typeof url !== "string") { throw new Error("var url must be of type string"); } for (i = 0; i < globalParams.length; i++) { url = addParam(url, globalParams[i].key, globalParams[i].value); } return new Promise(function (resolve, reject) { let xmlhttp = new XMLHttpRequest(); xmlhttp.open(method, url, true); for (i = 0; i < globalHeaders.length; i++) { xmlhttp.setRequestHeader( globalHeaders[i].key, globalHeaders[i].value ); } for (let key in headers) { if (headers.hasOwnProperty(key)) { xmlhttp.setRequestHeader(key, headers[key]); } } xmlhttp.onload = function () { if (4 === xmlhttp.readyState && 200 === xmlhttp.status) { resolve(xmlhttp.response); } else { document.dispatchEvent( new CustomEvent( "http-" + method.toLowerCase() + "-" + xmlhttp.status ) ); reject(new Error(xmlhttp.statusText)); } }; if (progress) { xmlhttp.addEventListener("progress", progress); xmlhttp.upload.addEventListener("progress", progress, false); } xmlhttp.onerror = function () { reject(new Error("Network Error")); }; xmlhttp.send(payload); }); }; return { get: function (url) { return request("GET", url, {}, ""); }, post: function (url, headers, payload) { return request("POST", url, headers, payload); }, put: function (url, headers, payload) { return request("PUT", url, headers, payload); }, patch: function (url, headers, payload) { return request("PATCH", url, headers, payload); }, delete: function (url) { return request("DELETE", url, {}, ""); }, addGlobalParam: function (key, value) { globalParams.push({ key: key, value: value }); }, addGlobalHeader: function (key, value) { globalHeaders.push({ key: key, value: value }); }, }; }, true, false ); window.ls.container.set( "cookie", function (document) { function get(name) { let value = "; " + document.cookie, parts = value.split("; " + name + "="); if (parts.length === 2) { return parts.pop().split(";").shift(); } return null; } function set(name, value, days) { let date = new Date(); date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); let expires = 0 < days ? "expires=" + date.toUTCString() : "expires=0"; document.cookie = name + "=" + value + ";" + expires + ";path=/"; return this; } return { get: get, set: set }; }, true, false ); window.ls.container.set( "view", function (http, container) { let stock = {}; let execute = function (view, node, container) { container.set("element", node, true, false); container.resolve(view.controller); if (true !== view.repeat) { node.removeAttribute(view.selector); } }; let parse = function (node, skip, callback) { if (node.tagName === "SCRIPT") { return; } if (node.attributes && skip !== true) { let attrs = []; let attrsLen = node.attributes.length; for (let x = 0; x < attrsLen; x++) { attrs.push(node.attributes[x].nodeName); } if (1 !== node.nodeType) { return; } if (attrs && attrsLen) { for (let x = 0; x < attrsLen; x++) { if (node.$lsSkip === true) { break; } let pointer = !/Edge/.test(navigator.userAgent) ? x : attrsLen - 1 - x; let length = attrsLen; let attr = attrs[pointer]; if (!stock[attr]) { continue; } let comp = stock[attr]; if (typeof comp.template === "function") { comp.template = container.resolve(comp.template); } if (!comp.template) { (function (comp, node, container) { execute(comp, node, container); })(comp, node, container); if (length !== attrsLen) { x--; } if (callback) { callback(); } continue; } node.classList.remove("load-end"); node.classList.add("load-start"); node.$lsSkip = true; http.get(comp.template).then( (function (node, comp) { return function (data) { node.$lsSkip = false; node.innerHTML = data; node.classList.remove("load-start"); node.classList.add("load-end"); (function (comp, node, container) { execute(comp, node, container); })(comp, node, container); parse(node, true); if (callback) { callback(); } }; })(node, comp), function (error) { throw new Error( "Failed to load comp template: " + error.message ); } ); } } } if (true === node.$lsSkip) { return; } let list = node ? node.childNodes : []; if (node.$lsSkip === true) { list = []; } for (let i = 0; i < list.length; i++) { let child = list[i]; parse(child); } }; return { stock: stock, add: function (object) { if (typeof object !== "object") { throw new Error("object must be of type object"); } let defaults = { selector: "", controller: function () {}, template: "", repeat: false, protected: false, }; for (let prop in defaults) { if (!defaults.hasOwnProperty(prop)) { continue; } if (prop in object) { continue; } object[prop] = defaults[prop]; } if (!object.selector) { throw new Error("View component is missing a selector attribute"); } stock[object.selector] = object; return this; }, render: function (element, callback) { parse(element, false, callback); element.dispatchEvent(new window.Event("rendered", { bubbles: false })); }, }; }, true, false ); window.ls.container.set( "router", function (window) { let getJsonFromUrl = function (URL) { let query; if (URL) { let pos = location.search.indexOf("?"); if (pos === -1) return []; query = location.search.substr(pos + 1); } else { query = location.search.substr(1); } let result = {}; query.split("&").forEach(function (part) { if (!part) { return; } part = part.split("+").join(" "); let eq = part.indexOf("="); let key = eq > -1 ? part.substr(0, eq) : part; let val = eq > -1 ? decodeURIComponent(part.substr(eq + 1)) : ""; let from = key.indexOf("["); if (from === -1) { result[decodeURIComponent(key)] = val; } else { let to = key.indexOf("]"); let index = decodeURIComponent(key.substring(from + 1, to)); key = decodeURIComponent(key.substring(0, from)); if (!result[key]) { result[key] = []; } if (!index) { result[key].push(val); } else { result[key][index] = val; } } }); return result; }; let states = []; let params = getJsonFromUrl(window.location.search); let hash = window.location.hash; let current = null; let previous = null; let getPrevious = () => previous; let getCurrent = () => current; let setPrevious = (value) => { previous = value; return this; }; let setCurrent = (value) => { current = value; return this; }; let setParam = function (key, value) { params[key] = value; return this; }; let getParam = function (key, def) { if (key in params) { return params[key]; } return def; }; let getParams = function () { return params; }; let getURL = function () { return window.location.href; }; let add = function (path, view) { if (typeof path !== "string") { throw new Error("path must be of type string"); } if (typeof view !== "object") { throw new Error("view must be of type object"); } states[states.length++] = { path: path, view: view }; return this; }; let match = function (location) { let url = location.pathname; states.sort(function (a, b) { return b.path.length - a.path.length; }); states.sort(function (a, b) { let n = b.path.split("/").length - a.path.split("/").length; if (n !== 0) { return n; } return b.path.length - a.path.length; }); for (let i = 0; i < states.length; i++) { let value = states[i]; value.path = value.path.substring(0, 1) !== "/" ? location.pathname + value.path : value.path; let match = new RegExp( "^" + value.path.replace(/:[^\s/]+/g, "([\\w-]+)") + "$" ); let found = url.match(match); if (found) { previous = current; current = value; return value; } } return null; }; let change = function (URL, replace) { if (!replace) { window.history.pushState({}, "", URL); } else { window.history.replaceState({}, "", URL); } window.dispatchEvent(new PopStateEvent("popstate", {})); return this; }; let reload = function () { return change(window.location.href); }; return { setParam: setParam, getParam: getParam, getParams: getParams, getURL: getURL, add: add, change: change, reload: reload, match: match, getCurrent: getCurrent, setCurrent: setCurrent, getPrevious: getPrevious, setPrevious: setPrevious, params: params, hash: hash, reset: function () { this.params = getJsonFromUrl(window.location.search); this.hash = window.location.hash; }, }; }, true, true ); window.ls.container.set( "expression", function (container, filter) { let paths = []; return { regex: /(\{{.*?\}})/gi, parse: function (string, def, cast = false) { def = def || ""; paths = []; return string .replace(this.regex, (match) => { let reference = match .substring(2, match.length - 2) .replace("['", ".") .replace("']", "") .trim(); reference = reference.split("|"); let path = container.scope(reference[0] || ""); let result = container.path(path); path = container.scope(path); if (!paths.includes(path)) { paths.push(path); } if (reference.length >= 2) { for (let i = 1; i < reference.length; i++) { result = filter.apply(reference[i], result); } } if (null === result || undefined === result) { result = def; } else if (typeof result === "object") { result = JSON.stringify(result, null, 4); } else if ( (typeof result === "object" || typeof result === "string") && cast ) { result = "'" + result + "'"; } return result; }) .replace(/\\{/g, "{") .replace(/\\}/g, "}"); }, getPaths: () => paths, }; }, true, false ); window.ls.container.set( "filter", function (container) { let filters = {}; let add = function (name, callback) { filters[name] = callback; return this; }; let apply = function (name, value) { container.set("$value", value, true, false); return container.resolve(filters[name]); }; add("uppercase", ($value) => { if (typeof $value !== "string") { return $value; } return $value.toUpperCase(); }); add("lowercase", ($value) => { if (typeof $value !== "string") { return $value; } return $value.toLowerCase(); }); return { add: add, apply: apply }; }, true, false ); window.ls.container.get("filter").add("escape", ($value) => { if (typeof $value !== "string") { return $value; } return $value .replace(/&/g, "&") .replace(//g, ">") .replace(/\"/g, """) .replace(/\'/g, "'") .replace(/\//g, "/"); }); window.ls = window.ls || {}; window.ls.container .set("window", window, true, false) .set("document", window.document, true, false) .set("element", window.document, true, false); window.ls.run = function (window) { try { this.view.render(window.document); } catch (error) { let handler = window.ls.container.resolve(this.error); handler(error); } }; window.ls.error = () => { return (error) => { console.error("ls-error", error.message, error.stack, error.toString()); }; }; window.ls.router = window.ls.container.get("router"); window.ls.view = window.ls.container.get("view"); window.ls.filter = window.ls.container.get("filter"); window.ls.container.get("view").add({ selector: "data-ls-router", controller: function (element, window, document, view, router) { let firstFromServer = element.getAttribute("data-first-from-server") === "true"; let scope = { selector: "data-ls-scope", template: false, repeat: true, controller: function () {}, }; let init = function (route) { let count = parseInt(element.getAttribute("data-ls-scope-count") || 0); element.setAttribute("data-ls-scope-count", count + 1); window.scrollTo(0, 0); if (window.document.body.scrollTo) { window.document.body.scrollTo(0, 0); } router.reset(); if (null === route) { return; } scope.template = undefined !== route.view.template ? route.view.template : null; scope.controller = undefined !== route.view.controller ? route.view.controller : function () {}; document.dispatchEvent(new CustomEvent("state-change")); if (firstFromServer && null === router.getPrevious()) { scope.template = ""; document.dispatchEvent(new CustomEvent("state-changed")); } else if (count === 1) { view.render(element, function () { document.dispatchEvent(new CustomEvent("state-changed")); }); } else if (null !== router.getPrevious()) { view.render(element, function () { document.dispatchEvent(new CustomEvent("state-changed")); }); } }; let findParent = function (tagName, el) { if ((el.nodeName || el.tagName).toLowerCase() === tagName.toLowerCase()) { return el; } while ((el = el.parentNode)) { if ( (el.nodeName || el.tagName).toLowerCase() === tagName.toLowerCase() ) { return el; } } return null; }; element.removeAttribute("data-ls-router"); element.setAttribute("data-ls-scope", ""); element.setAttribute("data-ls-scope-count", 1); view.add(scope); document.addEventListener("click", function (event) { let target = findParent("a", event.target); if (!target) { return false; } if (!target.href) { return false; } if (event.metaKey) { return false; } if ( target.hasAttribute("target") && "_blank" === target.getAttribute("target") ) { return false; } if (target.hostname !== window.location.hostname) { return false; } let route = router.match(target); if (null === route) { return false; } event.preventDefault(); if (window.location === target.href) { return false; } route.view.state = undefined === route.view.state ? true : route.view.state; if (true === route.view.state) { if ( router.getPrevious() && router.getPrevious().view && router.getPrevious().view.scope !== route.view.scope ) { window.location.href = target.href; return false; } window.history.pushState({}, "Unknown", target.href); } init(route); return true; }); window.addEventListener("popstate", function () { init(router.match(window.location)); }); window.addEventListener("hashchange", function () { init(router.match(window.location)); }); init(router.match(window.location)); }, }); window.ls.container.get("view").add({ selector: "data-ls-attrs", controller: function (element, expression, container) { let attrs = element.getAttribute("data-ls-attrs").trim().split(","); let paths = []; let debug = element.getAttribute("data-debug") || false; let check = () => { container.set("element", element, true, false); if (debug) { console.info("debug-ls-attrs attributes:", attrs); } for (let i = 0; i < attrs.length; i++) { let attr = attrs[i]; let key = expression.parse( attr.substring(0, attr.indexOf("=")) || attr ); paths = paths.concat(expression.getPaths()); let value = ""; if (attr.indexOf("=") > -1) { value = expression.parse(attr.substring(attr.indexOf("=") + 1)) || ""; paths = paths.concat(expression.getPaths()); } if (!key) { return null; } element.setAttribute(key, value); } }; check(); for (let i = 0; i < paths.length; i++) { let path = paths[i].split("."); while (path.length) { container.bind(element, path.join("."), check); path.pop(); } } }, }); window.ls.container.get("view").add({ selector: "data-ls-bind", controller: function (element, expression, container) { let debug = element.getAttribute("data-debug") || false; let echo = function (value, bind = true) { if ( element.tagName === "INPUT" || element.tagName === "SELECT" || element.tagName === "BUTTON" || element.tagName === "TEXTAREA" ) { let type = element.getAttribute("type"); if ("radio" === type) { if (value.toString() === element.value) { element.setAttribute("checked", "checked"); } else { element.removeAttribute("checked"); } if (bind) { element.addEventListener("change", () => { for (let i = 0; i < paths.length; i++) { if (element.checked) { value = element.value; } container.path(paths[i], value); } }); } return; } if ("checkbox" === type) { if ( typeof value === "boolean" || value === "true" || value === "false" ) { if (value === true || value === "true") { element.setAttribute("checked", "checked"); element.checked = true; } else { element.removeAttribute("checked"); element.checked = false; } } else { try { value = JSON.parse(value); element.checked = Array.isArray(value) && value.indexOf(element.value) > -1; value = element.value; } catch { return null; } } if (bind) { element.addEventListener("change", () => { for (let i = 0; i < paths.length; i++) { let value = container.path(paths[i]); let index = value.indexOf(element.value); if (element.checked && index < 0) { value.push(element.value); } if (!element.checked && index > -1) { value.splice(index, 1); } container.path(paths[i], value); } }); } return; } if (element.value !== value) { element.value = value; element.dispatchEvent(new Event("change")); } if (bind) { element.addEventListener("input", sync); element.addEventListener("change", sync); } } else { if (element.textContent != value) { element.textContent = value; } } }; let sync = (() => { return () => { if (debug) { console.info("debug-ls-bind", "sync-path", paths); console.info("debug-ls-bind", "sync-syntax", syntax); console.info("debug-ls-bind", "sync-syntax-parsed", parsedSyntax); console.info("debug-ls-bind", "sync-value", element.value); } for (let i = 0; i < paths.length; i++) { if ("{{" + paths[i] + "}}" !== parsedSyntax) { if (debug) { console.info("debug-ls-bind", "sync-skipped-path", paths[i]); console.info("debug-ls-bind", "sync-skipped-syntax", syntax); console.info( "debug-ls-bind", "sync-skipped-syntax-parsed", parsedSyntax ); } continue; } if (debug) { console.info("debug-ls-bind", "sync-loop-path", paths[i]); console.info("debug-ls-bind", "sync-loop-syntax", parsedSyntax); } container.path(paths[i], element.value); } }; })(); let syntax = element.getAttribute("data-ls-bind"); let parsedSyntax = container.scope(syntax); let unsync = !!element.getAttribute("data-unsync") || false; let result = expression.parse(syntax); let paths = expression.getPaths(); echo(result, !unsync); element.addEventListener("looped", function () { echo(expression.parse(parsedSyntax), false); }); for (let i = 0; i < paths.length; i++) { let path = paths[i].split("."); if (debug) { console.info("debug-ls-bind", "bind-path", path); console.info("debug-ls-bind", "bind-syntax", syntax); } while (path.length) { container.bind(element, path.join("."), () => { echo(expression.parse(parsedSyntax), false); }); path.pop(); } } }, }); window.ls.container.get("view").add({ selector: "data-ls-if", controller: function (element, expression, container, view) { let result = ""; let syntax = element.getAttribute("data-ls-if") || ""; let debug = element.getAttribute("data-debug") || false; let paths = []; let check = () => { if (debug) { console.info( "debug-ls-if", expression.parse( syntax.replace(/(\r\n|\n|\r)/gm, " "), "undefined", true ) ); } try { result = eval( expression.parse( syntax.replace(/(\r\n|\n|\r)/gm, " "), "undefined", true ) ); } catch (error) { throw new Error( 'Failed to evaluate expression "' + syntax + ' (resulted with: "' + result + '")": ' + error ); } if (debug) { console.info("debug-ls-if result:", result); } paths = expression.getPaths(); let prv = element.$lsSkip; element.$lsSkip = !result; if (!result) { element.style.visibility = "hidden"; element.style.display = "none"; } else { element.style.removeProperty("display"); element.style.removeProperty("visibility"); } if (prv === true && element.$lsSkip === false) { view.render(element); } }; check(); for (let i = 0; i < paths.length; i++) { let path = paths[i].split("."); while (path.length) { container.bind(element, path.join("."), check); path.pop(); } } }, }); window.ls.container.get("view").add({ selector: "data-ls-loop", template: false, nested: false, controller: function (element, view, container, window, expression) { let expr = expression.parse(element.getAttribute("data-ls-loop")); let as = element.getAttribute("data-ls-as"); let key = element.getAttribute("data-ls-key") || "$index"; let limit = parseInt( expression.parse(element.getAttribute("data-limit") || "") || -1 ); let debug = element.getAttribute("data-debug") || false; let echo = function () { let array = container.path(expr); let counter = 0; array = !array ? [] : array; let watch = !!(array && array.__proxy); while (element.hasChildNodes()) { element.removeChild(element.lastChild); element.lastChild = null; } if (array instanceof Array && typeof array !== "object") { throw new Error( "Reference value must be array or object. " + typeof array + " given" ); } let children = []; element.$lsSkip = true; element.style.visibility = 0 === array.length && element.style.visibility == "" ? "hidden" : "visible"; for (let prop in array) { if (counter == limit) { break; } counter++; if (!array.hasOwnProperty(prop)) { continue; } children[prop] = template.cloneNode(true); element.appendChild(children[prop]); ((index) => { let context = expr + "." + index; container.addNamespace(as, context); if (debug) { console.info("debug-ls-loop", "index", index); console.info("debug-ls-loop", "context", context); console.info( "debug-ls-loop", "context-path", container.path(context).name ); console.info("debug-ls-loop", "namespaces", container.namespaces); } container.set(as, container.path(context), true, watch); container.set(key, index, true, false); view.render(children[prop]); container.removeNamespace(as); })(prop); } element.dispatchEvent(new Event("looped")); }; let template = element.children.length === 1 ? element.children[0] : window.document.createElement("li"); echo(); container.bind(element, expr + ".length", echo); let path = (expr + ".length").split("."); while (path.length) { container.bind(element, path.join("."), echo); path.pop(); } }, }); window.ls.container.get("view").add({ selector: "data-ls-template", template: false, controller: function (element, view, http, expression, document, container) { let template = element.getAttribute("data-ls-template") || ""; let type = element.getAttribute("data-type") || "url"; let debug = element.getAttribute("data-debug") || false; let paths = []; let check = function (init = false) { let source = expression.parse(template); paths = expression.getPaths(); element.innerHTML = ""; if ("script" === type) { let inlineTemplate = document.getElementById(source); if (inlineTemplate && inlineTemplate.innerHTML) { element.innerHTML = inlineTemplate.innerHTML; element.dispatchEvent( new CustomEvent("template-loaded", { bubbles: true, cancelable: false, }) ); } else { if (debug) { console.error('Missing template "' + source + '"'); } } if (!init) { view.render(element); } return; } http.get(source).then( (function (element) { return function (data) { element.innerHTML = data; view.render(element); element.dispatchEvent( new CustomEvent("template-loaded", { bubbles: true, cancelable: false, }) ); }; })(element), function () { throw new Error("Failed loading template"); } ); }; check(true); for (let i = 0; i < paths.length; i++) { let path = paths[i].split("."); while (path.length) { container.bind(element, path.join("."), check); path.pop(); } } }, });