import { htmlDecode, ensureUniqueId } from '../utils'; import type { Runtime } from '../runtime'; import { getLogger } from '../logger'; import { pyExec } from '../pyexec'; const logger = getLogger('py-script'); export function make_PyScript(runtime: Runtime) { class PyScript extends HTMLElement { async connectedCallback() { ensureUniqueId(this); const pySrc = await this.getPySrc(); this.innerHTML = ''; await pyExec(runtime, pySrc, this); } async getPySrc(): Promise { if (this.hasAttribute('src')) { // XXX: what happens if the fetch() fails? // We should handle the case correctly, but in my defense // this case was broken also before the refactoring. FIXME! const url = this.getAttribute('src'); const response = await fetch(url); return await response.text(); } else { return htmlDecode(this.innerHTML); } } } return PyScript; } /** Defines all possible py-on* and their corresponding event types */ const pyAttributeToEvent: Map = new Map([ // Leaving pys-onClick and pys-onKeyDown for backward compatibility ["pys-onClick", "click"], ["pys-onKeyDown", "keydown"], ["py-onClick", "click"], ["py-onKeyDown", "keydown"], // Window Events ["py-afterprint", "afterprint"], ["py-beforeprint", "beforeprint"], ["py-beforeunload", "beforeunload"], ["py-error", "error"], ["py-hashchange", "hashchange"], ["py-load", "load"], ["py-message", "message"], ["py-offline", "offline"], ["py-online", "online"], ["py-pagehide", "pagehide"], ["py-pageshow", "pageshow"], ["py-popstate", "popstate"], ["py-resize", "resize"], ["py-storage", "storage"], ["py-unload", "unload"], // Form Events ["py-blur", "blur"], ["py-change", "change"], ["py-contextmenu", "contextmenu"], ["py-focus", "focus"], ["py-input", "input"], ["py-invalid", "invalid"], ["py-reset", "reset"], ["py-search", "search"], ["py-select", "select"], ["py-submit", "submit"], // Keyboard Events ["py-keydown", "keydown"], ["py-keypress", "keypress"], ["py-keyup", "keyup"], // Mouse Events ["py-click", "click"], ["py-dblclick", "dblclick"], ["py-mousedown", "mousedown"], ["py-mousemove", "mousemove"], ["py-mouseout", "mouseout"], ["py-mouseover", "mouseover"], ["py-mouseup", "mouseup"], ["py-mousewheel", "mousewheel"], ["py-wheel", "wheel"], // Drag Events ["py-drag", "drag"], ["py-dragend", "dragend"], ["py-dragenter", "dragenter"], ["py-dragleave", "dragleave"], ["py-dragover", "dragover"], ["py-dragstart", "dragstart"], ["py-drop", "drop"], ["py-scroll", "scroll"], // Clipboard Events ["py-copy", "copy"], ["py-cut", "cut"], ["py-paste", "paste"], // Media Events ["py-abort", "abort"], ["py-canplay", "canplay"], ["py-canplaythrough", "canplaythrough"], ["py-cuechange", "cuechange"], ["py-durationchange", "durationchange"], ["py-emptied", "emptied"], ["py-ended", "ended"], ["py-loadeddata", "loadeddata"], ["py-loadedmetadata", "loadedmetadata"], ["py-loadstart", "loadstart"], ["py-pause", "pause"], ["py-play", "play"], ["py-playing", "playing"], ["py-progress", "progress"], ["py-ratechange", "ratechange"], ["py-seeked", "seeked"], ["py-seeking", "seeking"], ["py-stalled", "stalled"], ["py-suspend", "suspend"], ["py-timeupdate", "timeupdate"], ["py-volumechange", "volumechange"], ["py-waiting", "waiting"], // Misc Events ["py-toggle", "toggle"], ]); /** Initialize all elements with py-* handlers attributes */ export async function initHandlers(runtime: Runtime) { logger.debug('Initializing py-* event handlers...'); for (const pyAttribute of pyAttributeToEvent.keys()) { await createElementsWithEventListeners(runtime, pyAttribute); } } /** Initializes an element with the given py-on* attribute and its handler */ async function createElementsWithEventListeners(runtime: Runtime, pyAttribute: string): Promise { const matches: NodeListOf = document.querySelectorAll(`[${pyAttribute}]`); for (const el of matches) { if (el.id.length === 0) { throw new TypeError(`<${el.tagName.toLowerCase()}> must have an id attribute, when using the ${pyAttribute} attribute`) } const handlerCode = el.getAttribute(pyAttribute); const event = pyAttributeToEvent.get(pyAttribute); if (pyAttribute === 'pys-onClick' || pyAttribute === 'pys-onKeyDown'){ console.warn("Use of pys-onClick and pys-onKeyDown attributes is deprecated in favor of py-onClick() and py-onKeyDown(). pys-on* attributes will be deprecated in a future version of PyScript.") const source = ` from pyodide.ffi import create_proxy Element("${el.id}").element.addEventListener("${event}", create_proxy(${handlerCode})) `; await runtime.run(source); } else{ el.addEventListener(event, () => { (async() => {await runtime.run(handlerCode)})(); }); } // TODO: Should we actually map handlers in JS instead of Python? // el.onclick = (evt: any) => { // console.log("click"); // new Promise((resolve, reject) => { // setTimeout(() => { // console.log('Inside') // }, 300); // }).then(() => { // console.log("resolved") // }); // // let handlerCode = el.getAttribute('py-onClick'); // // pyodide.runPython(handlerCode); // } } } /** Mount all elements with attribute py-mount into the Python namespace */ export async function mountElements(runtime: Runtime) { const matches: NodeListOf = document.querySelectorAll('[py-mount]'); logger.info(`py-mount: found ${matches.length} elements`); let source = ''; for (const el of matches) { const mountName = el.getAttribute('py-mount') || el.id.split('-').join('_'); source += `\n${mountName} = Element("${el.id}")`; } await runtime.run(source); }