UNPKG

simplyview

Version:

Library to rapidly build UI components, using declarative tools

793 lines (784 loc) 24.2 kB
(() => { // src/activate.mjs var listeners = /* @__PURE__ */ new Map(); var activate = { addListener: (name, callback) => { if (!listeners.has(name)) { listeners.set(name, []); } listeners.get(name).push(callback); initialCall(name); }, removeListener: (name, callback) => { if (!listeners.has(name)) { return false; } listeners.set(name, listeners.get(name).filter((listener) => { return listener != callback; })); } }; function initialCall(name) { const nodes = document.querySelectorAll('[data-simply-activate="' + name + '"]'); if (nodes) { for (let node of nodes) { callListeners(node); } } } function callListeners(node) { const activate2 = node?.dataset?.simplyActivate; if (activate2 && listeners.has(activate2)) { for (let callback of listeners.get(activate2)) { callback.call(node); } } } function handleChanges(changes) { let activateNodes = []; for (let change of changes) { if (change.type == "childList") { for (let node of change.addedNodes) { if (node.querySelectorAll) { var toActivate = Array.from(node.querySelectorAll("[data-simply-activate]")); if (node.matches("[data-simply-activate]")) { toActivate.push(node); } activateNodes = activateNodes.concat(toActivate); } } } } for (let node of activateNodes) { callListeners(node); } } var observer = new MutationObserver(handleChanges); observer.observe(document, { subtree: true, childList: true }); // src/action.mjs function actions(options, optionsCompat) { if (optionsCompat) { let app2 = options; options = optionsCompat; options.app = options; } if (options.app) { const actionHandler = { get: (target, property) => { return target[property].bind(options.app); } }; return new Proxy(options.actions, actionHandler); } else { return options; } } // src/route.mjs function routes(options, optionsCompat) { if (optionsCompat) { let app2 = options; options = optionsCompat; options.app = options; } return new SimplyRoute(options); } var SimplyRoute = class { constructor(options = {}) { this.root = options.root || "/"; this.app = options.app; this.addMissingSlash = !!options.addMissingSlash; this.matchExact = !!options.matchExact; this.clear(); if (options.routes) { this.load(options.routes); } } load(routes2) { parseRoutes(routes2, this.routeInfo, this.matchExact); } clear() { this.routeInfo = []; this.listeners = { match: {}, call: {}, finish: {} }; } match(path, options) { let args = { path, options }; args = this.runListeners("match", args); path = args.path ? args.path : path; let matches; if (!path) { if (this.match(document.location.pathname + document.location.hash)) { return true; } else { return this.match(document.location.pathname); } } path = getPath(path); for (let route of this.routeInfo) { matches = route.match.exec(path); if (this.addMissingSlash && !matches?.length) { if (path && path[path.length - 1] != "/") { matches = route.match.exec(path + "/"); if (matches) { path += "/"; history.replaceState({}, "", getURL(path)); } } } if (matches && matches.length) { var params = {}; route.params.forEach((key, i2) => { if (key == "*") { key = "remainder"; } params[key] = matches[i2 + 1]; }); Object.assign(params, options); args.route = route; args.params = params; args = this.runListeners("call", args); params = args.params ? args.params : params; args.result = route.action.call(route, params); this.runListeners("finish", args); return args.result; } } return false; } runListeners(action, params) { if (!Object.keys(this.listeners[action])) { return; } Object.keys(this.listeners[action]).forEach((route) => { var routeRe = getRegexpFromRoute(route); if (routeRe.exec(params.path)) { var result; for (let callback of this.listeners[action][route]) { result = callback.call(this.app, params); if (result) { params = result; } } } }); return params; } handleEvents() { globalThis.addEventListener("popstate", () => { if (this.match(getPath(document.location.pathname + document.location.hash, this.root)) === false) { this.match(getPath(document.location.pathname, this.root)); } }); this.app.container.addEventListener("click", (evt) => { if (evt.ctrlKey) { return; } if (evt.which != 1) { return; } var link = evt.target; while (link && link.tagName != "A") { link = link.parentElement; } if (link && link.pathname && link.hostname == globalThis.location.hostname && !link.link && !link.dataset.simplyCommand) { let path = getPath(link.pathname + link.hash, this.root); if (!this.has(path)) { path = getPath(link.pathname, this.root); } if (this.has(path)) { let params = this.runListeners("goto", { path }); if (params.path) { if (this.goto(params.path)) { evt.preventDefault(); return false; } } } } }); } goto(path) { history.pushState({}, "", getURL(path)); return this.match(path); } has(path) { path = getPath(path, this.root); for (let route of this.routeInfo) { var matches = route.match.exec(path); if (matches && matches.length) { return true; } } return false; } addListener(action, route, callback) { if (["goto", "match", "call", "finish"].indexOf(action) == -1) { throw new Error("Unknown action " + action); } if (!this.listeners[action][route]) { this.listeners[action][route] = []; } this.listeners[action][route].push(callback); } removeListener(action, route, callback) { if (["match", "call", "finish"].indexOf(action) == -1) { throw new Error("Unknown action " + action); } if (!this.listeners[action][route]) { return; } this.listeners[action][route] = this.listeners[action][route].filter((listener) => { return listener != callback; }); } init(options) { if (options.root) { this.root = options.root; } } }; function getPath(path, root = "/") { if (path.substring(0, root.length) == root || root[root.length - 1] == "/" && path.length == root.length - 1 && path == root.substring(0, path.length)) { path = path.substring(root.length); } if (path[0] != "/" && path[0] != "#") { path = "/" + path; } return path; } function getURL(path, root) { path = getPath(path, root); if (root[root.length - 1] === "/" && path[0] === "/") { path = path.substring(1); } return root + path; } function getRegexpFromRoute(route, exact = false) { if (exact) { return new RegExp("^" + route.replace(/:\w+/g, "([^/]+)").replace(/:\*/, "(.*)") + "(\\?|$)"); } return new RegExp("^" + route.replace(/:\w+/g, "([^/]+)").replace(/:\*/, "(.*)")); } function parseRoutes(routes2, routeInfo, exact = false) { const paths = Object.keys(routes2); const matchParams = /:(\w+|\*)/g; for (let path of paths) { let matches = []; let params = []; do { matches = matchParams.exec(path); if (matches) { params.push(matches[1]); } } while (matches); routeInfo.push({ match: getRegexpFromRoute(path, exact), params, action: routes2[path] }); } return routeInfo; } // src/command.mjs var SimplyCommands = class { constructor(options = {}) { if (!options.app) { options.app = {}; } if (!options.app.container) { options.app.container = document.body; } this.app = options.app; this.$handlers = options.handlers || defaultHandlers; if (options.commands) { Object.assign(this, options.commands); } const commandHandler = (evt) => { const command = getCommand(evt, this.$handlers); if (!command) { return; } if (!this[command.name]) { console.error("simply.command: undefined command " + command.name, command.source); return; } const shouldContinue = this[command.name].call(options.app, command.source, command.value); if (shouldContinue !== true) { evt.preventDefault(); evt.stopPropagation(); return false; } }; options.app.container.addEventListener("click", commandHandler); options.app.container.addEventListener("submit", commandHandler); options.app.container.addEventListener("change", commandHandler); options.app.container.addEventListener("input", commandHandler); } call(command, el, value) { if (!this[command]) { console.error("simply.command: undefined command " + command); return; } return this[command].call(this.app, el, value); } action(name) { console.warn("deprecated call to `this.commands.action`"); let params = Array.from(arguments).slice(); params.shift(); return this.app.actions[name](...params); } appendHandler(handler) { this.$handlers.push(handler); } prependHandler(handler) { this.$handlers.unshift(handler); } }; function commands(options = {}, optionsCompat) { if (optionsCompat) { let app2 = options; options = optionsCompat; options.app = options; } return new SimplyCommands(options); } function getCommand(evt, handlers) { var el = evt.target.closest("[data-simply-command]"); if (el) { for (let handler of handlers) { if (el.matches(handler.match)) { if (handler.check(el, evt)) { return { name: el.dataset.simplyCommand, source: el, value: handler.get(el) }; } return null; } } } return null; } var defaultHandlers = [ { match: "input,select,textarea", get: function(el) { if (el.tagName === "SELECT" && el.multiple) { let values = []; for (let option of el.options) { if (option.selected) { values.push(option.value); } } return values; } return el.dataset.simplyValue || el.value; }, check: function(el, evt) { return evt.type == "change" || el.dataset.simplyImmediate && evt.type == "input"; } }, { match: "a,button", get: function(el) { return el.dataset.simplyValue || el.href || el.value; }, check: function(el, evt) { return evt.type == "click" && evt.ctrlKey == false && evt.button == 0; } }, { match: "form", get: function(el) { let data = {}; for (let input of Array.from(el.elements)) { if (input.tagName == "INPUT" && (input.type == "checkbox" || input.type == "radio")) { if (!input.checked) { return; } } if (data[input.name] && !Array.isArray(data[input.name])) { data[input.name] = [data[input.name]]; } if (Array.isArray(data[input.name])) { data[input.name].push(input.value); } else { data[input.name] = input.value; } } return data; }, check: function(el, evt) { return evt.type == "submit"; } }, { match: "*", get: function(el) { return el.dataset.simplyValue; }, check: function(el, evt) { return evt.type == "click" && evt.ctrlKey == false && evt.button == 0; } } ]; // src/key.mjs var KEY = Object.freeze({ Compose: 229, Control: 17, Meta: 224, Alt: 18, Shift: 16 }); var SimplyKey = class { constructor(options = {}) { if (!options.app) { options.app = {}; } if (!options.app.container) { options.app.container = document.body; } Object.assign(this, options.keys); const keyHandler = (e) => { if (e.isComposing || e.keyCode === KEY.Compose) { return; } if (e.defaultPrevented) { return; } if (!e.target) { return; } let selectedKeyboard = "default"; if (e.target.closest("[data-simply-keyboard]")) { selectedKeyboard = e.target.closest("[data-simply-keyboard]").dataset.simplyKeyboard; } let keyCombination = []; if (e.ctrlKey && e.keyCode != KEY.Control) { keyCombination.push("Control"); } if (e.metaKey && e.keyCode != KEY.Meta) { keyCombination.push("Meta"); } if (e.altKey && e.keyCode != KEY.Alt) { keyCombination.push("Alt"); } if (e.shiftKey && e.keyCode != KEY.Shift) { keyCombination.push("Shift"); } keyCombination.push(e.key.toLowerCase()); let keyboards = []; let keyboardElement = event.target.closest("[data-simply-keyboard]"); while (keyboardElement) { keyboards.push(keyboardElement.dataset.simplyKeyboard); keyboardElement = keyboardElement.parentNode.closest("[data-simply-keyboard]"); } keyboards.push(""); let keyboard, subkeyboard; let separators = ["+", "-"]; for (i in keyboards) { keyboard = keyboards[i]; if (keyboard == "") { subkeyboard = "default"; } else { subkeyboard = keyboard; keyboard += "."; } for (let separator of separators) { let keyString = keyCombination.join(separator); if (this[subkeyboard] && typeof this[subkeyboard][keyString] == "function") { let _continue = this[subkeyboard][keyString].call(this[subkeyboard], e); if (!_continue) { e.preventDefault(); return; } } if (typeof this[subkeyboard + keyString] == "function") { let _continue = this[subkeyboard + keyString].call(this, e); if (!_continue) { e.preventDefault(); return; } } if (this[selectedKeyboard] && this[selectedKeyboard][keyString]) { let targets = options.app.container.querySelectorAll('[data-simply-accesskey="' + keyboard + keyString + '"]'); if (targets.length) { targets.forEach((t) => t.click()); e.preventDefault(); } } } } }; options.app.container.addEventListener("keydown", keyHandler); } }; function keys(options = {}, optionsCompat) { if (optionsCompat) { let app2 = options; options = optionsCompat; options.app = options; } return new SimplyKey(options); } // src/view.mjs function view(options, optionsCompat) { if (optionsCompat) { let app2 = options; options = optionsCompat; options.app = options; } if (options.app) { options.app.view = options.view || {}; const load = () => { const data = options.app.view; const path = globalThis.editor.data.getDataPath(options.app.container || document.body); options.app.view = globalThis.editor.currentData[path]; Object.assign(options.app.view, data); }; if (globalThis.editor && globalThis.editor.currentData) { load(); } else { document.addEventListener("simply-content-loaded", load); } return options.app.view; } else { return options.view; } } // src/app.mjs var SimplyApp = class { constructor(options = {}) { this.container = options.container || document.body; for (let key in options) { switch (key) { case "commands": this.commands = commands({ app: this, container: this.container, commands: options.commands }); break; case "keys": case "keyboard": this.keys = keys({ app: this, keys: options.keys }); break; case "routes": this.routes = routes({ app: this, routes: options.routes }); break; case "actions": this.actions = actions({ app: this, actions: options.actions }); this.action = function(name) { console.warn("deprecated call to `this.action`"); let params = Array.from(arguments).slice(); params.shift(); return this.actions[name](...params); }; break; case "view": this.view = view({ app: this, view: options.view }); break; default: this[key] = options[key]; break; } } } get app() { return this; } }; function app(options = {}) { return new SimplyApp(options); } // src/include.mjs function throttle(callbackFunction, intervalTime) { let eventId = 0; return () => { const myArguments = arguments; if (eventId) { return; } else { eventId = globalThis.setTimeout(() => { callbackFunction.apply(this, myArguments); eventId = 0; }, intervalTime); } }; } var runWhenIdle = (() => { if (globalThis.requestIdleCallback) { return (callback) => { globalThis.requestIdleCallback(callback, { timeout: 500 }); }; } return globalThis.requestAnimationFrame; })(); function rebaseHref(relative, base) { let url = new URL(relative, base); if (include.cacheBuster) { url.searchParams.set("cb", include.cacheBuster); } return url.href; } var observer2; var loaded = {}; var head = globalThis.document.querySelector("head"); var currentScript = globalThis.document.currentScript; var getScriptURL; var currentScriptURL; if (!currentScript) { getScriptURL = (() => { var scripts = document.getElementsByTagName("script"); var index = scripts.length - 1; var myScript = scripts[index]; return () => myScript.src; })(); currentScriptURL = getScriptURL(); } else { currentScriptURL = currentScript.src; } var waitForPreviousScripts = async () => { return new Promise(function(resolve) { var next = globalThis.document.createElement("script"); next.src = "https://cdn.jsdelivr.net/gh/simplyedit/simplyview/dist/simply.include.next.js"; next.async = false; globalThis.document.addEventListener("simply-include-next", () => { head.removeChild(next); resolve(); }, { once: true, passive: true }); head.appendChild(next); }); }; var scriptLocations = []; var include = { cacheBuster: null, scripts: (scripts, base) => { let arr = scripts.slice(); const importScript = () => { const script = arr.shift(); if (!script) { return; } const attrs = [].map.call(script.attributes, (attr) => { return attr.name; }); let clone = globalThis.document.createElement("script"); for (const attr of attrs) { clone.setAttribute(attr, script.getAttribute(attr)); } clone.removeAttribute("data-simply-location"); if (!clone.src) { clone.innerHTML = script.innerHTML; waitForPreviousScripts().then(() => { const node = scriptLocations[script.dataset.simplyLocation]; node.parentNode.insertBefore(clone, node); node.parentNode.removeChild(node); importScript(); }); } else { clone.src = rebaseHref(clone.src, base); if (!clone.hasAttribute("async") && !clone.hasAttribute("defer")) { clone.async = false; } const node = scriptLocations[script.dataset.simplyLocation]; node.parentNode.insertBefore(clone, node); node.parentNode.removeChild(node); loaded[clone.src] = true; importScript(); } }; if (arr.length) { importScript(); } }, html: (html, link) => { let fragment = globalThis.document.createRange().createContextualFragment(html); const stylesheets = fragment.querySelectorAll('link[rel="stylesheet"],style'); for (let stylesheet of stylesheets) { if (stylesheet.href) { stylesheet.href = rebaseHref(stylesheet.href, link.href); } head.appendChild(stylesheet); } let scriptsFragment = globalThis.document.createDocumentFragment(); const scripts = fragment.querySelectorAll("script"); if (scripts.length) { for (let script of scripts) { let placeholder = globalThis.document.createComment(script.src || "inline script"); script.parentNode.insertBefore(placeholder, script); script.dataset.simplyLocation = scriptLocations.length; scriptLocations.push(placeholder); scriptsFragment.appendChild(script); } globalThis.setTimeout(function() { include.scripts(Array.from(scriptsFragment.children), link ? link.href : globalThis.location.href); }, 10); } link.parentNode.insertBefore(fragment, link ? link : null); } }; var included = {}; var includeLinks = async (links) => { let remainingLinks = [].reduce.call(links, (remainder, link) => { if (link.rel == "simply-include-once" && included[link.href]) { link.parentNode.removeChild(link); } else { included[link.href] = true; link.rel = "simply-include-loading"; remainder.push(link); } return remainder; }, []); for (let link of remainingLinks) { if (!link.href) { return; } const response = await fetch(link.href); if (!response.ok) { console.log("simply-include: failed to load " + link.href); continue; } console.log("simply-include: loaded " + link.href); const html = await response.text(); include.html(html, link); link.parentNode.removeChild(link); } }; var handleChanges2 = throttle(() => { runWhenIdle(() => { var links = globalThis.document.querySelectorAll('link[rel="simply-include"],link[rel="simply-include-once"]'); if (links.length) { includeLinks(links); } }); }); var observe = () => { observer2 = new MutationObserver(handleChanges2); observer2.observe(globalThis.document, { subtree: true, childList: true }); }; observe(); handleChanges2(); // src/everything.mjs var simply = { activate, action: actions, app, command: commands, include, key: keys, route: routes, view }; window.simply = simply; var everything_default = simply; })();