Przeglądaj źródła

Support scrollIntoView alignment options (#3952)

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
Otger Rogla 1 miesiąc temu
rodzic
commit
28d38307a2

+ 2 - 2
packages/desktop/src/element.rs

@@ -79,12 +79,12 @@ impl RenderedElementBacking for DesktopElement {
 
     fn scroll_to(
         &self,
-        behavior: dioxus_html::ScrollBehavior,
+        options: dioxus_html::ScrollToOptions,
     ) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
         let script = format!(
             "return window.interpreter.scrollTo({}, {});",
             self.id.0,
-            serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior")
+            serde_json::to_string(&options).expect("Failed to serialize ScrollToOptions")
         );
         let webview = self
             .webview

+ 54 - 2
packages/html/src/events/mounted.rs

@@ -34,7 +34,7 @@ pub trait RenderedElementBacking: std::any::Any {
     /// Scroll to make the element visible
     fn scroll_to(
         &self,
-        _behavior: ScrollBehavior,
+        _options: ScrollToOptions,
     ) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
         Box::pin(async { Err(MountedError::NotSupported) })
     }
@@ -72,6 +72,46 @@ pub enum ScrollBehavior {
     Smooth,
 }
 
+/// The desired final position within the scrollable ancestor container for a given axis.
+#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
+#[doc(alias = "ScrollIntoViewOptions")]
+pub enum ScrollLogicalPosition {
+    /// Aligns the element's start edge (top or left) with the start of the scrollable container,
+    /// making the element appear at the start of the visible area.
+    #[cfg_attr(feature = "serialize", serde(rename = "start"))]
+    Start,
+    /// Aligns the element at the center of the scrollable container,
+    /// positioning it in the middle of the visible area.
+    #[cfg_attr(feature = "serialize", serde(rename = "center"))]
+    Center,
+    /// Aligns the element's end edge (bottom or right) with the end of the scrollable container,
+    /// making the element appear at the end of the visible area
+    #[cfg_attr(feature = "serialize", serde(rename = "end"))]
+    End,
+    /// Scrolls the element to the nearest edge in the given axis.
+    /// This minimizes the scrolling distance.
+    #[cfg_attr(feature = "serialize", serde(rename = "nearest"))]
+    Nearest,
+}
+
+/// The way that scrolling should be performed
+#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
+#[doc(alias = "ScrollIntoViewOptions")]
+pub struct ScrollToOptions {
+    pub behavior: ScrollBehavior,
+    pub vertical: ScrollLogicalPosition,
+    pub horizontal: ScrollLogicalPosition,
+}
+impl Default for ScrollToOptions {
+    fn default() -> Self {
+        Self {
+            behavior: ScrollBehavior::Smooth,
+            vertical: ScrollLogicalPosition::Start,
+            horizontal: ScrollLogicalPosition::Center,
+        }
+    }
+}
+
 /// An Element that has been rendered and allows reading and modifying information about it.
 ///
 /// Different platforms will have different implementations and different levels of support for this trait. Renderers that do not support specific features will return `None` for those queries.
@@ -125,7 +165,19 @@ impl MountedData {
         &self,
         behavior: ScrollBehavior,
     ) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
-        self.inner.scroll_to(behavior)
+        self.inner.scroll_to(ScrollToOptions {
+            behavior,
+            ..ScrollToOptions::default()
+        })
+    }
+
+    /// Scroll to make the element visible
+    #[doc(alias = "scrollIntoView")]
+    pub fn scroll_to_with_options(
+        &self,
+        options: ScrollToOptions,
+    ) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
+        self.inner.scroll_to(options)
     }
 
     /// Scroll to the given element offsets

+ 1 - 1
packages/interpreter/src/js/hash.txt

@@ -1 +1 @@
-[6449103750905854967, 17669692872757955279, 13069001215487072322, 11420464406527728232, 3770103091118609057, 5444526391971481782, 15257049569425934744, 5052021921702764563, 10988859153374944111, 16153602427306015669]
+[6449103750905854967, 17669692872757955279, 13069001215487072322, 11420464406527728232, 3770103091118609057, 5444526391971481782, 15100726369461302769, 5052021921702764563, 10988859153374944111, 16153602427306015669]

+ 1 - 1
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;i<options.length;i++)values.push(options[i].value);return values}function serializeEvent(event,target){let contents={},extend=(obj)=>contents={...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:event.changedTouches,target_touches:event.targetTouches,touches: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 serializeMouseEvent(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,offset_x:event.offsetX,offset_y:event.offsetY,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey}}function serializeKeyboardEvent(event){return{char_code:event.charCode,is_composing:event.isComposing,key:event.key,alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,key_code:event.keyCode,shift_key:event.shiftKey,location:event.location,repeat:event.repeat,which:event.which,code:event.code}}function serializeAnimationEvent(event){return{animation_name:event.animationName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement}}function serializeDragEvent(event){let files=void 0;if(event.dataTransfer&&event.dataTransfer.files&&event.dataTransfer.files.length>0)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;editsPath;eventsPath;kickStylesheets;queuedBytes=[];liveview;constructor(editsPath,eventsPath){super();this.editsPath=editsPath,this.eventsPath=eventsPath,this.kickStylesheets=!1}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,behavior){let node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollIntoView({behavior}),!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 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;i<array.length;i++){let end=array[i];for(node=node.firstChild;end>0;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<many;k++)root.appendChild(els[k])}handleEvent(event,name,bubbles){let target=event.target,realId=getTargetId(target),contents=serializeEvent(event,target),body={name,data:contents,element:realId,bubbles};if(this.preventDefaults(event),this.liveview){if(target instanceof HTMLInputElement&&(event.type==="change"||event.type==="input")){if(target.getAttribute("type")==="file"){this.readFiles(target,contents,bubbles,realId,name);return}}}let response=this.sendSerializedEvent(body);if(response){if(response.preventDefault)event.preventDefault();else if(target instanceof Element&&event.type==="click")this.handleClickNavigate(event,target);if(response.stopPropagation)event.stopPropagation()}}sendSerializedEvent(body){if(this.liveview){let message=this.serializeIpcMessage("user_event",body);this.ipc.postMessage(message)}else return handleVirtualdomEventSync(this.eventsPath,JSON.stringify(body))}preventDefaults(event){if(event.type==="submit")event.preventDefault()}handleClickNavigate(event,target){if(!this.intercept_link_redirects)return;let form=target.closest("form");if(target.tagName==="BUTTON"&&(event.type=="submit"||form))event.preventDefault();let a_element=target.closest("a");if(a_element){event.preventDefault();let href=a_element.getAttribute("href");if(href!==""&&href!==null&&href!==void 0)this.ipc.postMessage(this.serializeIpcMessage("browser_open",{href}))}}enqueueBytes(bytes){this.queuedBytes.push(bytes)}flushQueuedBytes(){let byteArray=this.queuedBytes;this.queuedBytes=[];for(let bytes of byteArray)this.run_from_bytes(bytes)}rafEdits(headless,bytes){if(headless)this.run_from_bytes(bytes),this.waitForRequest(headless);else this.enqueueBytes(bytes),requestAnimationFrame(()=>{this.flushQueuedBytes(),this.waitForRequest(headless)})}waitForRequest(headless){fetch(new Request(this.editsPath)).then((response)=>response.arrayBuffer()).then((bytes)=>{this.rafEdits(headless,bytes)})}kickAllStylesheetsOnPage(){let stylesheets=document.querySelectorAll("link[rel=stylesheet]");for(let i=0;i<stylesheets.length;i++){let sheet=stylesheets[i],splitByQuery=sheet.href.split("?"),url=splitByQuery[0],query=splitByQuery[1];if(!query)query="";let queryParams=new URLSearchParams(query);queryParams.delete("dx_force_reload"),queryParams.append("dx_force_reload",Math.random().toString()),sheet.href=`${url}?${queryParams}`}}async readFiles(target,contents,bubbles,realId,name){let files=target.files,file_contents={};for(let i=0;i<files.length;i++){let file=files[i];file_contents[file.name]=Array.from(new Uint8Array(await file.arrayBuffer()))}contents.files={files:file_contents};let message=this.sendSerializedEvent({name,element:realId,data:contents,bubbles});this.ipc.postMessage(message)}}function handleVirtualdomEventSync(endpoint,contents){let xhr=new XMLHttpRequest;xhr.open("POST",endpoint,!1),xhr.setRequestHeader("Content-Type","application/json");let contents_bytes=new TextEncoder().encode(contents),contents_base64=btoa(String.fromCharCode.apply(null,contents_bytes));return xhr.setRequestHeader("dioxus-data",contents_base64),xhr.send(),JSON.parse(xhr.responseText)}function getTargetId(target){if(!(target instanceof Node))return null;let ourTarget=target,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)}export{NativeInterpreter};
+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;i<options.length;i++)values.push(options[i].value);return values}function serializeEvent(event,target){let contents={},extend=(obj)=>contents={...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:event.changedTouches,target_touches:event.targetTouches,touches: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 serializeMouseEvent(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,offset_x:event.offsetX,offset_y:event.offsetY,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey}}function serializeKeyboardEvent(event){return{char_code:event.charCode,is_composing:event.isComposing,key:event.key,alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,key_code:event.keyCode,shift_key:event.shiftKey,location:event.location,repeat:event.repeat,which:event.which,code:event.code}}function serializeAnimationEvent(event){return{animation_name:event.animationName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement}}function serializeDragEvent(event){let files=void 0;if(event.dataTransfer&&event.dataTransfer.files&&event.dataTransfer.files.length>0)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;editsPath;eventsPath;kickStylesheets;queuedBytes=[];liveview;constructor(editsPath,eventsPath){super();this.editsPath=editsPath,this.eventsPath=eventsPath,this.kickStylesheets=!1}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 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;i<array.length;i++){let end=array[i];for(node=node.firstChild;end>0;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<many;k++)root.appendChild(els[k])}handleEvent(event,name,bubbles){let target=event.target,realId=getTargetId(target),contents=serializeEvent(event,target),body={name,data:contents,element:realId,bubbles};if(this.preventDefaults(event),this.liveview){if(target instanceof HTMLInputElement&&(event.type==="change"||event.type==="input")){if(target.getAttribute("type")==="file"){this.readFiles(target,contents,bubbles,realId,name);return}}}let response=this.sendSerializedEvent(body);if(response){if(response.preventDefault)event.preventDefault();else if(target instanceof Element&&event.type==="click")this.handleClickNavigate(event,target);if(response.stopPropagation)event.stopPropagation()}}sendSerializedEvent(body){if(this.liveview){let message=this.serializeIpcMessage("user_event",body);this.ipc.postMessage(message)}else return handleVirtualdomEventSync(this.eventsPath,JSON.stringify(body))}preventDefaults(event){if(event.type==="submit")event.preventDefault()}handleClickNavigate(event,target){if(!this.intercept_link_redirects)return;let form=target.closest("form");if(target.tagName==="BUTTON"&&(event.type=="submit"||form))event.preventDefault();let a_element=target.closest("a");if(a_element){event.preventDefault();let href=a_element.getAttribute("href");if(href!==""&&href!==null&&href!==void 0)this.ipc.postMessage(this.serializeIpcMessage("browser_open",{href}))}}enqueueBytes(bytes){this.queuedBytes.push(bytes)}flushQueuedBytes(){let byteArray=this.queuedBytes;this.queuedBytes=[];for(let bytes of byteArray)this.run_from_bytes(bytes)}rafEdits(headless,bytes){if(headless)this.run_from_bytes(bytes),this.waitForRequest(headless);else this.enqueueBytes(bytes),requestAnimationFrame(()=>{this.flushQueuedBytes(),this.waitForRequest(headless)})}waitForRequest(headless){fetch(new Request(this.editsPath)).then((response)=>response.arrayBuffer()).then((bytes)=>{this.rafEdits(headless,bytes)})}kickAllStylesheetsOnPage(){let stylesheets=document.querySelectorAll("link[rel=stylesheet]");for(let i=0;i<stylesheets.length;i++){let sheet=stylesheets[i],splitByQuery=sheet.href.split("?"),url=splitByQuery[0],query=splitByQuery[1];if(!query)query="";let queryParams=new URLSearchParams(query);queryParams.delete("dx_force_reload"),queryParams.append("dx_force_reload",Math.random().toString()),sheet.href=`${url}?${queryParams}`}}async readFiles(target,contents,bubbles,realId,name){let files=target.files,file_contents={};for(let i=0;i<files.length;i++){let file=files[i];file_contents[file.name]=Array.from(new Uint8Array(await file.arrayBuffer()))}contents.files={files:file_contents};let message=this.sendSerializedEvent({name,element:realId,data:contents,bubbles});this.ipc.postMessage(message)}}function handleVirtualdomEventSync(endpoint,contents){let xhr=new XMLHttpRequest;xhr.open("POST",endpoint,!1),xhr.setRequestHeader("Content-Type","application/json");let contents_bytes=new TextEncoder().encode(contents),contents_base64=btoa(String.fromCharCode.apply(null,contents_bytes));return xhr.setRequestHeader("dioxus-data",contents_base64),xhr.send(),JSON.parse(xhr.responseText)}function getTargetId(target){if(!(target instanceof Node))return null;let ourTarget=target,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)}export{NativeInterpreter};

+ 2 - 2
packages/interpreter/src/ts/native.ts

@@ -106,10 +106,10 @@ export class NativeInterpreter extends JSChannel_ {
     return JSON.stringify({ method, params });
   }
 
-  scrollTo(id: NodeId, behavior: ScrollBehavior): boolean {
+  scrollTo(id: NodeId, options: ScrollIntoViewOptions): boolean {
     const node = this.nodes[id];
     if (node instanceof HTMLElement) {
-      node.scrollIntoView({ behavior });
+      node.scrollIntoView(options);
       return true;
     }
     return false;

+ 2 - 2
packages/liveview/src/element.rs

@@ -72,12 +72,12 @@ impl RenderedElementBacking for LiveviewElement {
 
     fn scroll_to(
         &self,
-        behavior: dioxus_html::ScrollBehavior,
+        options: dioxus_html::ScrollToOptions,
     ) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
         let script = format!(
             "return window.interpreter.scrollTo({}, {});",
             self.id.0,
-            serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior")
+            serde_json::to_string(&options).expect("Failed to serialize ScrollToOptions")
         );
 
         let fut = self.query.new_query::<bool>(&script).resolve();

+ 17 - 9
packages/web/src/events/mounted.rs

@@ -66,17 +66,25 @@ impl dioxus_html::RenderedElementBacking for Synthetic<web_sys::Element> {
 
     fn scroll_to(
         &self,
-        behavior: dioxus_html::ScrollBehavior,
+        input_options: dioxus_html::ScrollToOptions,
     ) -> std::pin::Pin<Box<dyn std::future::Future<Output = dioxus_html::MountedResult<()>>>> {
         let options = web_sys::ScrollIntoViewOptions::new();
-        match behavior {
-            dioxus_html::ScrollBehavior::Instant => {
-                options.set_behavior(web_sys::ScrollBehavior::Instant);
-            }
-            dioxus_html::ScrollBehavior::Smooth => {
-                options.set_behavior(web_sys::ScrollBehavior::Smooth);
-            }
-        }
+        options.set_behavior(match input_options.behavior {
+            dioxus_html::ScrollBehavior::Instant => web_sys::ScrollBehavior::Instant,
+            dioxus_html::ScrollBehavior::Smooth => web_sys::ScrollBehavior::Smooth,
+        });
+        options.set_block(match input_options.vertical {
+            dioxus_html::ScrollLogicalPosition::Start => web_sys::ScrollLogicalPosition::Start,
+            dioxus_html::ScrollLogicalPosition::Center => web_sys::ScrollLogicalPosition::Center,
+            dioxus_html::ScrollLogicalPosition::End => web_sys::ScrollLogicalPosition::End,
+            dioxus_html::ScrollLogicalPosition::Nearest => web_sys::ScrollLogicalPosition::Nearest,
+        });
+        options.set_inline(match input_options.horizontal {
+            dioxus_html::ScrollLogicalPosition::Start => web_sys::ScrollLogicalPosition::Start,
+            dioxus_html::ScrollLogicalPosition::Center => web_sys::ScrollLogicalPosition::Center,
+            dioxus_html::ScrollLogicalPosition::End => web_sys::ScrollLogicalPosition::End,
+            dioxus_html::ScrollLogicalPosition::Nearest => web_sys::ScrollLogicalPosition::Nearest,
+        });
         self.event
             .scroll_into_view_with_scroll_into_view_options(&options);