You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

335 lines
13 KiB

<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
<meta name="viewport" content="{{ viewport }}" />
<link href="{{ favicon_url }}" rel="shortcut icon" />
<link href="{{ prefix | safe }}/_nicegui/{{version}}/static/nicegui.css" rel="stylesheet" type="text/css" />
<link href="{{ prefix | safe }}/_nicegui/{{version}}/static/fonts.css" rel="stylesheet" type="text/css" />
{% if prod_js %}
<link href="{{ prefix | safe }}/_nicegui/{{version}}/static/quasar.prod.css" rel="stylesheet" type="text/css" />
{% else %}
<link href="{{ prefix | safe }}/_nicegui/{{version}}/static/quasar.css" rel="stylesheet" type="text/css" />
{% endif %}
<!-- prevent Prettier from removing this line -->
{{ head_html | safe }}
</head>
<body>
<script src="{{ prefix | safe }}/_nicegui/{{version}}/static/es-module-shims.js"></script>
<script src="{{ prefix | safe }}/_nicegui/{{version}}/static/socket.io.min.js"></script>
{% if tailwind %}
<script src="{{ prefix | safe }}/_nicegui/{{version}}/static/tailwindcss.min.js"></script>
{% endif %}
<!-- prevent Prettier from removing this line -->
{% if prod_js %}
<script src="{{ prefix | safe }}/_nicegui/{{version}}/static/vue.global.prod.js"></script>
<script src="{{ prefix | safe }}/_nicegui/{{version}}/static/quasar.umd.prod.js"></script>
{% else %}
<script src="{{ prefix | safe }}/_nicegui/{{version}}/static/vue.global.js"></script>
<script src="{{ prefix | safe }}/_nicegui/{{version}}/static/quasar.umd.js"></script>
{% endif %}
<script src="{{ prefix | safe }}/_nicegui/{{version}}/static/lang/{{ language }}.umd.prod.js"></script>
<script type="importmap">
{"imports": {{ imports | safe }}}
</script>
{{ body_html | safe }}
<div id="app"></div>
<div id="popup">
<span>Connection lost.</span>
<span>Trying to reconnect...</span>
</div>
<script>
function getElement(id) {
const _id = id instanceof HTMLElement ? id.id : id;
return window.app.$refs["r" + _id];
}
</script>
<script type="module">
const True = true;
const False = false;
const None = undefined;
const loaded_libraries = new Set();
const loaded_components = new Set();
const raw_elements = String.raw`{{ elements | safe }}`;
const elements = JSON.parse(raw_elements.replace(/&#96;/g, '`')
.replace(/&gt;/g, '>')
.replace(/&lt;/g, '<')
.replace(/&amp;/g, '&'));
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 (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' + element.id,
ref: 'r' + element.id,
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(':')) {
props[key.substring(1)] = eval(value);
delete props[key];
}
});
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 = (...args) => {
const data = {
id: element.id,
listener_id: event.listener_id,
args: stringifyEventArgs(args, event.args),
};
const emitter = () => window.socket.emit("event", data);
throttle(emitter, event.throttle, event.leading_events, event.trailing_events, event.listener_id);
if (element.props["loopback"] === False && event.type == "update:model-value") {
element.props["model-value"] = args;
}
};
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 = {};
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, result});
}
});
}
function download(url, filename) {
const anchor = document.createElement("a");
anchor.href = url;
anchor.target = "_blank";
anchor.download = filename || "";
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
}
async function loadDependencies(element) {
if (element.component) {
const {name, key, tag} = element.component;
if (!loaded_components.has(name) && !key.endsWith('.vue')) {
const component = await import(`{{ prefix | safe }}/_nicegui/{{version}}/components/${key}`);
app = app.component(tag, component.default);
loaded_components.add(name);
}
}
for (const {name, key} of element.libraries) {
if (loaded_libraries.has(name)) continue;
await import(`{{ prefix | safe }}/_nicegui/{{version}}/libraries/${key}`);
loaded_libraries.add(name);
}
}
let app = Vue.createApp({
data() {
return {
elements,
};
},
render() {
return renderRecursively(this.elements, 0);
},
mounted() {
window.app = this;
const query = {{ socket_io_js_query_params | safe }};
const url = window.location.protocol === 'https:' ? 'wss://' : 'ws://' + window.location.host;
const extraHeaders = {{ socket_io_js_extra_headers | safe }};
const transports = {{ socket_io_js_transports | safe }};
window.path_prefix = "{{ prefix | safe }}";
window.socket = io(url, { path: "{{ prefix | safe }}/_nicegui_ws/socket.io", query, extraHeaders, transports });
const messageHandlers = {
connect: () => {
window.socket.emit("handshake", (ok) => {
if (!ok) {
console.log('reloading because handshake failed')
window.location.reload();
}
document.getElementById('popup').style.opacity = 0;
});
},
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: () => {
const checkAndReload = async () => {
await fetch(window.location.href, { headers: { 'NiceGUI-Check': 'try_reconnect' } });
console.log('reloading because reconnect was requested')
window.location.reload();
};
setInterval(checkAndReload, 500);
},
disconnect: () => {
document.getElementById('popup').style.opacity = 1;
},
update: async (msg) => {
for (const [id, element] of Object.entries(msg)) {
if (element === null) {
delete this.elements[id];
continue;
}
if (element.component || element.libraries.length > 0) {
await loadDependencies(element);
}
this.elements[element.id] = element;
}
},
run_method: (msg) => {
const element = getElement(msg.id);
if (element === null || element === undefined) return;
if (msg.name in element) {
element[msg.name](...msg.args);
} else {
element.$refs.qRef[msg.name](...msg.args);
}
},
run_javascript: (msg) => runJavascript(msg['code'], msg['request_id']),
open: (msg) => {
const url = msg.path.startsWith('/') ? "{{ prefix | safe }}" + msg.path : msg.path;
const target = msg.new_tab ? '_blank' : '_self';
window.open(url, target);
},
download: (msg) => download(msg.url, msg.filename),
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: {{ quasar_config | safe }}
});
{{ js_imports | safe }}
{{ vue_scripts | safe }}
const dark = {{ dark }};
Quasar.lang.set(Quasar.lang["{{ language }}".replace('-', '')]);
Quasar.Dark.set(dark === None ? "auto" : dark);
{% if tailwind %}
if (dark !== None) tailwind.config.darkMode = "class";
if (dark === True) document.body.classList.add("dark");
{% endif %}
app.mount("#app");
</script>
</body>
</html>