559 lines
19 KiB
JavaScript
559 lines
19 KiB
JavaScript
const True = true;
|
|
const False = false;
|
|
const None = undefined;
|
|
|
|
let app = undefined;
|
|
let mounted_app = undefined;
|
|
|
|
function initUnoCss() {
|
|
if (window.__unocss_runtime === undefined) return;
|
|
|
|
const renderedClasses = new Set();
|
|
let queue = Promise.resolve();
|
|
let isInitialized = false;
|
|
|
|
new MutationObserver((mutations) => {
|
|
let newClassesString = "";
|
|
function appendClass(className) {
|
|
if (renderedClasses.has(className)) return;
|
|
renderedClasses.add(className);
|
|
newClassesString += className + " ";
|
|
}
|
|
for (const mutation of mutations) {
|
|
if (mutation.type === "attributes") {
|
|
for (const className of mutation.target.classList) appendClass(className);
|
|
} else if (mutation.type === "childList") {
|
|
for (const node of mutation.addedNodes) {
|
|
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
|
for (const el of [node, ...node.querySelectorAll("*")]) {
|
|
for (const className of el.classList) appendClass(className);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (newClassesString.length === 0) return;
|
|
queue = queue.then(async () => {
|
|
await window.__unocss_runtime.extract(newClassesString);
|
|
if (isInitialized) return;
|
|
for (const style of document.querySelectorAll("style[data-unocss-runtime-layer]"))
|
|
document.head.appendChild(style);
|
|
document.getElementById("app").classList.remove("nicegui-unocss-loading");
|
|
isInitialized = true;
|
|
});
|
|
}).observe(document.body, { subtree: true, childList: true, attributes: true, attributeFilter: ["class"] });
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
let darkSetter = undefined;
|
|
|
|
function setDark(dark) {
|
|
if (dark === null) dark = None;
|
|
darkSetter?.(dark);
|
|
document
|
|
.getElementById("nicegui-color-scheme")
|
|
.setAttribute("content", dark === None ? "normal" : dark ? "dark" : "light");
|
|
}
|
|
|
|
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);
|
|
initUnoCss();
|
|
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;
|
|
options.query.document_id = window.documentId;
|
|
options.query.tab_id = TAB_ID;
|
|
options.query.old_tab_id = OLD_TAB_ID;
|
|
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);
|
|
|
|
function finishHandshake(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;
|
|
}
|
|
|
|
if (options.query.implicit_handshake) {
|
|
finishHandshake(true);
|
|
} else {
|
|
window.socket.emit("handshake", options.query, finishHandshake);
|
|
}
|
|
},
|
|
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
|
|
} else if (err.message == "Implicit handshake failed") {
|
|
console.log("reloading because implicit handshake failed for clientId " + window.clientId);
|
|
window.location.reload();
|
|
}
|
|
},
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|