diff --git a/packages/interpreter/build.rs b/packages/interpreter/build.rs index 6f0edeb31..a95f973f9 100644 --- a/packages/interpreter/build.rs +++ b/packages/interpreter/build.rs @@ -8,7 +8,7 @@ fn main() { let hash = hash_ts_files(); // If the hash matches the one on disk, we're good and don't need to update bindings - let expected = include_str!("./src/js/hash.txt").trim(); + let expected = include_str!("src/js/hash.txt").trim(); if expected == hash.to_string() { return; } @@ -21,23 +21,32 @@ fn main() { gen_bindings("native", "native"); gen_bindings("core", "core"); - std::fs::write("src/js/hash.txt", hash).unwrap(); + std::fs::write("src/js/hash.txt", hash.to_string()).unwrap(); } /// Hashes the contents of a directory -fn hash_ts_files() -> String { - let mut out = "".to_string(); +fn hash_ts_files() -> u128 { + let mut out = 0; - let files = &[ - include_str!("./src/ts/common.ts"), - include_str!("./src/ts/native.ts"), - include_str!("./src/ts/core.ts"), + let files = [ + include_str!("src/ts/common.ts"), + include_str!("src/ts/native.ts"), + include_str!("src/ts/core.ts"), ]; + // Let's make the dumbest hasher by summing the bytes of the files + // The location is multiplied by the byte value to make sure that the order of the bytes matters + let mut idx = 0; for file in files { - out = format!("{out}{file}"); + // windows + git does a weird thing with line endings, so we need to normalize them + for line in file.lines() { + idx += 1; + for byte in line.bytes() { + idx += 1; + out += (byte as u128) * (idx as u128); + } + } } - out } diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index c37c153a7..9fe9e2627 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1,557 +1 @@ -export { setAttributeInner } from "./set_attribute"; -export { retrieveFormValues } from "./form"; -// This file provides an extended variant of the interpreter used for desktop and liveview interaction -// -// This file lives on the renderer, not the host. It's basically a polyfill over functionality that the host can't -// provide since it doesn't have access to the dom. - -import { BaseInterpreter, NodeId } from "./core"; -import { SerializedEvent, serializeEvent } from "./serialize"; - -// okay so, we've got this JSChannel thing from sledgehammer, implicitly imported into our scope -// we want to extend it, and it technically extends base intepreter. To make typescript happy, -// we're going to bind the JSChannel_ object to the JSChannel object, and then extend it -var JSChannel_: typeof BaseInterpreter; - -// @ts-ignore - this is coming from the host -if (RawInterpreter !== undefined && RawInterpreter !== null) { - // @ts-ignore - this is coming from the host - JSChannel_ = RawInterpreter; -}; - -export class NativeInterpreter extends JSChannel_ { - intercept_link_redirects: boolean; - ipc: any; - editsPath: string; - - // eventually we want to remove liveview and build it into the server-side-events of fullstack - // however, for now we need to support it since SSE in fullstack doesn't exist yet - liveview: boolean; - - constructor(editsPath: string) { - super(); - this.editsPath = editsPath; - } - - initialize(root: HTMLElement): void { - this.intercept_link_redirects = true; - this.liveview = false; - - // attach an event listener on the body that prevents file drops from navigating - // this is because the browser will try to navigate to the file if it's dropped on the window - window.addEventListener("dragover", function (e) { - // // check which element is our target - if (e.target instanceof Element && e.target.tagName != "INPUT") { - e.preventDefault(); - } - }, false); - - window.addEventListener("drop", function (e) { - let target = e.target; - - if (!(target instanceof Element)) { - return; - } - - // Dropping a file on the window will navigate to the file, which we don't want - e.preventDefault(); - }, false); - - // attach a listener to the route that listens for clicks and prevents the default file dialog - window.addEventListener("click", (event) => { - const target = event.target; - if (target instanceof HTMLInputElement && target.getAttribute("type") === "file") { - // Send a message to the host to open the file dialog if the target is a file input and has a dioxus id attached to it - let target_id = getTargetId(target); - if (target_id !== null) { - const message = this.serializeIpcMessage("file_dialog", { - event: "change&input", - accept: target.getAttribute("accept"), - directory: target.getAttribute("webkitdirectory") === "true", - multiple: target.hasAttribute("multiple"), - target: target_id, - bubbles: event.bubbles, - }); - this.ipc.postMessage(message); - } - - // Prevent default regardless - we don't want file dialogs and we don't want the browser to navigate - event.preventDefault(); - } - }); - - - // @ts-ignore - wry gives us this - this.ipc = window.ipc; - - // make sure we pass the handler to the base interpreter - const handler: EventListener = (event) => this.handleEvent(event, event.type, true); - super.initialize(root, handler); - } - - serializeIpcMessage(method: string, params = {}) { - return JSON.stringify({ method, params }); - } - - scrollTo(id: NodeId, behavior: ScrollBehavior) { - const node = this.nodes[id]; - if (node instanceof HTMLElement) { - node.scrollIntoView({ behavior }); - } - } - - getClientRect(id: NodeId): { type: string; origin: number[]; size: number[]; } | undefined { - const node = this.nodes[id]; - if (node instanceof HTMLElement) { - const rect = node.getBoundingClientRect(); - return { - type: "GetClientRect", - origin: [rect.x, rect.y], - size: [rect.width, rect.height], - }; - } - } - - setFocus(id: NodeId, focus: boolean) { - const node = this.nodes[id]; - - if (node instanceof HTMLElement) { - if (focus) { - node.focus(); - } else { - node.blur(); - } - } - } - - // ignore the fact the base interpreter uses ptr + len but we use array... - // @ts-ignore - loadChild(array: number[]) { - // iterate through each number and get that child - let node = this.stack[this.stack.length - 1]; - - for (let i = 0; i < array.length; i++) { - let end = array[i]; - for (node = node.firstChild; end > 0; end--) { - node = node.nextSibling; - } - } - - return node; - } - - appendChildren(id: NodeId, many: number) { - const root = this.nodes[id]; - const els = this.stack.splice(this.stack.length - many); - - for (let k = 0; k < many; k++) { - root.appendChild(els[k]); - } - } - - handleEvent(event: Event, name: string, bubbles: boolean) { - const target = event.target!; - const realId = getTargetId(target)!; - const contents = serializeEvent(event, target); - - // Handle the event on the virtualdom and then preventDefault if it also preventsDefault - // Some listeners - let body = { - name: name, - data: contents, - element: realId, - bubbles, - }; - - // Run any prevent defaults the user might've set - // This is to support the prevent_default: "onclick" attribute that dioxus has had for a while, but is not necessary - // now that we expose preventDefault to the virtualdom on desktop - // Liveview will still need to use this - this.preventDefaults(event, target); - - // liveview does not have syncronous event handling, so we need to send the event to the host - if (this.liveview) { - // Okay, so the user might've requested some files to be read - if (target instanceof HTMLInputElement && (event.type === "change" || event.type === "input")) { - if (target.getAttribute("type") === "file") { - this.readFiles(target, contents, bubbles, realId, name); - } - } - } else { - - const message = this.serializeIpcMessage("user_event", body); - this.ipc.postMessage(message); - - // // Run the event handler on the virtualdom - // // capture/prevent default of the event if the virtualdom wants to - // const res = handleVirtualdomEventSync(JSON.stringify(body)); - - // if (res.preventDefault) { - // event.preventDefault(); - // } - - // if (res.stopPropagation) { - // event.stopPropagation(); - // } - } - } - - - - // This should: - // - prevent form submissions from navigating - // - prevent anchor tags from navigating - // - prevent buttons from submitting forms - // - let the virtualdom attempt to prevent the event - preventDefaults(event: Event, target: EventTarget) { - let preventDefaultRequests: string | null = null; - - // Some events can be triggered on text nodes, which don't have attributes - if (target instanceof Element) { - preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`); - } - - if (preventDefaultRequests && preventDefaultRequests.includes(`on${event.type}`)) { - event.preventDefault(); - } - - if (event.type === "submit") { - event.preventDefault(); - } - - // Attempt to intercept if the event is a click - if (target instanceof Element && event.type === "click") { - this.handleClickNavigate(event, target, preventDefaultRequests); - } - } - - handleClickNavigate(event: Event, target: Element, preventDefaultRequests: string) { - // todo call prevent default if it's the right type of event - if (!this.intercept_link_redirects) { - return; - } - - // prevent buttons in forms from submitting the form - if (target.tagName === "BUTTON" && event.type == "submit") { - event.preventDefault(); - } - - // If the target is an anchor tag, we want to intercept the click too, to prevent the browser from navigating - let a_element = target.closest("a"); - if (a_element == null) { - return; - } - - event.preventDefault(); - - let elementShouldPreventDefault = - preventDefaultRequests && preventDefaultRequests.includes(`onclick`); - - let aElementShouldPreventDefault = a_element.getAttribute( - `dioxus-prevent-default` - ); - - let linkShouldPreventDefault = - aElementShouldPreventDefault && - aElementShouldPreventDefault.includes(`onclick`); - - if (!elementShouldPreventDefault && !linkShouldPreventDefault) { - const href = a_element.getAttribute("href"); - if (href !== "" && href !== null && href !== undefined) { - this.ipc.postMessage( - this.serializeIpcMessage("browser_open", { href }) - ); - } - } - } - - waitForRequest(headless: boolean) { - fetch(new Request(this.editsPath)) - .then(response => response.arrayBuffer()) - .then(bytes => { - // In headless mode, the requestAnimationFrame callback is never called, so we need to run the bytes directly - if (headless) { - // @ts-ignore - this.run_from_bytes(bytes); - } else { - // @ts-ignore - requestAnimationFrame(() => this.run_from_bytes(bytes)); - } - this.waitForRequest(headless); - }); - } - - - // A liveview only function - // Desktop will intercept the event before it hits this - async readFiles(target: HTMLInputElement, contents: SerializedEvent, bubbles: boolean, realId: NodeId, name: string) { - let files = target.files!; - let file_contents: { [name: string]: number[] } = {}; - - for (let i = 0; i < files.length; i++) { - const file = files[i]; - file_contents[file.name] = Array.from( - new Uint8Array(await file.arrayBuffer()) - ); - } - - contents.files = { files: file_contents }; - - const message = this.serializeIpcMessage("user_event", { - name: name, - element: realId, - data: contents, - bubbles, - }); - - this.ipc.postMessage(message); - } -} - -type EventSyncResult = { - preventDefault: boolean; - stopPropagation: boolean; - stopImmediatePropagation: boolean; - filesRequested: boolean; -}; - -// This function sends the event to the virtualdom and then waits for the virtualdom to process it -// -// However, it's not really suitable for liveview, because it's synchronous and will block the main thread -// We should definitely consider using a websocket if we want to block... or just not block on liveview -// Liveview is a little bit of a tricky beast -function handleVirtualdomEventSync(contents: string): EventSyncResult { - // Handle the event on the virtualdom and then process whatever its output was - const xhr = new XMLHttpRequest(); - - // Serialize the event and send it to the custom protocol in the Rust side of things - xhr.timeout = 1000; - xhr.open("GET", "/handle/event.please", false); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.send(contents); - - // Deserialize the response, and then prevent the default/capture the event if the virtualdom wants to - return JSON.parse(xhr.responseText); -} - -function getTargetId(target: EventTarget): NodeId | null { - // Ensure that the target is a node, sometimes it's nota - if (!(target instanceof Node)) { - return null; - } - - let ourTarget = target; - let realId = null; - - while (realId == null) { - if (ourTarget === null) { - return null; - } - - if (ourTarget instanceof Element) { - realId = ourTarget.getAttribute(`data-dioxus-id`); - } - - ourTarget = ourTarget.parentNode; - } - - return parseInt(realId); -} - - -// function applyFileUpload() { -// let inputs = document.querySelectorAll("input"); -// for (let input of inputs) { -// if (!input.getAttribute("data-dioxus-file-listener")) { -// // prevent file inputs from opening the file dialog on click -// const type = input.getAttribute("type"); -// if (type === "file") { -// input.setAttribute("data-dioxus-file-listener", true); -// input.addEventListener("click", (event) => { -// let target = event.target; -// let target_id = find_real_id(target); -// if (target_id !== null) { -// const send = (event_name) => { -// const message = window.interpreter.serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name }); -// window.ipc.postMessage(message); -// }; -// send("change&input"); -// } -// event.preventDefault(); -// }); -// } -// } -// } -// The root interpreter class that holds state about the mapping between DOM and VirtualDom -// This always lives in the JS side of things, and is extended by the native and web interpreters - -import { setAttributeInner } from "./set_attribute"; - -export type NodeId = number; - -export class BaseInterpreter { - // non bubbling events listen at the element the listener was created at - global: { - [key: string]: { active: number, callback: EventListener } - }; - // bubbling events can listen at the root element - local: { - [key: string]: { - [key: string]: EventListener - } - }; - - root: HTMLElement; - handler: EventListener; - nodes: Node[]; - stack: Node[]; - templates: { - [key: number]: Node[] - }; - - // sledgehammer is generating this... - m: any; - - constructor() { } - - initialize(root: HTMLElement, handler: EventListener | null = null) { - this.global = {}; - this.local = {}; - this.root = root; - - this.nodes = [root]; - this.stack = [root]; - this.templates = {}; - - if (handler) { - this.handler = handler; - } - } - - createListener(event_name: string, element: HTMLElement, bubbles: boolean) { - if (bubbles) { - if (this.global[event_name] === undefined) { - this.global[event_name] = { active: 1, callback: this.handler }; - this.root.addEventListener(event_name, this.handler); - } else { - this.global[event_name].active++; - } - } else { - const id = element.getAttribute("data-dioxus-id"); - if (!this.local[id]) { - this.local[id] = {}; - } - element.addEventListener(event_name, this.handler); - } - } - - removeListener(element: HTMLElement, event_name: string, bubbles: boolean) { - if (bubbles) { - this.removeBubblingListener(event_name); - } else { - this.removeNonBubblingListener(element, event_name); - } - } - - removeBubblingListener(event_name: string) { - this.global[event_name].active--; - if (this.global[event_name].active === 0) { - this.root.removeEventListener(event_name, this.global[event_name].callback); - delete this.global[event_name]; - } - } - - removeNonBubblingListener(element: HTMLElement, event_name: string) { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id][event_name]; - if (Object.keys(this.local[id]).length === 0) { - delete this.local[id]; - } - element.removeEventListener(event_name, this.handler); - } - - removeAllNonBubblingListeners(element: HTMLElement) { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id]; - } - - getNode(id: NodeId): Node { - return this.nodes[id]; - } - - appendChildren(id: NodeId, many: number) { - const root = this.nodes[id]; - const els = this.stack.splice(this.stack.length - many); - for (let k = 0; k < many; k++) { - root.appendChild(els[k]); - } - } - - loadChild(ptr: number, len: number): Node { - // iterate through each number and get that child - let node = this.stack[this.stack.length - 1] as Node; - let ptr_end = ptr + len; - - for (; ptr < ptr_end; ptr++) { - let end = this.m.getUint8(ptr); - for (node = node.firstChild; end > 0; end--) { - node = node.nextSibling; - } - } - - return node; - } - - saveTemplate(nodes: HTMLElement[], tmpl_id: number) { - this.templates[tmpl_id] = nodes; - } - - hydrate(ids: { [key: number]: number }) { - const hydrateNodes = document.querySelectorAll('[data-node-hydration]'); - - for (let i = 0; i < hydrateNodes.length; i++) { - const hydrateNode = hydrateNodes[i] as HTMLElement; - const hydration = hydrateNode.getAttribute('data-node-hydration'); - const split = hydration!.split(','); - const id = ids[parseInt(split[0])]; - - this.nodes[id] = hydrateNode; - - if (split.length > 1) { - // @ts-ignore - hydrateNode.listening = split.length - 1; - hydrateNode.setAttribute('data-dioxus-id', id.toString()); - for (let j = 1; j < split.length; j++) { - const listener = split[j]; - const split2 = listener.split(':'); - const event_name = split2[0]; - const bubbles = split2[1] === '1'; - this.createListener(event_name, hydrateNode, bubbles); - } - } - } - - const treeWalker = document.createTreeWalker( - document.body, - NodeFilter.SHOW_COMMENT, - ); - - let currentNode = treeWalker.nextNode(); - - while (currentNode) { - const id = currentNode.textContent!; - const split = id.split('node-id'); - - if (split.length > 1) { - this.nodes[ids[parseInt(split[1])]] = currentNode.nextSibling; - } - - currentNode = treeWalker.nextNode(); - } - } - - setAttributeInner(node: HTMLElement, field: string, value: string, ns: string) { - setAttributeInner(node, field, value, ns); - } -} - +12655652627 \ No newline at end of file