Skip to content

Instantly share code, notes, and snippets.

@phcorp
Last active February 3, 2024 18:46
Show Gist options
  • Save phcorp/85e061e0fcd2755d8cda09565fc9fb51 to your computer and use it in GitHub Desktop.
Save phcorp/85e061e0fcd2755d8cda09565fc9fb51 to your computer and use it in GitHub Desktop.
Emulate dragging on a desktop device
const IGNORED_SELECTOR = 'input, label, select, textarea, button, fieldset, legend, datalist, output, option, optgroup';
const SCROLLABLE_SELECTOR = '.drag-to-scroll';
/**
* Transform mouse drag gesture to scroll.
*
* @param {{
* [ignoredSelector]: string, // The ignored elements selector
* [selector]: string, // The scrollable elements selector
* }}
*
* @return {(function(): void)|*} - Uninstall function
*/
const dragToScroll = ({
ignoredSelector = IGNORED_SELECTOR,
selector = SCROLLABLE_SELECTOR,
}) => {
/**
* Returns the scrollable element from a child element.
*
* @param {HTMLElement|null} element - The child element
*
* @return {null|HTMLElement} - The element
*/
const getScrollableElement = (element) => {
if (element?.matches(ignoredSelector) || element?.closest(ignoredSelector)) {
return null;
}
if (element?.matches(selector)) {
return element;
}
const parentScrollableElement = element?.closest(selector);
if (parentScrollableElement) {
return parentScrollableElement;
}
return null;
};
const mouseupListener = (event) => {
const scrollableElement = getScrollableElement(event.target || event.detail.target);
if (!scrollableElement ||scrollableElement.dataset.dragging !== 'true') {
return;
}
scrollableElement.dataset.dragging = 'false';
scrollableElement.dispatchEvent(new CustomEvent('scrollend', {
detail: {
target: scrollableElement,
},
}));
event.preventDefault();
event.stopPropagation();
};
const mousedownListener = (event) => {
const scrollableElement = getScrollableElement(event.target);
if (!scrollableElement || getScrollableElement(document.elementFromPoint(event.pageX, event.pageY)) !== scrollableElement) {
return;
}
scrollableElement.dataset.dragging = 'true';
scrollableElement.dataset.draggingLastClientX = `${event.clientX}`;
scrollableElement.dataset.draggingLastClienty = `${event.clientY}`;
scrollableElement.addEventListener('mouseleave', (event) => {
mouseupListener(new CustomEvent('mouseup', {
bubbles: true,
detail: {
target: scrollableElement,
},
}));
}, {
capture: false,
once: true,
});
event.preventDefault();
event.stopPropagation();
};
const mousemoveListener = (event) => {
const scrollableElement = getScrollableElement(event.target);
if (!scrollableElement || scrollableElement.dataset.dragging !== 'true') {
return;
}
const lastClientX = parseInt(scrollableElement.dataset.draggingLastClientX, 10);
const lastClientY = parseInt(scrollableElement.dataset.draggingLastClienty, 10);
const clientX = event.clientX - lastClientX;
const clientY = event.clientY - lastClientY;
scrollableElement.scrollLeft -= clientX;
scrollableElement.scrollTop -= clientY;
scrollableElement.dataset.draggingLastClientX = `${event.clientX}`;
scrollableElement.dataset.draggingLastClienty = `${event.clientY}`;
if (scrollableElement === document.body) {
document.documentElement.scrollLeft -= clientX;
document.documentElement.scrollTop -= clientY;
}
scrollableElement.dispatchEvent(new CustomEvent('scroll', {
detail: {
target: scrollableElement,
},
}));
};
const mouseleaveListener = (event) => {
if(event.clientY <= 0 || event.clientX <= 0 || (event.clientX >= window.innerWidth || event.clientY >= window.innerHeight)) {
const pageX = Math.max(0, Math.min(window.innerWidth - 1, event.pageX));
const pageY = Math.max(0, Math.min(window.innerHeight - 1, event.pageY));
const scrollableElement = getScrollableElement(document.elementFromPoint(pageX, pageY));
if (!scrollableElement) {
return;
}
mouseupListener(new CustomEvent('mouseup', {
bubbles: true,
detail: {
target: scrollableElement,
},
}));
}
};
document.addEventListener('mouseleave', mouseleaveListener, {
capture: false,
});
document.addEventListener('mouseup', mouseupListener, {
capture: true,
});
document.addEventListener('mousemove', mousemoveListener, {
capture: false,
});
document.addEventListener('mousedown', mousedownListener, {
capture: true,
});
const style = document.createElement('style');
style.innerHTML = `
body {
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' width='24'%3E%3Ccircle cx='12' cy='12' r='10' fill='rgba(0, 0, 0, .5)' stroke='rgba(255, 255, 255, .5)' stroke-width='2'%3E%3C/circle%3E%3C/svg%3E") 12 12, default;
a {
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' width='24'%3E%3Ccircle cx='12' cy='12' r='10' fill='rgba(0, 0, 0, .5)' stroke='rgba(255, 255, 255, .5)' stroke-width='2'%3E%3C/circle%3E%3C/svg%3E") 12 12, default;
}
}
`;
document.body.appendChild(style);
return () => {
document.removeEventListener('mousedown', mousedownListener, {
capture: true,
});
document.removeEventListener('mousemove', mousemoveListener, {
capture: false,
});
document.removeEventListener('mouseup', mouseupListener, {
capture: true,
});
document.removeEventListener('mouseleave', mouseleaveListener, {
capture: false,
});
document.body.removeChild(style);
document.querySelectorAll(selector).forEach((scrollableElement) => {
delete scrollableElement.dataset.dragging;
delete scrollableElement.dataset.draggingLastClientX;
delete scrollableElement.dataset.draggingLastClienty;
});
};
};
export default dragToScroll;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment