From 0d2e1f4051d3a053a37bcf0abbb54c83bade49ba Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 15 Jul 2025 16:26:52 -0500 Subject: [PATCH] Prevent desktop reloads at the wry level (#4422) * Prevent desktop reloads at the wry level * add a test * only allow one navigation --- packages/desktop/Cargo.toml | 5 ++ packages/desktop/headless_tests/forms.rs | 109 +++++++++++++++++++++++ packages/desktop/src/webview.rs | 9 +- packages/interpreter/src/js/hash.txt | 2 +- packages/interpreter/src/js/native.js | 2 +- packages/interpreter/src/ts/native.ts | 9 -- packages/web/src/dom.rs | 8 +- 7 files changed, 124 insertions(+), 20 deletions(-) create mode 100644 packages/desktop/headless_tests/forms.rs diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 6beeafeff..3fc9b6763 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -138,6 +138,11 @@ name = "check_rendering" path = "headless_tests/rendering.rs" harness = false +[[test]] +name = "check_forms" +path = "headless_tests/forms.rs" +harness = false + [[test]] name = "check_eval" path = "headless_tests/eval.rs" diff --git a/packages/desktop/headless_tests/forms.rs b/packages/desktop/headless_tests/forms.rs new file mode 100644 index 000000000..6e939dfb3 --- /dev/null +++ b/packages/desktop/headless_tests/forms.rs @@ -0,0 +1,109 @@ +use dioxus::prelude::*; +use dioxus_desktop::DesktopContext; +use dioxus_document::eval; + +#[path = "./utils.rs"] +mod utils; + +fn main() { + #[cfg(not(windows))] + utils::check_app_exits(check_html_renders); +} + +async fn inner_html(count: usize) -> String { + let html = document::eval(&format!( + r#"// Wait until the element is available + const interval = setInterval(() => {{ + const element = document.getElementById('div-{count}'); + if (element) {{ + dioxus.send(element.innerHTML); + clearInterval(interval); + }} + }}, 100);"# + )) + .recv::() + .await + .unwrap(); + + println!("html: {html}"); + html +} + +fn check_html_renders() -> Element { + let mut signal = use_signal(|| 0); + + use_effect(move || { + let signal = signal(); + // Pass the test once the count is greater than 10 + if signal > 10 { + let desktop_context: DesktopContext = consume_context(); + desktop_context.close(); + } + + spawn(async move { + let raw_html = inner_html(signal).await; + println!("{raw_html}"); + // Make sure the html contains count is {signal} + assert!(raw_html.contains(&format!("count is {}", signal))); + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + eval( + r#"let button = document.querySelector('#increment'); + button.dispatchEvent(new MouseEvent('click', { + view: window, + bubbles: true, + cancelable: true, + buttons: 2, + button: 2, + }));"#, + ); + + // If the signal is even, also submit the form. This should not effect the count because the navigation is blocked + if signal % 2 == 0 { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + eval( + r#"let form = document.getElementById('form-submit'); + form.dispatchEvent(new Event('click', { + view: window, + bubbles: true, + cancelable: true, + buttons: 2, + button: 2, + }));"#, + ); + } + }); + }); + + rsx! { + div { + id: "div-{signal}", + h1 { "Form" } + form { + input { + r#type: "text", + name: "username", + id: "username", + value: "goodbye" + } + input { r#type: "text", name: "full-name", value: "lorem" } + input { r#type: "password", name: "password", value: "ipsum" } + input { + r#type: "radio", + name: "color", + value: "red", + checked: true + } + input { r#type: "radio", name: "color", value: "blue" } + button { id: "form-submit", r#type: "submit", value: "Submit", "Submit the form" } + } + + button { + id: "increment", + onclick: move |_| { + signal += 1; + }, + "count is {signal}" + } + } + } +} diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index d46583148..01f0c6123 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -21,6 +21,7 @@ use dioxus_history::{History, MemoryHistory}; use dioxus_hooks::to_owned; use dioxus_html::{HasFileData, HtmlEvent, PlatformEventData}; use futures_util::{pin_mut, FutureExt}; +use std::sync::atomic::AtomicBool; use std::sync::Arc; use std::{cell::OnceCell, time::Duration}; use std::{rc::Rc, task::Waker}; @@ -300,6 +301,8 @@ impl WebviewInstance { } }; + let page_loaded = AtomicBool::new(false); + let mut webview = WebViewBuilder::new_with_web_context(&mut web_context) .with_bounds(wry::Rect { position: wry::dpi::Position::Logical(wry::dpi::LogicalPosition::new(0.0, 0.0)), @@ -311,11 +314,13 @@ impl WebviewInstance { .with_transparent(cfg.window.window.transparent) .with_url("dioxus://index.html/") .with_ipc_handler(ipc_handler) - .with_navigation_handler(|var| { + .with_navigation_handler(move |var| { // We don't want to allow any navigation // We only want to serve the index file and assets if var.starts_with("dioxus://") || var.starts_with("http://dioxus.") { - true + // After the page has loaded once, don't allow any more navigation + let page_loaded = page_loaded.swap(true, std::sync::atomic::Ordering::SeqCst); + !page_loaded } else { if var.starts_with("http://") || var.starts_with("https://") diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index 89420bad2..407f37e41 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -[6449103750905854967, 17669692872757955279, 13069001215487072322, 11420464406527728232, 3770103091118609057, 5444526391971481782, 8634731337869623803, 5052021921702764563, 7041565893588933645, 11339769846046015954] \ No newline at end of file +[6449103750905854967, 17669692872757955279, 13069001215487072322, 11420464406527728232, 3770103091118609057, 5444526391971481782, 14692042304579321130, 5052021921702764563, 7041565893588933645, 11339769846046015954] \ No newline at end of file diff --git a/packages/interpreter/src/js/native.js b/packages/interpreter/src/js/native.js index 0894f82f3..f08b6f006 100644 --- a/packages/interpreter/src/js/native.js +++ b/packages/interpreter/src/js/native.js @@ -1 +1 @@ -function retrieveValues(event,target){let contents={values:{}},form=target.closest("form");if(form){if(event.type==="input"||event.type==="change"||event.type==="submit"||event.type==="reset"||event.type==="click")contents=retrieveFormValues(form)}return contents}function retrieveFormValues(form){let formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(contents[key])contents[key].push(value);else contents[key]=[value]}),{valid:form.checkValidity(),values:contents}}function retrieveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;icontents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(event instanceof CustomEvent){let detail=event.detail;if(detail instanceof ResizeObserverEntry)extend(serializeResizeEventDetail(detail));else if(detail instanceof IntersectionObserverEntry)extend(serializeIntersectionEventDetail(detail))}if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);if(event.type==="scroll")extend(serializeScrollEvent(event));return contents}function toSerializableResizeObserverSize(size,is_inline_width){return[is_inline_width?size.inlineSize:size.blockSize,is_inline_width?size.blockSize:size.inlineSize]}function serializeResizeEventDetail(detail){let is_inline_width=!0;if(detail.target instanceof HTMLElement){if(window.getComputedStyle(detail.target).getPropertyValue("writing-mode")!=="horizontal-tb")is_inline_width=!1}return{border_box_size:detail.borderBoxSize!==void 0?toSerializableResizeObserverSize(detail.borderBoxSize[0],is_inline_width):detail.contentRect,content_box_size:detail.contentBoxSize!==void 0?toSerializableResizeObserverSize(detail.contentBoxSize[0],is_inline_width):detail.contentRect,content_rect:detail.contentRect}}function serializeIntersectionEventDetail(detail){return{bounding_client_rect:detail.boundingClientRect,intersection_ratio:detail.intersectionRatio,intersection_rect:detail.intersectionRect,is_intersecting:detail.isIntersecting,root_bounds:detail.rootBounds,time_ms:Math.floor(Date.now()+detail.time)}}function serializeInputEvent(event,target){let contents={};if(target instanceof HTMLElement){let values=retrieveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retrieveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents}function serializeWheelEvent(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}}function serializeTouchEvent(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:serializeTouchList(event.changedTouches),target_touches:serializeTouchList(event.targetTouches),touches:serializeTouchList(event.touches)}}function serializePointerEvent(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}}function serializeTouchList(touchList){let serializedTouches=[];for(let i=0;i0)files={files:{placeholder:[]}};return{mouse:{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,...serializeMouseEvent(event)},files}}function serializeScrollEvent(event){let scrollLeft=0,scrollTop=0,scrollWidth=0,scrollHeight=0,clientWidth=0,clientHeight=0;if(event.target instanceof Element)scrollLeft=event.target.scrollLeft,scrollTop=event.target.scrollTop,scrollWidth=event.target.scrollWidth,scrollHeight=event.target.scrollHeight,clientWidth=event.target.clientWidth,clientHeight=event.target.clientHeight;else if(event.target===document)scrollLeft=window.scrollX||document.documentElement.scrollLeft,scrollTop=window.scrollY||document.documentElement.scrollTop,scrollWidth=document.documentElement.scrollWidth,scrollHeight=document.documentElement.scrollHeight,clientWidth=document.documentElement.clientWidth,clientHeight=document.documentElement.clientHeight;return{scroll_left:scrollLeft,scroll_top:scrollTop,scroll_width:scrollWidth,scroll_height:scrollHeight,client_width:clientWidth,client_height:clientHeight}}var JSChannel_;if(RawInterpreter!==void 0&&RawInterpreter!==null)JSChannel_=RawInterpreter;class NativeInterpreter extends JSChannel_{intercept_link_redirects;ipc;edits;eventsPath;headless;kickStylesheets;queuedBytes=[];liveview;constructor(eventsPath,headless){super();this.eventsPath=eventsPath,this.kickStylesheets=!1,this.headless=headless}initialize(root){this.intercept_link_redirects=!0,this.liveview=!1,window.addEventListener("dragover",function(e){if(e.target instanceof Element&&e.target.tagName!="INPUT")e.preventDefault()},!1),window.addEventListener("drop",function(e){if(!(e.target instanceof Element))return;e.preventDefault()},!1),window.addEventListener("click",(event)=>{let target=event.target;if(target instanceof HTMLInputElement&&target.getAttribute("type")==="file"){let target_id=getTargetId(target);if(target_id!==null){let 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()}}}),this.ipc=window.ipc;let handler=(event)=>this.handleEvent(event,event.type,!0);super.initialize(root,handler)}serializeIpcMessage(method,params={}){return JSON.stringify({method,params})}scrollTo(id,options){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollIntoView(options),!0;return!1}scroll(id,x,y,behavior){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scroll({top:y,left:x,behavior}),!0;return!1}getScrollHeight(id){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollHeight}getScrollLeft(id){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollLeft}getScrollTop(id){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollTop}getScrollWidth(id){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollWidth}getClientRect(id){let node=this.nodes[id];if(node instanceof HTMLElement){let rect=node.getBoundingClientRect();return{type:"GetClientRect",origin:[rect.x,rect.y],size:[rect.width,rect.height]}}}setFocus(id,focus){let node=this.nodes[id];if(node instanceof HTMLElement)if(focus)node.focus();else node.blur()}handleWindowsDragDrop(){if(window.dxDragLastElement){let dragLeaveEvent=new DragEvent("dragleave",{bubbles:!0,cancelable:!0});window.dxDragLastElement.dispatchEvent(dragLeaveEvent);let data=new DataTransfer,file=new File(["content"],"file.txt",{type:"text/plain"});data.items.add(file);let dragDropEvent=new DragEvent("drop",{bubbles:!0,cancelable:!0,dataTransfer:data});window.dxDragLastElement.dispatchEvent(dragDropEvent),window.dxDragLastElement=null}}handleWindowsDragOver(xPos,yPos){let displayScaleFactor=window.devicePixelRatio||1;xPos/=displayScaleFactor,yPos/=displayScaleFactor;let element=document.elementFromPoint(xPos,yPos);if(element!=window.dxDragLastElement){if(window.dxDragLastElement){let dragLeaveEvent=new DragEvent("dragleave",{bubbles:!0,cancelable:!0});window.dxDragLastElement.dispatchEvent(dragLeaveEvent)}let dragOverEvent=new DragEvent("dragover",{bubbles:!0,cancelable:!0});element.dispatchEvent(dragOverEvent),window.dxDragLastElement=element}}handleWindowsDragLeave(){if(window.dxDragLastElement){let dragLeaveEvent=new DragEvent("dragleave",{bubbles:!0,cancelable:!0});window.dxDragLastElement.dispatchEvent(dragLeaveEvent),window.dxDragLastElement=null}}loadChild(array){let node=this.stack[this.stack.length-1];for(let i=0;i0;end--)node=node.nextSibling}return node}appendChildren(id,many){let root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k{this.flushQueuedBytes(),this.markEditsFinished()})}waitForRequest(editsPath,required_server_key){this.edits=new WebSocket(editsPath);let authenticated=!1;this.edits.onclose=()=>{setTimeout(()=>{if(this.edits.url!=editsPath)return;this.waitForRequest(editsPath,required_server_key)},100)},this.edits.onmessage=(event)=>{let data=event.data;if(data instanceof Blob){if(!authenticated)return;data.arrayBuffer().then((buffer)=>{this.rafEdits(buffer)})}else if(typeof data==="string"){if(data===required_server_key){authenticated=!0;return}}}}markEditsFinished(){this.edits.send(new ArrayBuffer(0))}kickAllStylesheetsOnPage(){let stylesheets=document.querySelectorAll("link[rel=stylesheet]");for(let i=0;i{if(contents[key])contents[key].push(value);else contents[key]=[value]}),{valid:form.checkValidity(),values:contents}}function retrieveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;icontents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(event instanceof CustomEvent){let detail=event.detail;if(detail instanceof ResizeObserverEntry)extend(serializeResizeEventDetail(detail));else if(detail instanceof IntersectionObserverEntry)extend(serializeIntersectionEventDetail(detail))}if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);if(event.type==="scroll")extend(serializeScrollEvent(event));return contents}function toSerializableResizeObserverSize(size,is_inline_width){return[is_inline_width?size.inlineSize:size.blockSize,is_inline_width?size.blockSize:size.inlineSize]}function serializeResizeEventDetail(detail){let is_inline_width=!0;if(detail.target instanceof HTMLElement){if(window.getComputedStyle(detail.target).getPropertyValue("writing-mode")!=="horizontal-tb")is_inline_width=!1}return{border_box_size:detail.borderBoxSize!==void 0?toSerializableResizeObserverSize(detail.borderBoxSize[0],is_inline_width):detail.contentRect,content_box_size:detail.contentBoxSize!==void 0?toSerializableResizeObserverSize(detail.contentBoxSize[0],is_inline_width):detail.contentRect,content_rect:detail.contentRect}}function serializeIntersectionEventDetail(detail){return{bounding_client_rect:detail.boundingClientRect,intersection_ratio:detail.intersectionRatio,intersection_rect:detail.intersectionRect,is_intersecting:detail.isIntersecting,root_bounds:detail.rootBounds,time_ms:Math.floor(Date.now()+detail.time)}}function serializeInputEvent(event,target){let contents={};if(target instanceof HTMLElement){let values=retrieveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retrieveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents}function serializeWheelEvent(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}}function serializeTouchEvent(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:serializeTouchList(event.changedTouches),target_touches:serializeTouchList(event.targetTouches),touches:serializeTouchList(event.touches)}}function serializePointerEvent(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}}function serializeTouchList(touchList){let serializedTouches=[];for(let i=0;i0)files={files:{placeholder:[]}};return{mouse:{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,...serializeMouseEvent(event)},files}}function serializeScrollEvent(event){let scrollLeft=0,scrollTop=0,scrollWidth=0,scrollHeight=0,clientWidth=0,clientHeight=0;if(event.target instanceof Element)scrollLeft=event.target.scrollLeft,scrollTop=event.target.scrollTop,scrollWidth=event.target.scrollWidth,scrollHeight=event.target.scrollHeight,clientWidth=event.target.clientWidth,clientHeight=event.target.clientHeight;else if(event.target===document)scrollLeft=window.scrollX||document.documentElement.scrollLeft,scrollTop=window.scrollY||document.documentElement.scrollTop,scrollWidth=document.documentElement.scrollWidth,scrollHeight=document.documentElement.scrollHeight,clientWidth=document.documentElement.clientWidth,clientHeight=document.documentElement.clientHeight;return{scroll_left:scrollLeft,scroll_top:scrollTop,scroll_width:scrollWidth,scroll_height:scrollHeight,client_width:clientWidth,client_height:clientHeight}}var JSChannel_;if(RawInterpreter!==void 0&&RawInterpreter!==null)JSChannel_=RawInterpreter;class NativeInterpreter extends JSChannel_{intercept_link_redirects;ipc;edits;eventsPath;headless;kickStylesheets;queuedBytes=[];liveview;constructor(eventsPath,headless){super();this.eventsPath=eventsPath,this.kickStylesheets=!1,this.headless=headless}initialize(root){this.intercept_link_redirects=!0,this.liveview=!1,window.addEventListener("dragover",function(e){if(e.target instanceof Element&&e.target.tagName!="INPUT")e.preventDefault()},!1),window.addEventListener("drop",function(e){if(!(e.target instanceof Element))return;e.preventDefault()},!1),window.addEventListener("click",(event)=>{let target=event.target;if(target instanceof HTMLInputElement&&target.getAttribute("type")==="file"){let target_id=getTargetId(target);if(target_id!==null){let 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()}}}),this.ipc=window.ipc;let handler=(event)=>this.handleEvent(event,event.type,!0);super.initialize(root,handler)}serializeIpcMessage(method,params={}){return JSON.stringify({method,params})}scrollTo(id,options){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollIntoView(options),!0;return!1}scroll(id,x,y,behavior){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scroll({top:y,left:x,behavior}),!0;return!1}getScrollHeight(id){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollHeight}getScrollLeft(id){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollLeft}getScrollTop(id){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollTop}getScrollWidth(id){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollWidth}getClientRect(id){let node=this.nodes[id];if(node instanceof HTMLElement){let rect=node.getBoundingClientRect();return{type:"GetClientRect",origin:[rect.x,rect.y],size:[rect.width,rect.height]}}}setFocus(id,focus){let node=this.nodes[id];if(node instanceof HTMLElement)if(focus)node.focus();else node.blur()}handleWindowsDragDrop(){if(window.dxDragLastElement){let dragLeaveEvent=new DragEvent("dragleave",{bubbles:!0,cancelable:!0});window.dxDragLastElement.dispatchEvent(dragLeaveEvent);let data=new DataTransfer,file=new File(["content"],"file.txt",{type:"text/plain"});data.items.add(file);let dragDropEvent=new DragEvent("drop",{bubbles:!0,cancelable:!0,dataTransfer:data});window.dxDragLastElement.dispatchEvent(dragDropEvent),window.dxDragLastElement=null}}handleWindowsDragOver(xPos,yPos){let displayScaleFactor=window.devicePixelRatio||1;xPos/=displayScaleFactor,yPos/=displayScaleFactor;let element=document.elementFromPoint(xPos,yPos);if(element!=window.dxDragLastElement){if(window.dxDragLastElement){let dragLeaveEvent=new DragEvent("dragleave",{bubbles:!0,cancelable:!0});window.dxDragLastElement.dispatchEvent(dragLeaveEvent)}let dragOverEvent=new DragEvent("dragover",{bubbles:!0,cancelable:!0});element.dispatchEvent(dragOverEvent),window.dxDragLastElement=element}}handleWindowsDragLeave(){if(window.dxDragLastElement){let dragLeaveEvent=new DragEvent("dragleave",{bubbles:!0,cancelable:!0});window.dxDragLastElement.dispatchEvent(dragLeaveEvent),window.dxDragLastElement=null}}loadChild(array){let node=this.stack[this.stack.length-1];for(let i=0;i0;end--)node=node.nextSibling}return node}appendChildren(id,many){let root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k{this.flushQueuedBytes(),this.markEditsFinished()})}waitForRequest(editsPath,required_server_key){this.edits=new WebSocket(editsPath);let authenticated=!1;this.edits.onclose=()=>{setTimeout(()=>{if(this.edits.url!=editsPath)return;this.waitForRequest(editsPath,required_server_key)},100)},this.edits.onmessage=(event)=>{let data=event.data;if(data instanceof Blob){if(!authenticated)return;data.arrayBuffer().then((buffer)=>{this.rafEdits(buffer)})}else if(typeof data==="string"){if(data===required_server_key){authenticated=!0;return}}}}markEditsFinished(){this.edits.send(new ArrayBuffer(0))}kickAllStylesheetsOnPage(){let stylesheets=document.querySelectorAll("link[rel=stylesheet]");for(let i=0;i