diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e8ed5d0dd9..dd5df1278a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,3 +48,11 @@ repos: )$ additional_dependencies: - tomli; python_version<'3.11' + - repo: local + hooks: + - id: nicegui-js-build + name: Build nicegui.js from src + entry: bash -c 'cd nicegui/static && npm run build' + language: system + files: ^nicegui/static/src/.*\.js$ + pass_filenames: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 285f6109f9..41beeeb09e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -368,6 +368,45 @@ To update or add new dependencies, we follow these steps: 3. Run `npm run build` to copy the dependencies into the `nicegui/static/` directory or to bundle the dependencies in the `nicegui/elements/.../` directories. +### JavaScript Development for nicegui.js + +NiceGUI's core JavaScript is modularized in `nicegui/static/src/` and built into `nicegui/static/nicegui.js`. + +**Setup:** + +```bash +cd nicegui/static +npm install +``` + +**Development workflow:** + +```bash +# Terminal 1: Watch and auto-rebuild +cd nicegui/static +npm run watch + +# Terminal 2: Run dev server +python main.py +``` + +**Module structure:** + +- `src/index.js` - Entry point +- `src/constants.js` - Python-style constants +- `src/colors.js` - Color utilities +- `src/elements.js` - Element access +- `src/events.js` - Event handling and throttling +- `src/render.js` - Vue rendering logic +- `src/socket.js` - Socket.IO handlers (integrated in app.js) +- `src/utils.js` - General utilities +- `src/app.js` - Vue app creation +- `src/quasar-hack.js` - Quasar CSS fixes + +The pre-commit hook automatically rebuilds `nicegui.js` when you modify `src/` files. + +**Testing:** Run `pytest tests/` after JavaScript changes. + The following tools are used to update other resources: - fetch_google_fonts.py for fetching the Google Fonts diff --git a/nicegui/static/.gitignore b/nicegui/static/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/nicegui/static/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/nicegui/static/nicegui.js b/nicegui/static/nicegui.js index 3986406315..aea675187e 100644 --- a/nicegui/static/nicegui.js +++ b/nicegui/static/nicegui.js @@ -1,189 +1,177 @@ -const True = true; -const False = false; -const None = undefined; - -let app = undefined; -let mounted_app = undefined; - -function applyColors(colors) { - const quasarColors = ["primary", "secondary", "accent", "dark", "dark-page", "positive", "negative", "info", "warning"]; - let customCSS = ""; - for (let color in colors) { - if (quasarColors.includes(color)) - continue; - const colorName = color.replaceAll("_", "-"); - const colorVar = "--q-" + colorName; - document.body.style.setProperty(colorVar, colors[color]); - customCSS += `.text-${colorName} { color: var(${colorVar}) !important; }\n`; - customCSS += `.bg-${colorName} { background-color: var(${colorVar}) !important; }\n`; +var NiceGUI = function(exports) { + "use strict"; + const True = true; + const False = false; + const None = void 0; + function applyColors(colors) { + const quasarColors = [ "primary", "secondary", "accent", "dark", "dark-page", "positive", "negative", "info", "warning" ]; + let customCSS = ""; + for (let color in colors) { + if (quasarColors.includes(color)) continue; + const colorName = color.replaceAll("_", "-"); + const colorVar = "--q-" + colorName; + document.body.style.setProperty(colorVar, colors[color]); + customCSS += `.text-${colorName} { color: var(${colorVar}) !important; }\n`; + customCSS += `.bg-${colorName} { background-color: var(${colorVar}) !important; }\n`; + } + if (!customCSS) return; + const style = document.createElement("style"); + style.innerHTML = customCSS; + style.dataset.niceguiCustomColors = ""; + document.head.querySelectorAll("[data-nicegui-custom-colors]").forEach(el => el.remove()); + document.getElementsByTagName("head")[0].appendChild(style); } - if (!customCSS) return; - const style = document.createElement("style"); - style.innerHTML = customCSS; - style.dataset.niceguiCustomColors = ""; - document.head.querySelectorAll("[data-nicegui-custom-colors]").forEach((el) => el.remove()); - document.getElementsByTagName("head")[0].appendChild(style); -} - -function parseElements(raw_elements) { - return JSON.parse( - raw_elements - .replace(/$/g, "$") - .replace(/`/g, "`") - .replace(/>/g, ">") - .replace(/</g, "<") - .replace(/&/g, "&") - ); -} - -function replaceUndefinedAttributes(element) { - element.class ??= []; - element.style ??= {}; - element.props ??= {}; - element.text ??= null; - element.events ??= []; - element.update_method ??= null; - element.slots = { - default: { ids: element.children || [] }, - ...(element.slots ?? {}), - }; -} - -function getElement(id) { - const _id = id instanceof Element ? id.id.slice(1) : id; - return mounted_app.$refs["r" + _id]; -} - -function getHtmlElement(id) { - let id_as_a_string = id.toString(); - if (!id_as_a_string.startsWith("c")) { - id_as_a_string = "c" + id_as_a_string; + let mounted_app; + function setMountedApp(app) { + mounted_app = app; } - return document.getElementById(id_as_a_string); -} - -function runMethod(target, method_name, args) { - if (typeof target === "object") { - if (method_name in target) { - return target[method_name](...args); - } else { - return eval(method_name)(target, ...args); - } + function getMountedApp() { + return mounted_app; } - const element = getElement(target); - if (element === null || element === undefined) return; - if (method_name in element) { - return element[method_name](...args); - } else if (method_name in (element.$refs.qRef || [])) { - return element.$refs.qRef[method_name](...args); - } else { - return eval(method_name)(element, ...args); + function replaceUndefinedAttributes(element) { + element.class ??= []; + element.style ??= {}; + element.props ??= {}; + element.text ??= null; + element.events ??= []; + element.update_method ??= null; + element.slots = { + default: { + ids: element.children || [] + }, + ...element.slots ?? {} + }; } -} - -function getComputedProp(target, prop_name) { - if (typeof target === "object" && prop_name in target) { - return target[prop_name]; + function getElement(id) { + const _id = id instanceof Element ? id.id.slice(1) : id; + return mounted_app.$refs["r" + _id]; } - const element = getElement(target); - if (element === null || element === undefined) return; - if (prop_name in element) { - return element[prop_name]; - } else if (prop_name in (element.$refs.qRef || [])) { - return element.$refs.qRef[prop_name]; + function getHtmlElement(id) { + let id_as_a_string = id.toString(); + if (!id_as_a_string.startsWith("c")) id_as_a_string = "c" + id_as_a_string; + return document.getElementById(id_as_a_string); } -} - -function emitEvent(event_name, ...args) { - getElement(0).$emit(event_name, ...args); -} - -function logAndEmit(level, message) { - if (level === "error") { - console.error(message); - } else if (level === "warning") { - console.warn(message); - } else { - console.log(message); + function parseElements(raw_elements) { + return JSON.parse(raw_elements.replace(/$/g, "$").replace(/`/g, "`").replace(/>/g, ">").replace(/</g, "<").replace(/&/g, "&")); } - window.socket.emit("log", { client_id: window.clientId, level, message }); -} - -function stringifyEventArgs(args, event_args) { - const result = []; - args.forEach((arg, i) => { - if (event_args !== null && i >= event_args.length) return; - let filtered = {}; - if (typeof arg !== "object" || arg === null || Array.isArray(arg)) { - filtered = arg; - } else { - for (let k in arg) { - // ignore "Restricted" fields in Firefox (see #2469) - if (k == "originalTarget") { - try { - arg[k].toString(); - } catch (e) { - continue; - } - } - if (event_args === null || event_args[i] === null || event_args[i].includes(k)) { - filtered[k] = arg[k]; + function runMethod(target, method_name, args) { + if (typeof target === "object") if (method_name in target) return target[method_name](...args); else return eval(method_name)(target, ...args); + const element = getElement(target); + if (element === null || element === void 0) return; + if (method_name in element) return element[method_name](...args); else if (method_name in (element.$refs.qRef || [])) return element.$refs.qRef[method_name](...args); else return eval(method_name)(element, ...args); + } + function getComputedProp(target, prop_name) { + if (typeof target === "object" && prop_name in target) return target[prop_name]; + const element = getElement(target); + if (element === null || element === void 0) return; + if (prop_name in element) return element[prop_name]; else if (prop_name in (element.$refs.qRef || [])) return element.$refs.qRef[prop_name]; + } + function emitEvent(event_name, ...args) { + getElement(0).$emit(event_name, ...args); + } + function logAndEmit(level, message) { + if (level === "error") console.error(message); else if (level === "warning") console.warn(message); else console.log(message); + window.socket.emit("log", { + client_id: window.clientId, + level: level, + message: message + }); + } + function runJavascript(code, request_id) { + new Promise(resolve => resolve(eval(code))).catch(reason => { + if (reason instanceof SyntaxError) return eval(`(async() => {${code}})()`); else throw reason; + }).then(result => { + if (request_id) window.socket.emit("javascript_response", { + request_id: request_id, + client_id: window.clientId, + result: result + }); + }); + } + function download(src, filename, mediaType, prefix) { + const anchor = document.createElement("a"); + if (typeof src === "string") anchor.href = src.startsWith("/") ? prefix + src : src; else anchor.href = URL.createObjectURL(new Blob([ src ], { + type: mediaType + })); + anchor.target = "_blank"; + anchor.download = filename || ""; + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); + if (typeof src !== "string") URL.revokeObjectURL(anchor.href); + } + function ack() { + if (!window.socket || !window.did_handshake) return; + if (window.ackedMessageId >= window.nextMessageId) return; + window.socket.emit("ack", { + client_id: window.clientId, + next_message_id: window.nextMessageId + }); + window.ackedMessageId = window.nextMessageId; + } + function createRandomUUID() { + try { + return crypto.randomUUID(); + } catch (e) { + return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c => (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16)); + } + } + const OLD_TAB_ID = sessionStorage.__nicegui_tab_closed === "false" ? sessionStorage.__nicegui_tab_id : null; + const TAB_ID = !sessionStorage.__nicegui_tab_id || sessionStorage.__nicegui_tab_closed === "false" ? sessionStorage.__nicegui_tab_id = createRandomUUID() : sessionStorage.__nicegui_tab_id; + sessionStorage.__nicegui_tab_closed = "false"; + window.onbeforeunload = function() { + sessionStorage.__nicegui_tab_closed = "true"; + }; + function stringifyEventArgs(args, event_args) { + const result = []; + args.forEach((arg, i) => { + if (event_args !== null && i >= event_args.length) return; + let filtered = {}; + if (typeof arg !== "object" || arg === null || Array.isArray(arg)) filtered = arg; else for (let k in arg) { + if (k == "originalTarget") try { + arg[k].toString(); + } catch (e) { + continue; } + if (event_args === null || event_args[i] === null || event_args[i].includes(k)) filtered[k] = arg[k]; } - } - result.push(JSON.stringify(filtered, (k, v) => (v instanceof Node || v instanceof Window ? undefined : v))); - }); - return result; -} - -const waitingCallbacks = new Map(); -function throttle(callback, time, leading, trailing, id) { - if (time <= 0) { - // execute callback immediately and return - callback(); - return; + result.push(JSON.stringify(filtered, (k, v) => v instanceof Node || v instanceof Window ? void 0 : v)); + }); + return result; } - if (waitingCallbacks.has(id)) { - if (trailing) { - // update trailing callback - waitingCallbacks.set(id, callback); - } - } else { - if (leading) { - // execute leading callback and set timeout to block more leading callbacks + const waitingCallbacks = new Map; + function throttle(callback, time, leading, trailing, id) { + if (time <= 0) { callback(); - waitingCallbacks.set(id, null); - } else if (trailing) { - // set trailing callback and set timeout to execute it - waitingCallbacks.set(id, callback); + return; } - if (leading || trailing) { - // set timeout to remove block and to execute trailing callback - setTimeout(() => { + if (waitingCallbacks.has(id)) { + if (trailing) waitingCallbacks.set(id, callback); + } else { + if (leading) { + callback(); + waitingCallbacks.set(id, null); + } else if (trailing) waitingCallbacks.set(id, callback); + if (leading || trailing) setTimeout(() => { const trailingCallback = waitingCallbacks.get(id); if (trailingCallback) trailingCallback(); waitingCallbacks.delete(id); - }, 1000 * time); + }, 1e3 * time); } } -} -function renderRecursively(elements, id, propsContext) { - const element = elements[id]; - if (element === undefined) { - return; - } - - const props = { - id: "c" + id, - ref: "r" + id, - key: id, // HACK: workaround for #600 and #898 - class: element.class.join(" ") || undefined, - style: Object.entries(element.style).reduce((str, [p, val]) => `${str}${p}:${val};`, "") || undefined, - ...element.props, - }; - Object.entries(props).forEach(([key, value]) => { - if (key.startsWith(":")) { - try { + function renderRecursively(elements, id, propsContext) { + const element = elements[id]; + if (element === void 0) return; + const props = { + id: "c" + id, + ref: "r" + id, + key: id, + class: element.class.join(" ") || void 0, + style: Object.entries(element.style).reduce((str, [p, val]) => `${str}${p}:${val};`, "") || void 0, + ...element.props + }; + Object.entries(props).forEach(([key, value]) => { + if (key.startsWith(":")) try { try { props[key.substring(1)] = new Function("props", `return (${value})`)(propsContext); } catch (e) { @@ -193,303 +181,258 @@ function renderRecursively(elements, id, propsContext) { } catch (e) { console.error(`Error while converting ${key} attribute to function:`, e); } - } - }); - element.events.forEach((event) => { - let event_name = "on" + event.type[0].toLocaleUpperCase() + event.type.substring(1); - event.specials.forEach((s) => (event_name += s[0].toLocaleUpperCase() + s.substring(1))); - - const emit = (...args) => { - const emitter = () => - window.socket?.emit("event", { + }); + element.events.forEach(event => { + let event_name = "on" + event.type[0].toLocaleUpperCase() + event.type.substring(1); + event.specials.forEach(s => event_name += s[0].toLocaleUpperCase() + s.substring(1)); + const emit = (...args) => { + const emitter = () => window.socket?.emit("event", { id: id, client_id: window.clientId, listener_id: event.listener_id, - args: stringifyEventArgs(args, event.args), + args: stringifyEventArgs(args, event.args) }); - const delayed_emitter = () => { - if (window.did_handshake) emitter(); - else setTimeout(delayed_emitter, 10); + const delayed_emitter = () => { + if (window.did_handshake) emitter(); else setTimeout(delayed_emitter, 10); + }; + throttle(delayed_emitter, event.throttle, event.leading_events, event.trailing_events, event.listener_id); + if (element.props["loopback"] === False && event.type == "update:modelValue") element.props["model-value"] = args; }; - throttle(delayed_emitter, event.throttle, event.leading_events, event.trailing_events, event.listener_id); - if (element.props["loopback"] === False && event.type == "update:modelValue") { - element.props["model-value"] = args; - } + let handler; + if (event.js_handler) handler = eval(event.js_handler); else handler = emit; + handler = Vue.withModifiers(handler, event.modifiers); + handler = event.keys.length ? Vue.withKeys(handler, event.keys) : handler; + if (props[event_name]) props[event_name].push(handler); else props[event_name] = [ handler ]; + }); + const slots = {}; + const element_slots = { + default: { + ids: element.children || [] + }, + ...element.slots }; - - let handler; - if (event.js_handler) { - const props = propsContext; // make `props` accessible from inside the event handler - handler = eval(event.js_handler); - } else { - handler = emit; - } - - handler = Vue.withModifiers(handler, event.modifiers); - handler = event.keys.length ? Vue.withKeys(handler, event.keys) : handler; - if (props[event_name]) { - props[event_name].push(handler); - } else { - props[event_name] = [handler]; - } - }); - const slots = {}; - const element_slots = { - default: { ids: element.children || [] }, - ...element.slots, - }; - Object.entries(element_slots).forEach(([name, data]) => { - slots[name] = (props) => { - const rendered = []; - if (data.template) { - rendered.push( - Vue.h( - { - props: { props: { type: Object, default: {} } }, - template: data.template, - }, - { - props: props, + Object.entries(element_slots).forEach(([name, data]) => { + slots[name] = props => { + const rendered = []; + if (data.template) rendered.push(Vue.h({ + props: { + props: { + type: Object, + default: {} } - ) - ); - } - const children = data.ids.map((id) => renderRecursively(elements, id, props || propsContext)); - if (name === "default" && element.text !== null) { - children.unshift(element.text); - } - return [...rendered, ...children]; - }; - }); - return Vue.h(app.config.isNativeTag(element.tag) ? element.tag : Vue.resolveComponent(element.tag), props, slots); -} - -function runJavascript(code, request_id) { - new Promise((resolve) => resolve(eval(code))) - .catch((reason) => { - if (reason instanceof SyntaxError) return eval(`(async() => {${code}})()`); - else throw reason; - }) - .then((result) => { - if (request_id) { - window.socket.emit("javascript_response", { request_id, client_id: window.clientId, result }); - } + }, + template: data.template + }, { + props: props + })); + const children = data.ids.map(id => renderRecursively(elements, id, props || propsContext)); + if (name === "default" && element.text !== null) children.unshift(element.text); + return [ ...rendered, ...children ]; + }; }); -} - -function download(src, filename, mediaType, prefix) { - const anchor = document.createElement("a"); - if (typeof src === "string") { - anchor.href = src.startsWith("/") ? prefix + src : src; - } else { - anchor.href = URL.createObjectURL(new Blob([src], { type: mediaType })); + const app = getApp(); + return Vue.h(app.config.isNativeTag(element.tag) ? element.tag : Vue.resolveComponent(element.tag), props, slots); } - anchor.target = "_blank"; - anchor.download = filename || ""; - document.body.appendChild(anchor); - anchor.click(); - document.body.removeChild(anchor); - if (typeof src !== "string") { - URL.revokeObjectURL(anchor.href); + let app; + function getApp() { + return app; } -} - -function ack() { - if (!window.socket || !window.did_handshake) return; - if (window.ackedMessageId >= window.nextMessageId) return; - window.socket.emit("ack", { - client_id: window.clientId, - next_message_id: window.nextMessageId, - }); - window.ackedMessageId = window.nextMessageId; -} - -function createRandomUUID() { - try { - return crypto.randomUUID(); - } catch (e) { - // https://stackoverflow.com/a/2117523/3419103 - return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) => - (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16) - ); - } -} - -const OLD_TAB_ID = sessionStorage.__nicegui_tab_closed === "false" ? sessionStorage.__nicegui_tab_id : null; -const TAB_ID = - !sessionStorage.__nicegui_tab_id || sessionStorage.__nicegui_tab_closed === "false" - ? (sessionStorage.__nicegui_tab_id = createRandomUUID()) - : sessionStorage.__nicegui_tab_id; -sessionStorage.__nicegui_tab_closed = "false"; -window.onbeforeunload = function () { - sessionStorage.__nicegui_tab_closed = "true"; -}; - -function createApp(elements, options) { - Object.entries(elements).forEach(([_, element]) => replaceUndefinedAttributes(element)); - setInterval(() => ack(), 3000); - return (app = Vue.createApp({ - data() { - return { - elements, - }; - }, - render() { - return renderRecursively(this.elements, 0); - }, - mounted() { - mounted_app = this; - window.documentId = createRandomUUID(); - window.clientId = options.query.client_id; - const url = window.location.protocol === "https:" ? "wss://" : "ws://" + window.location.host; - window.path_prefix = options.prefix; - window.nextMessageId = options.query.next_message_id; - window.ackedMessageId = -1; - window.socket = io(url, { - path: `${options.prefix}/_nicegui_ws/socket.io`, - query: options.query, - extraHeaders: options.extraHeaders, - transports: - "prerendering" in document && document.prerendering === true - ? ["polling", ...options.transports] - : options.transports, - }); - window.did_handshake = false; - const messageHandlers = { - connect: () => { - function wrapFunction(originalFunction) { - const MAX_WEBSOCKET_MESSAGE_SIZE = 1000000 - 100; // 1MB without 100 bytes of slack for the message header - return function (...args) { - const msg = args[0]; - if (typeof msg === "string" && msg.length > MAX_WEBSOCKET_MESSAGE_SIZE) { - const errorMessage = `Payload size ${msg.length} exceeds the maximum allowed limit.`; - console.error(errorMessage); - args[0] = `42["log",{"client_id":"${window.clientId}","level":"error","message":"${errorMessage}"}]`; - if (window.tooLongMessageTimerId) clearTimeout(window.tooLongMessageTimerId); - const popup = document.getElementById("too_long_message_popup"); - popup.ariaHidden = false; - window.tooLongMessageTimerId = setTimeout(() => (popup.ariaHidden = true), 5000); - } - return originalFunction.call(this, ...args); + function createApp(elements, options) { + Object.entries(elements).forEach(([_, element]) => replaceUndefinedAttributes(element)); + setInterval(() => ack(), 3e3); + return app = Vue.createApp({ + data() { + return { + elements: elements + }; + }, + render() { + return renderRecursively(this.elements, 0); + }, + mounted() { + setMountedApp(this); + window.documentId = createRandomUUID(); + window.clientId = options.query.client_id; + const url = window.location.protocol === "https:" ? "wss://" : "ws://" + window.location.host; + window.path_prefix = options.prefix; + window.nextMessageId = options.query.next_message_id; + window.ackedMessageId = -1; + window.socket = io(url, { + path: `${options.prefix}/_nicegui_ws/socket.io`, + query: options.query, + extraHeaders: options.extraHeaders, + transports: "prerendering" in document && document.prerendering === true ? [ "polling", ...options.transports ] : options.transports + }); + window.did_handshake = false; + const messageHandlers = { + connect: () => { + function wrapFunction(originalFunction) { + const MAX_WEBSOCKET_MESSAGE_SIZE = 1e6 - 100; + return function(...args) { + const msg = args[0]; + if (typeof msg === "string" && msg.length > MAX_WEBSOCKET_MESSAGE_SIZE) { + const errorMessage = `Payload size ${msg.length} exceeds the maximum allowed limit.`; + console.error(errorMessage); + args[0] = `42["log",{"client_id":"${window.clientId}","level":"error","message":"${errorMessage}"}]`; + if (window.tooLongMessageTimerId) clearTimeout(window.tooLongMessageTimerId); + const popup = document.getElementById("too_long_message_popup"); + popup.ariaHidden = false; + window.tooLongMessageTimerId = setTimeout(() => popup.ariaHidden = true, 5e3); + } + return originalFunction.call(this, ...args); + }; + } + const transport = window.socket.io.engine.transport; + if (transport?.ws?.send) transport.ws.send = wrapFunction(transport.ws.send); + if (transport?.doWrite) transport.doWrite = wrapFunction(transport.doWrite); + const args = { + client_id: window.clientId, + document_id: window.documentId, + tab_id: TAB_ID, + old_tab_id: OLD_TAB_ID, + next_message_id: window.nextMessageId }; - } - const transport = window.socket.io.engine.transport; - if (transport?.ws?.send) transport.ws.send = wrapFunction(transport.ws.send); - if (transport?.doWrite) transport.doWrite = wrapFunction(transport.doWrite); - - const args = { - client_id: window.clientId, - document_id: window.documentId, - tab_id: TAB_ID, - old_tab_id: OLD_TAB_ID, - next_message_id: window.nextMessageId, - }; - window.socket.emit("handshake", args, (ok) => { - if (!ok) { - console.log("reloading because handshake failed for clientId " + window.clientId); + window.socket.emit("handshake", args, ok => { + if (!ok) { + console.log("reloading because handshake failed for clientId " + window.clientId); + window.location.reload(); + } + window.did_handshake = true; + document.getElementById("popup").ariaHidden = true; + }); + }, + connect_error: err => { + if (err.message == "timeout") { + console.log("reloading because connection timed out"); window.location.reload(); } - window.did_handshake = true; - document.getElementById("popup").ariaHidden = true; - }); - }, - connect_error: (err) => { - if (err.message == "timeout") { - console.log("reloading because connection timed out"); - window.location.reload(); // see https://github.com/zauberzeug/nicegui/issues/198 - } - }, - try_reconnect: async () => { - document.getElementById("popup").ariaHidden = false; - await fetch(window.location.href, { headers: { "NiceGUI-Check": "try_reconnect" } }); - console.log("reloading because reconnect was requested"); - window.location.reload(); - }, - disconnect: () => { - document.getElementById("popup").ariaHidden = false; - }, - load_js_components: async (msg) => { - const urls = msg.components.map((c) => `${options.prefix}/_nicegui/${options.version}/components/${c.key}`); - const imports = await Promise.all(urls.map((url) => import(url))); - msg.components.forEach((c, i) => app.component(c.tag, imports[i].default)); - }, - update: async (msg) => { - let eventListenersChanged = false; - for (const [id, element] of Object.entries(msg)) { - if (element === null) continue; - if (!(id in this.elements)) continue; - const oldListenerIds = new Set((this.elements[id]?.events || []).map((ev) => ev.listener_id)); - if (element.events?.some((e) => !oldListenerIds.has(e.listener_id))) { - delete this.elements[id]; - eventListenersChanged = true; + }, + try_reconnect: async () => { + document.getElementById("popup").ariaHidden = false; + await fetch(window.location.href, { + headers: { + "NiceGUI-Check": "try_reconnect" + } + }); + console.log("reloading because reconnect was requested"); + window.location.reload(); + }, + disconnect: () => { + document.getElementById("popup").ariaHidden = false; + }, + load_js_components: async msg => { + const urls = msg.components.map(c => `${options.prefix}/_nicegui/${options.version}/components/${c.key}`); + const imports = await Promise.all(urls.map(url => import(url))); + msg.components.forEach((c, i) => app.component(c.tag, imports[i].default)); + }, + update: async msg => { + let eventListenersChanged = false; + for (const [id, element] of Object.entries(msg)) { + if (element === null) continue; + if (!(id in this.elements)) continue; + const oldListenerIds = new Set((this.elements[id]?.events || []).map(ev => ev.listener_id)); + if (element.events?.some(e => !oldListenerIds.has(e.listener_id))) { + delete this.elements[id]; + eventListenersChanged = true; + } } - } - if (eventListenersChanged) { - logAndEmit("warning", "Event listeners changed after initial definition. Re-rendering affected elements."); - await this.$nextTick(); - } - - for (const [id, element] of Object.entries(msg)) { - if (element === null) { - delete this.elements[id]; - continue; + if (eventListenersChanged) { + logAndEmit("warning", "Event listeners changed after initial definition. Re-rendering affected elements."); + await this.$nextTick(); } - replaceUndefinedAttributes(element); - this.elements[id] = element; - } - - await this.$nextTick(); - for (const [id, element] of Object.entries(msg)) { - if (element?.update_method) { - getElement(id)?.[element.update_method](); + for (const [id, element] of Object.entries(msg)) { + if (element === null) { + delete this.elements[id]; + continue; + } + replaceUndefinedAttributes(element); + this.elements[id] = element; } - } - }, - run_javascript: (msg) => runJavascript(msg.code, msg.request_id), - open: (msg) => { - const url = msg.path.startsWith("/") ? options.prefix + msg.path : msg.path; - const target = msg.new_tab ? "_blank" : "_self"; - window.open(url, target); - }, - download: (msg) => download(msg.src, msg.filename, msg.media_type, options.prefix), - notify: (msg) => Quasar.Notify.create(msg), - }; - const socketMessageQueue = []; - let isProcessingSocketMessage = false; - for (const [event, handler] of Object.entries(messageHandlers)) { - window.socket.on(event, async (...args) => { - if (args.length > 0 && args[0]._id !== undefined) { + await this.$nextTick(); + for (const [id, element] of Object.entries(msg)) if (element?.update_method) getElement(id)?.[element.update_method](); + }, + run_javascript: msg => runJavascript(msg.code, msg.request_id), + open: msg => { + const url = msg.path.startsWith("/") ? options.prefix + msg.path : msg.path; + const target = msg.new_tab ? "_blank" : "_self"; + window.open(url, target); + }, + download: msg => download(msg.src, msg.filename, msg.media_type, options.prefix), + notify: msg => Quasar.Notify.create(msg) + }; + const socketMessageQueue = []; + let isProcessingSocketMessage = false; + for (const [event, handler] of Object.entries(messageHandlers)) window.socket.on(event, async (...args) => { + if (args.length > 0 && args[0]._id !== void 0) { const message_id = args[0]._id; if (message_id < window.nextMessageId) return; window.nextMessageId = message_id + 1; delete args[0]._id; } socketMessageQueue.push(() => handler(...args)); - if (!isProcessingSocketMessage) { - while (socketMessageQueue.length > 0) { - const handler = socketMessageQueue.shift(); - isProcessingSocketMessage = true; - try { - await handler(); - } catch (e) { - console.error(e); - } - isProcessingSocketMessage = false; + if (!isProcessingSocketMessage) while (socketMessageQueue.length > 0) { + const handler = socketMessageQueue.shift(); + isProcessingSocketMessage = true; + try { + await handler(); + } catch (e) { + console.error(e); } + isProcessingSocketMessage = false; } }); } - }, - })); -} - -// HACK: remove Quasar's rules for divs in QCard (#2265, #2301) -for (const importRule of document.styleSheets[0].cssRules) { - if (importRule instanceof CSSImportRule && /quasar/.test(importRule.styleSheet?.href)) { - for (const rule of Array.from(importRule.styleSheet.cssRules)) { - if (rule instanceof CSSStyleRule && /\.q-card > div/.test(rule.selectorText)) { - if (/\.q-card > div/.test(rule.selectorText)) rule.selectorText = ".nicegui-card-tight" + rule.selectorText; - } - } + }); + } + for (const importRule of document.styleSheets[0].cssRules) if (importRule instanceof CSSImportRule && /quasar/.test(importRule.styleSheet?.href)) for (const rule of Array.from(importRule.styleSheet.cssRules)) if (rule instanceof CSSStyleRule && /\.q-card > div/.test(rule.selectorText)) if (/\.q-card > div/.test(rule.selectorText)) rule.selectorText = ".nicegui-card-tight" + rule.selectorText; + if (typeof window !== "undefined") { + window.True = True; + window.False = False; + window.None = None; + window.getElement = getElement; + window.getHtmlElement = getHtmlElement; + window.runMethod = runMethod; + window.getComputedProp = getComputedProp; + window.emitEvent = emitEvent; + window.logAndEmit = logAndEmit; + window.runJavascript = runJavascript; + window.download = download; + window.ack = ack; + window.parseElements = parseElements; + window.createApp = createApp; + window.applyColors = applyColors; + window.TAB_ID = TAB_ID; + window.OLD_TAB_ID = OLD_TAB_ID; + Object.defineProperty(window, "mounted_app", { + get: getMountedApp, + enumerable: true, + configurable: true + }); + Object.defineProperty(window, "app", { + get: getApp, + enumerable: true, + configurable: true + }); } -} + exports.False = False; + exports.None = None; + exports.OLD_TAB_ID = OLD_TAB_ID; + exports.TAB_ID = TAB_ID; + exports.True = True; + exports.ack = ack; + exports.applyColors = applyColors; + exports.createApp = createApp; + exports.download = download; + exports.emitEvent = emitEvent; + exports.getApp = getApp; + exports.getComputedProp = getComputedProp; + exports.getElement = getElement; + exports.getHtmlElement = getHtmlElement; + exports.getMountedApp = getMountedApp; + exports.logAndEmit = logAndEmit; + exports.parseElements = parseElements; + exports.replaceUndefinedAttributes = replaceUndefinedAttributes; + exports.runJavascript = runJavascript; + exports.runMethod = runMethod; + return exports; +}({}); diff --git a/nicegui/static/nicegui.old.js b/nicegui/static/nicegui.old.js new file mode 100644 index 0000000000..3986406315 --- /dev/null +++ b/nicegui/static/nicegui.old.js @@ -0,0 +1,495 @@ +const True = true; +const False = false; +const None = undefined; + +let app = undefined; +let mounted_app = undefined; + +function applyColors(colors) { + const quasarColors = ["primary", "secondary", "accent", "dark", "dark-page", "positive", "negative", "info", "warning"]; + let customCSS = ""; + for (let color in colors) { + if (quasarColors.includes(color)) + continue; + const colorName = color.replaceAll("_", "-"); + const colorVar = "--q-" + colorName; + document.body.style.setProperty(colorVar, colors[color]); + customCSS += `.text-${colorName} { color: var(${colorVar}) !important; }\n`; + customCSS += `.bg-${colorName} { background-color: var(${colorVar}) !important; }\n`; + } + if (!customCSS) return; + const style = document.createElement("style"); + style.innerHTML = customCSS; + style.dataset.niceguiCustomColors = ""; + document.head.querySelectorAll("[data-nicegui-custom-colors]").forEach((el) => el.remove()); + document.getElementsByTagName("head")[0].appendChild(style); +} + +function parseElements(raw_elements) { + return JSON.parse( + raw_elements + .replace(/$/g, "$") + .replace(/`/g, "`") + .replace(/>/g, ">") + .replace(/</g, "<") + .replace(/&/g, "&") + ); +} + +function replaceUndefinedAttributes(element) { + element.class ??= []; + element.style ??= {}; + element.props ??= {}; + element.text ??= null; + element.events ??= []; + element.update_method ??= null; + element.slots = { + default: { ids: element.children || [] }, + ...(element.slots ?? {}), + }; +} + +function getElement(id) { + const _id = id instanceof Element ? id.id.slice(1) : id; + return mounted_app.$refs["r" + _id]; +} + +function getHtmlElement(id) { + let id_as_a_string = id.toString(); + if (!id_as_a_string.startsWith("c")) { + id_as_a_string = "c" + id_as_a_string; + } + return document.getElementById(id_as_a_string); +} + +function runMethod(target, method_name, args) { + if (typeof target === "object") { + if (method_name in target) { + return target[method_name](...args); + } else { + return eval(method_name)(target, ...args); + } + } + const element = getElement(target); + if (element === null || element === undefined) return; + if (method_name in element) { + return element[method_name](...args); + } else if (method_name in (element.$refs.qRef || [])) { + return element.$refs.qRef[method_name](...args); + } else { + return eval(method_name)(element, ...args); + } +} + +function getComputedProp(target, prop_name) { + if (typeof target === "object" && prop_name in target) { + return target[prop_name]; + } + const element = getElement(target); + if (element === null || element === undefined) return; + if (prop_name in element) { + return element[prop_name]; + } else if (prop_name in (element.$refs.qRef || [])) { + return element.$refs.qRef[prop_name]; + } +} + +function emitEvent(event_name, ...args) { + getElement(0).$emit(event_name, ...args); +} + +function logAndEmit(level, message) { + if (level === "error") { + console.error(message); + } else if (level === "warning") { + console.warn(message); + } else { + console.log(message); + } + window.socket.emit("log", { client_id: window.clientId, level, message }); +} + +function stringifyEventArgs(args, event_args) { + const result = []; + args.forEach((arg, i) => { + if (event_args !== null && i >= event_args.length) return; + let filtered = {}; + if (typeof arg !== "object" || arg === null || Array.isArray(arg)) { + filtered = arg; + } else { + for (let k in arg) { + // ignore "Restricted" fields in Firefox (see #2469) + if (k == "originalTarget") { + try { + arg[k].toString(); + } catch (e) { + continue; + } + } + if (event_args === null || event_args[i] === null || event_args[i].includes(k)) { + filtered[k] = arg[k]; + } + } + } + result.push(JSON.stringify(filtered, (k, v) => (v instanceof Node || v instanceof Window ? undefined : v))); + }); + return result; +} + +const waitingCallbacks = new Map(); +function throttle(callback, time, leading, trailing, id) { + if (time <= 0) { + // execute callback immediately and return + callback(); + return; + } + if (waitingCallbacks.has(id)) { + if (trailing) { + // update trailing callback + waitingCallbacks.set(id, callback); + } + } else { + if (leading) { + // execute leading callback and set timeout to block more leading callbacks + callback(); + waitingCallbacks.set(id, null); + } else if (trailing) { + // set trailing callback and set timeout to execute it + waitingCallbacks.set(id, callback); + } + if (leading || trailing) { + // set timeout to remove block and to execute trailing callback + setTimeout(() => { + const trailingCallback = waitingCallbacks.get(id); + if (trailingCallback) trailingCallback(); + waitingCallbacks.delete(id); + }, 1000 * time); + } + } +} +function renderRecursively(elements, id, propsContext) { + const element = elements[id]; + if (element === undefined) { + return; + } + + const props = { + id: "c" + id, + ref: "r" + id, + key: id, // HACK: workaround for #600 and #898 + class: element.class.join(" ") || undefined, + style: Object.entries(element.style).reduce((str, [p, val]) => `${str}${p}:${val};`, "") || undefined, + ...element.props, + }; + Object.entries(props).forEach(([key, value]) => { + if (key.startsWith(":")) { + try { + try { + props[key.substring(1)] = new Function("props", `return (${value})`)(propsContext); + } catch (e) { + props[key.substring(1)] = eval(value); + } + delete props[key]; + } catch (e) { + console.error(`Error while converting ${key} attribute to function:`, e); + } + } + }); + element.events.forEach((event) => { + let event_name = "on" + event.type[0].toLocaleUpperCase() + event.type.substring(1); + event.specials.forEach((s) => (event_name += s[0].toLocaleUpperCase() + s.substring(1))); + + const emit = (...args) => { + const emitter = () => + window.socket?.emit("event", { + id: id, + client_id: window.clientId, + listener_id: event.listener_id, + args: stringifyEventArgs(args, event.args), + }); + const delayed_emitter = () => { + if (window.did_handshake) emitter(); + else setTimeout(delayed_emitter, 10); + }; + throttle(delayed_emitter, event.throttle, event.leading_events, event.trailing_events, event.listener_id); + if (element.props["loopback"] === False && event.type == "update:modelValue") { + element.props["model-value"] = args; + } + }; + + let handler; + if (event.js_handler) { + const props = propsContext; // make `props` accessible from inside the event handler + handler = eval(event.js_handler); + } else { + handler = emit; + } + + handler = Vue.withModifiers(handler, event.modifiers); + handler = event.keys.length ? Vue.withKeys(handler, event.keys) : handler; + if (props[event_name]) { + props[event_name].push(handler); + } else { + props[event_name] = [handler]; + } + }); + const slots = {}; + const element_slots = { + default: { ids: element.children || [] }, + ...element.slots, + }; + Object.entries(element_slots).forEach(([name, data]) => { + slots[name] = (props) => { + const rendered = []; + if (data.template) { + rendered.push( + Vue.h( + { + props: { props: { type: Object, default: {} } }, + template: data.template, + }, + { + props: props, + } + ) + ); + } + const children = data.ids.map((id) => renderRecursively(elements, id, props || propsContext)); + if (name === "default" && element.text !== null) { + children.unshift(element.text); + } + return [...rendered, ...children]; + }; + }); + return Vue.h(app.config.isNativeTag(element.tag) ? element.tag : Vue.resolveComponent(element.tag), props, slots); +} + +function runJavascript(code, request_id) { + new Promise((resolve) => resolve(eval(code))) + .catch((reason) => { + if (reason instanceof SyntaxError) return eval(`(async() => {${code}})()`); + else throw reason; + }) + .then((result) => { + if (request_id) { + window.socket.emit("javascript_response", { request_id, client_id: window.clientId, result }); + } + }); +} + +function download(src, filename, mediaType, prefix) { + const anchor = document.createElement("a"); + if (typeof src === "string") { + anchor.href = src.startsWith("/") ? prefix + src : src; + } else { + anchor.href = URL.createObjectURL(new Blob([src], { type: mediaType })); + } + anchor.target = "_blank"; + anchor.download = filename || ""; + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); + if (typeof src !== "string") { + URL.revokeObjectURL(anchor.href); + } +} + +function ack() { + if (!window.socket || !window.did_handshake) return; + if (window.ackedMessageId >= window.nextMessageId) return; + window.socket.emit("ack", { + client_id: window.clientId, + next_message_id: window.nextMessageId, + }); + window.ackedMessageId = window.nextMessageId; +} + +function createRandomUUID() { + try { + return crypto.randomUUID(); + } catch (e) { + // https://stackoverflow.com/a/2117523/3419103 + return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) => + (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16) + ); + } +} + +const OLD_TAB_ID = sessionStorage.__nicegui_tab_closed === "false" ? sessionStorage.__nicegui_tab_id : null; +const TAB_ID = + !sessionStorage.__nicegui_tab_id || sessionStorage.__nicegui_tab_closed === "false" + ? (sessionStorage.__nicegui_tab_id = createRandomUUID()) + : sessionStorage.__nicegui_tab_id; +sessionStorage.__nicegui_tab_closed = "false"; +window.onbeforeunload = function () { + sessionStorage.__nicegui_tab_closed = "true"; +}; + +function createApp(elements, options) { + Object.entries(elements).forEach(([_, element]) => replaceUndefinedAttributes(element)); + setInterval(() => ack(), 3000); + return (app = Vue.createApp({ + data() { + return { + elements, + }; + }, + render() { + return renderRecursively(this.elements, 0); + }, + mounted() { + mounted_app = this; + window.documentId = createRandomUUID(); + window.clientId = options.query.client_id; + const url = window.location.protocol === "https:" ? "wss://" : "ws://" + window.location.host; + window.path_prefix = options.prefix; + window.nextMessageId = options.query.next_message_id; + window.ackedMessageId = -1; + window.socket = io(url, { + path: `${options.prefix}/_nicegui_ws/socket.io`, + query: options.query, + extraHeaders: options.extraHeaders, + transports: + "prerendering" in document && document.prerendering === true + ? ["polling", ...options.transports] + : options.transports, + }); + window.did_handshake = false; + const messageHandlers = { + connect: () => { + function wrapFunction(originalFunction) { + const MAX_WEBSOCKET_MESSAGE_SIZE = 1000000 - 100; // 1MB without 100 bytes of slack for the message header + return function (...args) { + const msg = args[0]; + if (typeof msg === "string" && msg.length > MAX_WEBSOCKET_MESSAGE_SIZE) { + const errorMessage = `Payload size ${msg.length} exceeds the maximum allowed limit.`; + console.error(errorMessage); + args[0] = `42["log",{"client_id":"${window.clientId}","level":"error","message":"${errorMessage}"}]`; + if (window.tooLongMessageTimerId) clearTimeout(window.tooLongMessageTimerId); + const popup = document.getElementById("too_long_message_popup"); + popup.ariaHidden = false; + window.tooLongMessageTimerId = setTimeout(() => (popup.ariaHidden = true), 5000); + } + return originalFunction.call(this, ...args); + }; + } + const transport = window.socket.io.engine.transport; + if (transport?.ws?.send) transport.ws.send = wrapFunction(transport.ws.send); + if (transport?.doWrite) transport.doWrite = wrapFunction(transport.doWrite); + + const args = { + client_id: window.clientId, + document_id: window.documentId, + tab_id: TAB_ID, + old_tab_id: OLD_TAB_ID, + next_message_id: window.nextMessageId, + }; + window.socket.emit("handshake", args, (ok) => { + if (!ok) { + console.log("reloading because handshake failed for clientId " + window.clientId); + window.location.reload(); + } + window.did_handshake = true; + document.getElementById("popup").ariaHidden = true; + }); + }, + connect_error: (err) => { + if (err.message == "timeout") { + console.log("reloading because connection timed out"); + window.location.reload(); // see https://github.com/zauberzeug/nicegui/issues/198 + } + }, + try_reconnect: async () => { + document.getElementById("popup").ariaHidden = false; + await fetch(window.location.href, { headers: { "NiceGUI-Check": "try_reconnect" } }); + console.log("reloading because reconnect was requested"); + window.location.reload(); + }, + disconnect: () => { + document.getElementById("popup").ariaHidden = false; + }, + load_js_components: async (msg) => { + const urls = msg.components.map((c) => `${options.prefix}/_nicegui/${options.version}/components/${c.key}`); + const imports = await Promise.all(urls.map((url) => import(url))); + msg.components.forEach((c, i) => app.component(c.tag, imports[i].default)); + }, + update: async (msg) => { + let eventListenersChanged = false; + for (const [id, element] of Object.entries(msg)) { + if (element === null) continue; + if (!(id in this.elements)) continue; + const oldListenerIds = new Set((this.elements[id]?.events || []).map((ev) => ev.listener_id)); + if (element.events?.some((e) => !oldListenerIds.has(e.listener_id))) { + delete this.elements[id]; + eventListenersChanged = true; + } + } + if (eventListenersChanged) { + logAndEmit("warning", "Event listeners changed after initial definition. Re-rendering affected elements."); + await this.$nextTick(); + } + + for (const [id, element] of Object.entries(msg)) { + if (element === null) { + delete this.elements[id]; + continue; + } + replaceUndefinedAttributes(element); + this.elements[id] = element; + } + + await this.$nextTick(); + for (const [id, element] of Object.entries(msg)) { + if (element?.update_method) { + getElement(id)?.[element.update_method](); + } + } + }, + run_javascript: (msg) => runJavascript(msg.code, msg.request_id), + open: (msg) => { + const url = msg.path.startsWith("/") ? options.prefix + msg.path : msg.path; + const target = msg.new_tab ? "_blank" : "_self"; + window.open(url, target); + }, + download: (msg) => download(msg.src, msg.filename, msg.media_type, options.prefix), + notify: (msg) => Quasar.Notify.create(msg), + }; + const socketMessageQueue = []; + let isProcessingSocketMessage = false; + for (const [event, handler] of Object.entries(messageHandlers)) { + window.socket.on(event, async (...args) => { + if (args.length > 0 && args[0]._id !== undefined) { + const message_id = args[0]._id; + if (message_id < window.nextMessageId) return; + window.nextMessageId = message_id + 1; + delete args[0]._id; + } + socketMessageQueue.push(() => handler(...args)); + if (!isProcessingSocketMessage) { + while (socketMessageQueue.length > 0) { + const handler = socketMessageQueue.shift(); + isProcessingSocketMessage = true; + try { + await handler(); + } catch (e) { + console.error(e); + } + isProcessingSocketMessage = false; + } + } + }); + } + }, + })); +} + +// HACK: remove Quasar's rules for divs in QCard (#2265, #2301) +for (const importRule of document.styleSheets[0].cssRules) { + if (importRule instanceof CSSImportRule && /quasar/.test(importRule.styleSheet?.href)) { + for (const rule of Array.from(importRule.styleSheet.cssRules)) { + if (rule instanceof CSSStyleRule && /\.q-card > div/.test(rule.selectorText)) { + if (/\.q-card > div/.test(rule.selectorText)) rule.selectorText = ".nicegui-card-tight" + rule.selectorText; + } + } + } +} diff --git a/nicegui/static/package-lock.json b/nicegui/static/package-lock.json new file mode 100644 index 0000000000..b551401cfc --- /dev/null +++ b/nicegui/static/package-lock.json @@ -0,0 +1,793 @@ +{ + "name": "nicegui-static", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nicegui-static", + "devDependencies": { + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-terser": "^0.4.4", + "rollup": "^4.52.3" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/smob": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", + "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + } + } +} diff --git a/nicegui/static/package.json b/nicegui/static/package.json new file mode 100644 index 0000000000..371dad1bd8 --- /dev/null +++ b/nicegui/static/package.json @@ -0,0 +1,14 @@ +{ + "name": "nicegui-static", + "private": true, + "scripts": { + "build": "rollup -c rollup.config.mjs", + "watch": "rollup -c rollup.config.mjs --watch", + "clean": "rm -f nicegui.js" + }, + "devDependencies": { + "rollup": "^4.52.3", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-terser": "^0.4.4" + } +} diff --git a/nicegui/static/rollup.config.mjs b/nicegui/static/rollup.config.mjs new file mode 100644 index 0000000000..22bc764301 --- /dev/null +++ b/nicegui/static/rollup.config.mjs @@ -0,0 +1,33 @@ +import nodeResolve from "@rollup/plugin-node-resolve"; +import terser from "@rollup/plugin-terser"; + +export default { + input: "./src/index.js", + output: { + file: "./nicegui.js", + format: "iife", + name: "NiceGUI", + sourcemap: false, + globals: { + "vue": "Vue", + "quasar": "Quasar", + "socket.io": "io" + } + }, + external: ["vue", "quasar", "socket.io"], + plugins: [ + nodeResolve(), + terser({ + mangle: false, + compress: { + defaults: false, + unused: true, + }, + format: { + comments: "some", + beautify: true, + indent_level: 2, + } + }), + ], +}; diff --git a/nicegui/static/src/app.js b/nicegui/static/src/app.js new file mode 100644 index 0000000000..49583ab575 --- /dev/null +++ b/nicegui/static/src/app.js @@ -0,0 +1,170 @@ +import { replaceUndefinedAttributes, setMountedApp, getElement } from "./elements.js"; +import { renderRecursively } from "./render.js"; +import { ack, createRandomUUID, TAB_ID, OLD_TAB_ID } from "./utils.js"; +import { logAndEmit } from "./utils.js"; +import { runJavascript, download } from "./utils.js"; + +let app = undefined; + +// Export getter for app so render.js can access it +export function getApp() { + return app; +} + +export function createApp(elements, options) { + Object.entries(elements).forEach(([_, element]) => replaceUndefinedAttributes(element)); + setInterval(() => ack(), 3000); + return (app = Vue.createApp({ + data() { + return { + elements, + }; + }, + render() { + return renderRecursively(this.elements, 0); + }, + mounted() { + setMountedApp(this); + window.documentId = createRandomUUID(); + window.clientId = options.query.client_id; + const url = window.location.protocol === "https:" ? "wss://" : "ws://" + window.location.host; + window.path_prefix = options.prefix; + window.nextMessageId = options.query.next_message_id; + window.ackedMessageId = -1; + window.socket = io(url, { + path: `${options.prefix}/_nicegui_ws/socket.io`, + query: options.query, + extraHeaders: options.extraHeaders, + transports: + "prerendering" in document && document.prerendering === true + ? ["polling", ...options.transports] + : options.transports, + }); + window.did_handshake = false; + const messageHandlers = { + connect: () => { + function wrapFunction(originalFunction) { + const MAX_WEBSOCKET_MESSAGE_SIZE = 1000000 - 100; // 1MB without 100 bytes of slack for the message header + return function (...args) { + const msg = args[0]; + if (typeof msg === "string" && msg.length > MAX_WEBSOCKET_MESSAGE_SIZE) { + const errorMessage = `Payload size ${msg.length} exceeds the maximum allowed limit.`; + console.error(errorMessage); + args[0] = `42["log",{"client_id":"${window.clientId}","level":"error","message":"${errorMessage}"}]`; + if (window.tooLongMessageTimerId) clearTimeout(window.tooLongMessageTimerId); + const popup = document.getElementById("too_long_message_popup"); + popup.ariaHidden = false; + window.tooLongMessageTimerId = setTimeout(() => (popup.ariaHidden = true), 5000); + } + return originalFunction.call(this, ...args); + }; + } + const transport = window.socket.io.engine.transport; + if (transport?.ws?.send) transport.ws.send = wrapFunction(transport.ws.send); + if (transport?.doWrite) transport.doWrite = wrapFunction(transport.doWrite); + + const args = { + client_id: window.clientId, + document_id: window.documentId, + tab_id: TAB_ID, + old_tab_id: OLD_TAB_ID, + next_message_id: window.nextMessageId, + }; + window.socket.emit("handshake", args, (ok) => { + if (!ok) { + console.log("reloading because handshake failed for clientId " + window.clientId); + window.location.reload(); + } + window.did_handshake = true; + document.getElementById("popup").ariaHidden = true; + }); + }, + connect_error: (err) => { + if (err.message == "timeout") { + console.log("reloading because connection timed out"); + window.location.reload(); // see https://github.com/zauberzeug/nicegui/issues/198 + } + }, + try_reconnect: async () => { + document.getElementById("popup").ariaHidden = false; + await fetch(window.location.href, { headers: { "NiceGUI-Check": "try_reconnect" } }); + console.log("reloading because reconnect was requested"); + window.location.reload(); + }, + disconnect: () => { + document.getElementById("popup").ariaHidden = false; + }, + load_js_components: async (msg) => { + const urls = msg.components.map((c) => `${options.prefix}/_nicegui/${options.version}/components/${c.key}`); + const imports = await Promise.all(urls.map((url) => import(url))); + msg.components.forEach((c, i) => app.component(c.tag, imports[i].default)); + }, + update: async (msg) => { + let eventListenersChanged = false; + for (const [id, element] of Object.entries(msg)) { + if (element === null) continue; + if (!(id in this.elements)) continue; + const oldListenerIds = new Set((this.elements[id]?.events || []).map((ev) => ev.listener_id)); + if (element.events?.some((e) => !oldListenerIds.has(e.listener_id))) { + delete this.elements[id]; + eventListenersChanged = true; + } + } + if (eventListenersChanged) { + logAndEmit("warning", "Event listeners changed after initial definition. Re-rendering affected elements."); + await this.$nextTick(); + } + + for (const [id, element] of Object.entries(msg)) { + if (element === null) { + delete this.elements[id]; + continue; + } + replaceUndefinedAttributes(element); + this.elements[id] = element; + } + + await this.$nextTick(); + for (const [id, element] of Object.entries(msg)) { + if (element?.update_method) { + getElement(id)?.[element.update_method](); + } + } + }, + run_javascript: (msg) => runJavascript(msg.code, msg.request_id), + open: (msg) => { + const url = msg.path.startsWith("/") ? options.prefix + msg.path : msg.path; + const target = msg.new_tab ? "_blank" : "_self"; + window.open(url, target); + }, + download: (msg) => download(msg.src, msg.filename, msg.media_type, options.prefix), + notify: (msg) => Quasar.Notify.create(msg), + }; + const socketMessageQueue = []; + let isProcessingSocketMessage = false; + for (const [event, handler] of Object.entries(messageHandlers)) { + window.socket.on(event, async (...args) => { + if (args.length > 0 && args[0]._id !== undefined) { + const message_id = args[0]._id; + if (message_id < window.nextMessageId) return; + window.nextMessageId = message_id + 1; + delete args[0]._id; + } + socketMessageQueue.push(() => handler(...args)); + if (!isProcessingSocketMessage) { + while (socketMessageQueue.length > 0) { + const handler = socketMessageQueue.shift(); + isProcessingSocketMessage = true; + try { + await handler(); + } catch (e) { + console.error(e); + } + isProcessingSocketMessage = false; + } + } + }); + } + }, + })); +} diff --git a/nicegui/static/src/colors.js b/nicegui/static/src/colors.js new file mode 100644 index 0000000000..7086a611d1 --- /dev/null +++ b/nicegui/static/src/colors.js @@ -0,0 +1,19 @@ +export function applyColors(colors) { + const quasarColors = ["primary", "secondary", "accent", "dark", "dark-page", "positive", "negative", "info", "warning"]; + let customCSS = ""; + for (let color in colors) { + if (quasarColors.includes(color)) + continue; + const colorName = color.replaceAll("_", "-"); + const colorVar = "--q-" + colorName; + document.body.style.setProperty(colorVar, colors[color]); + customCSS += `.text-${colorName} { color: var(${colorVar}) !important; }\n`; + customCSS += `.bg-${colorName} { background-color: var(${colorVar}) !important; }\n`; + } + if (!customCSS) return; + const style = document.createElement("style"); + style.innerHTML = customCSS; + style.dataset.niceguiCustomColors = ""; + document.head.querySelectorAll("[data-nicegui-custom-colors]").forEach((el) => el.remove()); + document.getElementsByTagName("head")[0].appendChild(style); +} diff --git a/nicegui/static/src/constants.js b/nicegui/static/src/constants.js new file mode 100644 index 0000000000..d35b056698 --- /dev/null +++ b/nicegui/static/src/constants.js @@ -0,0 +1,3 @@ +export const True = true; +export const False = false; +export const None = undefined; diff --git a/nicegui/static/src/elements.js b/nicegui/static/src/elements.js new file mode 100644 index 0000000000..ef1bc43278 --- /dev/null +++ b/nicegui/static/src/elements.js @@ -0,0 +1,37 @@ +import { None } from "./constants.js"; + +let mounted_app = undefined; + +export function setMountedApp(app) { + mounted_app = app; +} + +export function getMountedApp() { + return mounted_app; +} + +export function replaceUndefinedAttributes(element) { + element.class ??= []; + element.style ??= {}; + element.props ??= {}; + element.text ??= null; + element.events ??= []; + element.update_method ??= null; + element.slots = { + default: { ids: element.children || [] }, + ...(element.slots ?? {}), + }; +} + +export function getElement(id) { + const _id = id instanceof Element ? id.id.slice(1) : id; + return mounted_app.$refs["r" + _id]; +} + +export function getHtmlElement(id) { + let id_as_a_string = id.toString(); + if (!id_as_a_string.startsWith("c")) { + id_as_a_string = "c" + id_as_a_string; + } + return document.getElementById(id_as_a_string); +} diff --git a/nicegui/static/src/events.js b/nicegui/static/src/events.js new file mode 100644 index 0000000000..af5c4cfd05 --- /dev/null +++ b/nicegui/static/src/events.js @@ -0,0 +1,59 @@ +export function stringifyEventArgs(args, event_args) { + const result = []; + args.forEach((arg, i) => { + if (event_args !== null && i >= event_args.length) return; + let filtered = {}; + if (typeof arg !== "object" || arg === null || Array.isArray(arg)) { + filtered = arg; + } else { + for (let k in arg) { + // ignore "Restricted" fields in Firefox (see #2469) + if (k == "originalTarget") { + try { + arg[k].toString(); + } catch (e) { + continue; + } + } + if (event_args === null || event_args[i] === null || event_args[i].includes(k)) { + filtered[k] = arg[k]; + } + } + } + result.push(JSON.stringify(filtered, (k, v) => (v instanceof Node || v instanceof Window ? undefined : v))); + }); + return result; +} + +const waitingCallbacks = new Map(); + +export function throttle(callback, time, leading, trailing, id) { + if (time <= 0) { + // execute callback immediately and return + callback(); + return; + } + if (waitingCallbacks.has(id)) { + if (trailing) { + // update trailing callback + waitingCallbacks.set(id, callback); + } + } else { + if (leading) { + // execute leading callback and set timeout to block more leading callbacks + callback(); + waitingCallbacks.set(id, null); + } else if (trailing) { + // set trailing callback and set timeout to execute it + waitingCallbacks.set(id, callback); + } + if (leading || trailing) { + // set timeout to remove block and to execute trailing callback + setTimeout(() => { + const trailingCallback = waitingCallbacks.get(id); + if (trailingCallback) trailingCallback(); + waitingCallbacks.delete(id); + }, 1000 * time); + } + } +} diff --git a/nicegui/static/src/index.js b/nicegui/static/src/index.js new file mode 100644 index 0000000000..d5d0afb2d6 --- /dev/null +++ b/nicegui/static/src/index.js @@ -0,0 +1,67 @@ +// Import all modules +import { True, False, None } from "./constants.js"; +import { applyColors } from "./colors.js"; +import { getElement, getHtmlElement, replaceUndefinedAttributes, getMountedApp } from "./elements.js"; +import { parseElements, runMethod, getComputedProp, emitEvent, logAndEmit, runJavascript, download, ack, TAB_ID, OLD_TAB_ID } from "./utils.js"; +import { createApp, getApp } from "./app.js"; +import "./quasar-hack.js"; + +// Make all functions globally available for backwards compatibility +// Python code uses run_javascript to call these functions +if (typeof window !== "undefined") { + window.True = True; + window.False = False; + window.None = None; + window.getElement = getElement; + window.getHtmlElement = getHtmlElement; + window.runMethod = runMethod; + window.getComputedProp = getComputedProp; + window.emitEvent = emitEvent; + window.logAndEmit = logAndEmit; + window.runJavascript = runJavascript; + window.download = download; + window.ack = ack; + window.parseElements = parseElements; + window.createApp = createApp; + window.applyColors = applyColors; + window.TAB_ID = TAB_ID; + window.OLD_TAB_ID = OLD_TAB_ID; + + // Expose mounted_app via getter for backwards compatibility + Object.defineProperty(window, "mounted_app", { + get: getMountedApp, + enumerable: true, + configurable: true + }); + + // Expose app via getter for backwards compatibility + Object.defineProperty(window, "app", { + get: getApp, + enumerable: true, + configurable: true + }); +} + +// Re-export everything for potential ES module usage +export { + True, + False, + None, + applyColors, + getElement, + getHtmlElement, + replaceUndefinedAttributes, + parseElements, + runMethod, + getComputedProp, + emitEvent, + logAndEmit, + runJavascript, + download, + ack, + createApp, + getApp, + getMountedApp, + TAB_ID, + OLD_TAB_ID, +}; diff --git a/nicegui/static/src/quasar-hack.js b/nicegui/static/src/quasar-hack.js new file mode 100644 index 0000000000..12e06c2004 --- /dev/null +++ b/nicegui/static/src/quasar-hack.js @@ -0,0 +1,10 @@ +// HACK: remove Quasar's rules for divs in QCard (#2265, #2301) +for (const importRule of document.styleSheets[0].cssRules) { + if (importRule instanceof CSSImportRule && /quasar/.test(importRule.styleSheet?.href)) { + for (const rule of Array.from(importRule.styleSheet.cssRules)) { + if (rule instanceof CSSStyleRule && /\.q-card > div/.test(rule.selectorText)) { + if (/\.q-card > div/.test(rule.selectorText)) rule.selectorText = ".nicegui-card-tight" + rule.selectorText; + } + } + } +} diff --git a/nicegui/static/src/render.js b/nicegui/static/src/render.js new file mode 100644 index 0000000000..560074411f --- /dev/null +++ b/nicegui/static/src/render.js @@ -0,0 +1,102 @@ +import { True, False, None } from "./constants.js"; +import { stringifyEventArgs, throttle } from "./events.js"; +import { getElement } from "./elements.js"; +import { getApp } from "./app.js"; + +export function renderRecursively(elements, id, propsContext) { + const element = elements[id]; + if (element === undefined) { + return; + } + + const props = { + id: "c" + id, + ref: "r" + id, + key: id, // HACK: workaround for #600 and #898 + class: element.class.join(" ") || undefined, + style: Object.entries(element.style).reduce((str, [p, val]) => `${str}${p}:${val};`, "") || undefined, + ...element.props, + }; + Object.entries(props).forEach(([key, value]) => { + if (key.startsWith(":")) { + try { + try { + props[key.substring(1)] = new Function("props", `return (${value})`)(propsContext); + } catch (e) { + props[key.substring(1)] = eval(value); + } + delete props[key]; + } catch (e) { + console.error(`Error while converting ${key} attribute to function:`, e); + } + } + }); + element.events.forEach((event) => { + let event_name = "on" + event.type[0].toLocaleUpperCase() + event.type.substring(1); + event.specials.forEach((s) => (event_name += s[0].toLocaleUpperCase() + s.substring(1))); + + const emit = (...args) => { + const emitter = () => + window.socket?.emit("event", { + id: id, + client_id: window.clientId, + listener_id: event.listener_id, + args: stringifyEventArgs(args, event.args), + }); + const delayed_emitter = () => { + if (window.did_handshake) emitter(); + else setTimeout(delayed_emitter, 10); + }; + throttle(delayed_emitter, event.throttle, event.leading_events, event.trailing_events, event.listener_id); + if (element.props["loopback"] === False && event.type == "update:modelValue") { + element.props["model-value"] = args; + } + }; + + let handler; + if (event.js_handler) { + const props = propsContext; // make `props` accessible from inside the event handler + handler = eval(event.js_handler); + } else { + handler = emit; + } + + handler = Vue.withModifiers(handler, event.modifiers); + handler = event.keys.length ? Vue.withKeys(handler, event.keys) : handler; + if (props[event_name]) { + props[event_name].push(handler); + } else { + props[event_name] = [handler]; + } + }); + const slots = {}; + const element_slots = { + default: { ids: element.children || [] }, + ...element.slots, + }; + Object.entries(element_slots).forEach(([name, data]) => { + slots[name] = (props) => { + const rendered = []; + if (data.template) { + rendered.push( + Vue.h( + { + props: { props: { type: Object, default: {} } }, + template: data.template, + }, + { + props: props, + } + ) + ); + } + const children = data.ids.map((id) => renderRecursively(elements, id, props || propsContext)); + if (name === "default" && element.text !== null) { + children.unshift(element.text); + } + return [...rendered, ...children]; + }; + }); + const app = getApp(); + return Vue.h(app.config.isNativeTag(element.tag) ? element.tag : Vue.resolveComponent(element.tag), props, slots); +} diff --git a/nicegui/static/src/utils.js b/nicegui/static/src/utils.js new file mode 100644 index 0000000000..e38a7b088b --- /dev/null +++ b/nicegui/static/src/utils.js @@ -0,0 +1,120 @@ +import { getElement } from "./elements.js"; + +export function parseElements(raw_elements) { + return JSON.parse( + raw_elements + .replace(/$/g, "$") + .replace(/`/g, "`") + .replace(/>/g, ">") + .replace(/</g, "<") + .replace(/&/g, "&") + ); +} + +export function runMethod(target, method_name, args) { + if (typeof target === "object") { + if (method_name in target) { + return target[method_name](...args); + } else { + return eval(method_name)(target, ...args); + } + } + const element = getElement(target); + if (element === null || element === undefined) return; + if (method_name in element) { + return element[method_name](...args); + } else if (method_name in (element.$refs.qRef || [])) { + return element.$refs.qRef[method_name](...args); + } else { + return eval(method_name)(element, ...args); + } +} + +export function getComputedProp(target, prop_name) { + if (typeof target === "object" && prop_name in target) { + return target[prop_name]; + } + const element = getElement(target); + if (element === null || element === undefined) return; + if (prop_name in element) { + return element[prop_name]; + } else if (prop_name in (element.$refs.qRef || [])) { + return element.$refs.qRef[prop_name]; + } +} + +export function emitEvent(event_name, ...args) { + getElement(0).$emit(event_name, ...args); +} + +export function logAndEmit(level, message) { + if (level === "error") { + console.error(message); + } else if (level === "warning") { + console.warn(message); + } else { + console.log(message); + } + window.socket.emit("log", { client_id: window.clientId, level, message }); +} + +export function runJavascript(code, request_id) { + new Promise((resolve) => resolve(eval(code))) + .catch((reason) => { + if (reason instanceof SyntaxError) return eval(`(async() => {${code}})()`); + else throw reason; + }) + .then((result) => { + if (request_id) { + window.socket.emit("javascript_response", { request_id, client_id: window.clientId, result }); + } + }); +} + +export function download(src, filename, mediaType, prefix) { + const anchor = document.createElement("a"); + if (typeof src === "string") { + anchor.href = src.startsWith("/") ? prefix + src : src; + } else { + anchor.href = URL.createObjectURL(new Blob([src], { type: mediaType })); + } + anchor.target = "_blank"; + anchor.download = filename || ""; + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); + if (typeof src !== "string") { + URL.revokeObjectURL(anchor.href); + } +} + +export function ack() { + if (!window.socket || !window.did_handshake) return; + if (window.ackedMessageId >= window.nextMessageId) return; + window.socket.emit("ack", { + client_id: window.clientId, + next_message_id: window.nextMessageId, + }); + window.ackedMessageId = window.nextMessageId; +} + +export function createRandomUUID() { + try { + return crypto.randomUUID(); + } catch (e) { + // https://stackoverflow.com/a/2117523/3419103 + return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) => + (+c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16) + ); + } +} + +export const OLD_TAB_ID = sessionStorage.__nicegui_tab_closed === "false" ? sessionStorage.__nicegui_tab_id : null; +export const TAB_ID = + !sessionStorage.__nicegui_tab_id || sessionStorage.__nicegui_tab_closed === "false" + ? (sessionStorage.__nicegui_tab_id = createRandomUUID()) + : sessionStorage.__nicegui_tab_id; +sessionStorage.__nicegui_tab_closed = "false"; +window.onbeforeunload = function () { + sessionStorage.__nicegui_tab_closed = "true"; +};