diff --git a/_posts/2024-02-21-css-iframe-scaling.md b/_posts/2024-02-21-css-iframe-scaling.md index 4d6e24d43de97..57e5b8932a633 100644 --- a/_posts/2024-02-21-css-iframe-scaling.md +++ b/_posts/2024-02-21-css-iframe-scaling.md @@ -8,7 +8,22 @@ bit with CSS to explore my options a bit. I sometimes want to embed a web page within another so that I can give a complementary -view of the content. The ingredients of this are +view of the content. +And I want to be able to resize the web page instead of having it a fixed size. +It is a bit tricky to do this in a way that works for all browsers: phones and tablets are tricky! + +*[Note: After originally posting this page, I had to come back and add a whole other +section "The second attempt" becaues it did not work for touchscreens. +And, in the end, I am a bit frustrated with how much code I had to write to make this work.]* + +## The first attempt + +My first attempt comes pretty close. It works great on desktops and laptops but not on ipads. +But I will go through it in detail because it is mostly just standard CSS and HTML with just a small amount +of Javascript and because it is the starting point for the second (working) attempt. +(The part of this that will have to change is the use of "resize" in the final CSS.) + +The ingredients of the first attempt are - Use an iframe to embed the web page. @@ -93,7 +108,7 @@ view of the content. The ingredients of this are +This approach has two problems. + +1. It does not work on iOS (Safari or Chrome) because the resize handle does not show up. +2. Even on a laptop, it is not that great because the resize handle is at the bottom corner of the left-hand box. + There is no way to move the handle so that dragging anywhere on the vertical divider resizes the box. + +## The second attempt + +The second attempt is based on two articles by [Phuoc Nguyen](https://phuoc.ng/) +that implement a similar interface by creating three boxes: a left-hand side, +a 2px-wide box that is used as a dragbar, and a right-hand side. +The [first article](https://phuoc.ng/collection/html-dom/create-resizable-split-views/) uses plain Javascript/typescript +while the [second article](https://phuoc.ng/collection/react-drag-drop/create-resizable-split-views/) uses React. + +The important thing here is that, because we implement the dragbar ourselves, we have complete control over +its appearance and, critically, we can add support for [touch events](https://developer.mozilla.org/en-US/docs/Web/API/Touch_events) +to fix the problems on touchscreen devices. + +- The HTML is similar to the first version except that, this time, + we have an additional "div" that is used to implement the dragbar. + +
+
+ Left +
+
+
+
+ +
+
+ +- The basics of the CSS support are basically the same as in the first attempt + except that now we have to specify the style of the dragbar. + It + has an off-gray color, + goes the full height, + is two "pixels" wide, + the cursor changes to a left-right arrow when it is over the dragbar, + and both user selection and touch action are initially off. + + + + (The reason I put quotations round "pixels" above is that, apparently, + the "px" measurement used in CSS is [really an angular measurement](http://inamidst.com/stuff/notes/csspx).) + +- The big difference between the two approaches is that we need to write quite a lot + of Javascript because we have to implement all the dragbar behavior ourselves. + + - The entire block of code is only executed after all the HTML has been + loaded - this makes the code less sensitive to whether the script is + loaded in the page header or embedded somewhere in the page. + + Also, the first thing we do is extract all the key elements that + we need to refer to. + + + + - When the dragbar is moved, we need to adjust the width of the + left-hand box. (This will be used for both mouse and touch events.) + + const updateWidth = (leftWidth, dx) => { + const newLeftWidth = (leftWidth + dx); + leftSide.style.width = `${newLeftWidth}px`; + }; + + - And we also have code to add and remove the cursor, + user selection and pointer events. + (This is explained in Phuoc Ng's original articles.) + + const updateCursor = () => { + resizer.style.cursor = 'ew-resize'; + document.body.style.cursor = 'ew-resize'; + leftSide.style.userSelect = 'none'; + leftSide.style.pointerEvents = 'none'; + rightSide.style.userSelect = 'none'; + rightSide.style.pointerEvents = 'none'; + }; + + const resetCursor = () => { + resizer.style.removeProperty('cursor'); + document.body.style.removeProperty('cursor'); + leftSide.style.removeProperty('user-select'); + leftSide.style.removeProperty('pointer-events'); + rightSide.style.removeProperty('user-select'); + rightSide.style.removeProperty('pointer-events'); + }; + + - With those helper functions in place, we can now define what + is to happen on mouse-down, mouse-move and mouse-up events. + + On mouse-down, we record where the slider is and where the cursor is. + On mouse-move, we call "updateWidth" to resize the left-hand box. + (All the other boxes will be updated by the HTML layout engine.) + On mouse-up, we stop tracking the mouse. + + const mouseDownHandler = function (e) { + const startx = e.clientX; + const leftWidth = leftSide.getBoundingClientRect().width; + + const mouseMoveHandler = function (e) { + updateWidth(leftWidth, e.clientX - startx); + updateCursor(); + }; + + const mouseUpHandler = function () { + resetCursor(); + document.removeEventListener('mousemove', mouseMoveHandler); + document.removeEventListener('mouseup', mouseUpHandler); + }; + + // Attach the listeners to document + document.addEventListener('mousemove', mouseMoveHandler); + document.addEventListener('mouseup', mouseUpHandler); + }; + + resizer.addEventListener('mousedown', mouseDownHandler); + + - Handling touch events is very similar except that we deal with + the touchstart, touchmove and touchend events. + + const touchStartHandler = function (e) { + const touch = e.touches[0]; + const startx = touch.clientX; + const leftWidth = leftSide.getBoundingClientRect().width; + + const touchMoveHandler = function (e) { + const touch = e.touches[0]; + updateWidth(leftWidth, touch.clientX - startx); + updateCursor(); + }; + + const touchEndHandler = function () { + resetCursor(); + document.removeEventListener('touchmove', mouseMoveHandler); + document.removeEventListener('touchend', mouseUpHandler); + }; + + // Attach the listeners to document + document.addEventListener('touchmove', touchMoveHandler); + document.addEventListener('touchend', touchEndHandler); + }; + resizer.addEventListener('touchstart', touchStartHandler); + + - And, finally, we have the resize observer to rescale the iframe whenever + the box containing it is resized. + + const resizeObserver2 = new ResizeObserver((entries) => { + for (const entry of entries) { + const outer = entry.target; + const inner = outer.children[0]; + const window_scale = outer.clientWidth / 800; + inner.style.transform = `scale(${window_scale})`; + inner.height = window.innerHeight / window_scale; + } + }); + resizeObserver2.observe(iframe_box2); + + (Incidentally, as I was debugging this, I was stuck for ages on why scaling + was not working properly. It turns out that I was trying to rescale the + iframe when the size of the iframe changed and this did not work because + the iframe does not change size just because its parent box has changed size. + Watching its parent box fixed this.) + +With all that code in place, we finally have a resizable box --- you can drag the +dragbar between the two boxes and drag it from left to right and watch the +embedded web page scale itself to fit the available space. + + + +
+
+Left +
+
+
+
+ +
+
+ + + +So, now that I have something that works, what am I going to do with it? +To be honest, I am a bit disturbed by the complexity of the solution +and by the amount of code I left out that would have made the solution +more flexible and robust. +I suspect that the right solution would have been to find an existing +library and use that.