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
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(/`/g, '`') |
|
.replace(/>/g, '>') |
|
.replace(/</g, '<') |
|
.replace(/&/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>
|
|
|