Skip to content

Instantly share code, notes, and snippets.

@evnchn
Created March 13, 2025 02:09
Show Gist options
  • Select an option

  • Save evnchn/74769527c4a23913804b63e69ba4e7c0 to your computer and use it in GitHub Desktop.

Select an option

Save evnchn/74769527c4a23913804b63e69ba4e7c0 to your computer and use it in GitHub Desktop.
const True = true;
const False = false;
const None = undefined;
let app = undefined;
let mounted_app = undefined;
const loaded_libraries = new Set();
const loaded_components = new Set();
function parseElements(raw_elements) {
return JSON.parse(
raw_elements
.replace(/$/g, "$")
.replace(/`/g, "`")
.replace(/>/g, ">")
.replace(/&lt;/g, "<")
.replace(/&amp;/g, "&")
);
}
function replaceUndefinedAttributes(elements, id) {
const element = elements[id];
if (element === undefined) {
return;
}
element.class ??= [];
element.style ??= {};
element.props ??= {};
element.text ??= null;
element.events ??= [];
element.component ??= null;
element.libraries ??= [];
element.slots = {
default: { ids: element.children || [] },
...(element.slots ?? {}),
};
Object.values(element.slots).forEach((slot) => slot.ids.forEach((id) => replaceUndefinedAttributes(elements, id)));
}
function getElement(id) {
const _id = id instanceof HTMLElement ? id.id : id;
return mounted_app.$refs["r" + _id];
}
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 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) {
const element = elements[id];
if (element === undefined) {
return;
}
// @todo: Try avoid this with better handling of initial page load.
if (element.component) loaded_components.add(element.component.name);
element.libraries.forEach((library) => loaded_libraries.add(library.name));
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(`return (${value})`)();
} 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)));
let handler;
if (event.js_handler) {
handler = eval(event.js_handler);
} else {
handler = (...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(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;
}
};
}
console.log("Before the Vue part, the handler for event", event_name, "is", handler.toString());
console.log("Event modifiers:", event.modifiers, JSON.stringify(event.modifiers));
console.log("Event keys:", event.keys, JSON.stringify(event.keys));
handler = Vue.withModifiers(handler, event.modifiers);
handler = event.keys.length ? Vue.withKeys(handler, event.keys) : handler;
console.log("After the Vue part, the handler for event", event_name, "is", handler.toString());
console.log("=====");
if (props[event_name]) {
props[event_name].push(handler);
console.log("It is pushed to the existing array of handlers for this event", props[event_name]);
} else {
props[event_name] = [handler];
console.log("It is set as the only handler for this event", props[event_name]);
}
});
if (id==5) {console.log("By the time we are done, props are", props, JSON.stringify(props));
console.log(props["onKeydown"].toString());
}
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));
if (name === "default" && element.text !== null) {
children.unshift(element.text);
}
return [...rendered, ...children];
};
});
return Vue.h(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);
}
}
async function loadDependencies(element, prefix, version) {
if (element.component) {
const { name, key, tag } = element.component;
if (!loaded_components.has(name) && !key.endsWith(".vue")) {
const component = await import(`${prefix}/_nicegui/${version}/components/${key}`);
app.component(tag, component.default);
loaded_components.add(name);
}
}
if (element.libraries) {
for (const { name, key } of element.libraries) {
if (loaded_libraries.has(name)) continue;
await import(`${prefix}/_nicegui/${version}/libraries/${key}`);
loaded_libraries.add(name);
}
}
}
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)
);
}
}
function createApp(elements, options) {
replaceUndefinedAttributes(elements, 0);
return (app = Vue.createApp({
data() {
return {
elements,
};
},
render() {
return renderRecursively(this.elements, 0);
},
mounted() {
mounted_app = this;
window.clientId = options.query.client_id;
const url = window.location.protocol === "https:" ? "wss://" : "ws://" + window.location.host;
window.path_prefix = options.prefix;
window.socket = io(url, {
path: `${options.prefix}/_nicegui_ws/socket.io`,
query: options.query,
extraHeaders: options.extraHeaders,
transports: options.transports,
});
window.did_handshake = false;
const messageHandlers = {
connect: () => {
let tabId = sessionStorage.getItem("__nicegui_tab_id");
if (!tabId) {
tabId = createRandomUUID();
sessionStorage.setItem("__nicegui_tab_id", tabId);
}
window.socket.emit("handshake", { client_id: window.clientId, tab_id: tabId }, (ok) => {
if (!ok) {
console.log("reloading because handshake failed for clientId " + window.clientId);
window.location.reload();
}
document.getElementById("popup").ariaHidden = true;
});
window.did_handshake = 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;
},
update: async (msg) => {
for (const [id, element] of Object.entries(msg)) {
if (element === null) {
delete this.elements[id];
continue;
}
if (element.component || element.libraries) {
await loadDependencies(element, options.prefix, options.version);
}
this.elements[id] = element;
replaceUndefinedAttributes(this.elements, id);
}
},
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) => {
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;
}
}
});
}
},
}).use(Quasar, {
config: options.quasarConfig,
}));
}
// HACK: remove Quasar's rules for divs in QCard (#2265, #2301)
for (let sheet of document.styleSheets) {
if (/\/quasar(?:\.prod)?\.css$/.test(sheet.href)) {
for (let rule of sheet.cssRules) {
if (/\.q-card > div/.test(rule.selectorText)) rule.selectorText = ".nicegui-card-tight" + rule.selectorText;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment