123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398 |
- // 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;
- kickStylesheets: boolean;
- // 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;
- this.kickStylesheets = false;
- }
- 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);
- 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 {
- requestAnimationFrame(() => {
- // @ts-ignore
- this.run_from_bytes(bytes)
- });
- }
- this.waitForRequest(headless);
- });
- }
- kickAllStylesheetsOnPage() {
- // If this function is being called and we have not explicitly set kickStylesheets to true, then we should
- // force kick the stylesheets, regardless if they have a dioxus attribute or not
- // This happens when any hotreload happens.
- let stylesheets = document.querySelectorAll("link[rel=stylesheet]");
- for (let i = 0; i < stylesheets.length; i++) {
- let sheet = stylesheets[i] as HTMLLinkElement;
- // Using `cache: reload` will force the browser to re-fetch the stylesheet and bust the cache
- fetch(sheet.href, { cache: "reload" }).then(() => {
- sheet.href = sheet.href + "?" + Math.random();
- });
- }
- }
- // 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();
- // });
- // }
- // }
- // }
|