While browsing I was wondering how difficult it would be to implement a draggable before/after widget. My HTML/js knowledge was pretty rusty but I thought it can't be too hard. Turns out I was right! Here is how I created a simple, dependency-free[1] HTML before/after widget in a few hours.
We Start with this simple HTML.
<body> <h1>Before after comparisons</h1> <div class="ycBeforeAfter" style="position: relative; display: inline-block;"> <p>Before</p> <img src="before.png" alt="Before" width="500px" height="500px" /> <p>After</p> <img src="after.png" alt="After"> </div> </body>
The Idea is to iterate all elements on the page with the class ycBeforeAfter
and enable the behavior for those pics.
We assume, that the pictures are exactly the same size. The before pic will be on the left and the after pic on the right.
All Elements except the first two images within the ycBeforeAfter
Element will be dropped.
But Why did I bother to write the before and after paragraphs then?
I would like the page to work without js as well. So If your browser doesn't support js or you have js disabled you would simply see the images with the descriptions after one another.
The first version of the JavaScript file simply places the after pic directly on top of the before pic:
var containerDivs = document.getElementsByClassName("ycBeforeAfter"); //We could have multiple before/after comparisons on the page. for (var i = 0; i < containerDivs.length; i++) { beforeAfter(containerDivs[i]); } function beforeAfter(containerDiv){ const pics = containerDiv.querySelectorAll("img"); var beforePic = pics[0]; var afterPic = pics[1]; var picWidth = beforePic.width var picHeight = beforePic.height //Clear the container first while(containerDiv.hasChildNodes()) { containerDiv.removeChild(containerDiv.lastChild); } afterPic.style.position = "relative"; afterPic.zIndex = 1; afterPic.classList.add("afterPic"); containerDiv.appendChild(afterPic); beforePic.style.position = "absolute"; //We want crop the image instead of resizing it. when we later change the size beforePic.style.objectFit ="cover"; beforePic.style.objectPosition="left top"; beforePic.style.top = 0; beforePic.style.left = 0; beforePic.height = beforePic.height; beforePic.zIndex = 2; beforePic.classList.add("beforePic"); containerDiv.appendChild(beforePic);
Now, it seems counter-intuitive to place the "before pic" second but this will make the logic much easier. The only thing we have to do later is change the beforePic's width.
We want to create a draggable divider now. I went with a simple, single-colored bar.
//... function createDivider(imgHeight){ var divider = document.createElement("div"); divider.classList.add("ycBeforeAfterDivider"); divider.style.position = "absolute"; divider.style.top=0; divider.style.zIndex = 10; divider.style.width = dividerSize; //global var divider.style.height = imgHeight + "px"; divider.style.backgroundColor = dividerColor; // global var return divider; } //...
Now lets write a helper function to move the divider element and resize the before pic at the same time:
function setDividerPos(container,newX){ var divider = container.querySelector(".ycBeforeAfterDivider"); dividerXPos = newX; //Checks to make sure we don't move the divider out of bounds if(dividerXPos > container.clientWidth){ dividerXPos = container.clientWidth; } if(dividerXPos < 0) { dividerXPos = 0; } divider.style.left = dividerXPos + "px"; beforePic = container.querySelector(".beforePic"); beforePic.width = dividerXPos; }
We can already use the widget via the console now, of course this is pretty impractical.
So lets add the drag functionality.
We will create 3 handler functions: onContainerMouseDown
, onDividerDrag
and onStopDragging
.
The mouse events will be attached to the container element.
The first function is called on "mousedown" and attaches the other handlers to the container.
It also sets the divider position once. So when a random position in the image is clicked the divider will jump there.
The onDividerDrag
function is called on "mousemove" and only updates the divider position. The onStopDragging
function simply removes the "mousemove" and "mouseup" handlers on "mouseup".
function beforeAfter(containerDiv){ //.. previous code unchanged containerDiv.onmousedown = onContainerMouseDown; } function onContainerMouseDown(e){ if(e.buttons != 1){ // Ignore non-left mouse button clicks return } var container = e.target.parentNode; e.preventDefault(); container.onmousemove = onDividerDrag container.onmouseup = onStopDragging var newDividerX = e.pageX - container.offsetLeft - Math.round((dividerSize /2)) setDividerPos(container,newDividerX); } function onDividerDrag(e){ var container = e.target.parentNode e.preventDefault() var newDividerX = e.pageX - container.offsetLeft - Math.round((dividerSize /2)) setDividerPos(container,newDividerX); } function onStopDragging(e){ e.target.parentNode.onmousemove = null e.target.parentNode.onmouseup = null }
I added a few more lines so we don't have to let go of the divider inside of the container bounds:
document.onmouseup = function (e) { for(i=0;i<containerDivs.length;i++){ containerDivs[i].onmousemove = null containerDivs[i].onmouseup = null } }
All desktop browsers should be supported now but this does not work on mobile/touch devices yet. Supporting touch devices isn't too hard, we only need handle different inputs. Touch events are more complicated then normal mouse events though, so the code is slightly different:
function beforeAfter(containerDiv){ //previous code unchanged containerDiv.addEventListener('touchstart', onTouchStart, false); } function onTouchStart(e){ if(e.targetTouches.length != 1){ // Only interpret single finger touches return } var container = e.targetTouches[0].target.parentNode; container.addEventListener('touchmove', onTouchMove, false); container.addEventListener('touchend', onTouchEnd, false); container.addEventListener('touchcancel', onTouchEnd, false); var newDividerX = e.targetTouches[0].pageX - container.offsetLeft - Math.round((dividerSize /2)) setDividerPos(container,newDividerX); e.preventDefault(); } function onTouchMove(e){ var container = e.targetTouches[0].target.parentNode var newDividerX = e.targetTouches[0].pageX - container.offsetLeft - Math.round((dividerSize /2)) setDividerPos(container,newDividerX); e.preventDefault() } function onTouchEnd(e) { var container = e.target.parentNode container.removeEventListener('touchmove', onTouchMove); container.removeEventListener('touchend', onTouchEnd); container.removeEventListener('touchcancel', onTouchEnd); container.onmousemove = null container.onmouseup = null }
The next step would be a little refactoring to remove the duplicate code between the input methods. But I am too lazy to do this right now. But I am a fan of the "Rule of three"
This is all the code needed to make a really simple before/after widget. You can see an example in action here .The code can be found on Codeberg.
[1] Well, dependency-free if you don't count the multiple GB Browser you need to use it.