joicomponents
Version:
Design patterns to build native web components
186 lines (150 loc) • 5.86 kB
JavaScript
(function () {
//pure filter function
function filterOnAttribute(e, attributeName) {
for (let el = e.target; el; el = el.parentNode) {
if (!el.hasAttribute)
return null;
if (el.hasAttribute(attributeName))
return el;
}
return null;
}
//dispatch event to replace default action
function dispatchTrailingEvent(target, composedEvent, trigger, unpreventable) {
if (!composedEvent || !target)
return;
if (unpreventable)
setTimeout(function(){target.dispatchEvent(composedEvent);}, 0);
else
setTimeout(function(){trigger.defaultPrevented || target.dispatchEvent(composedEvent);}, 0);
}
//recording start
let recorded = undefined;
let cachedUserSelect = undefined;
const mousemoveListener = e => onMousemove(e);
const mouseupListener = e => onMouseup(e);
const mouseoutListener = e => onMouseout(e);
function startRecordingEvent(e, cancelMouseout) {
recorded = [e];
//capture the mouse pointer (akin to preventing the default action) start
const bodyStyle = document.querySelector("body");
cachedUserSelect = bodyStyle.userSelect;
bodyStyle.userSelect = "none";
//capture the mouse pointer (akin to preventing the default action) end
window.addEventListener("mousemove", mousemoveListener, true);
window.addEventListener("mouseup", mouseupListener, true);
!cancelMouseout && window.addEventListener("mouseout", mouseoutListener, true);
}
function recordEvent(e) {
recorded.push(e);
}
function stopRecordingEvent() {
recorded = undefined;
//release the captured mouse pointer start
document.querySelector("body").style.userSelect = cachedUserSelect;
cachedUserSelect = undefined;
//release the captured mouse pointer end
window.removeEventListener("mousemove", mousemoveListener, true);
window.removeEventListener("mouseup", mouseupListener, true);
window.removeEventListener("mouseout", mouseoutListener, true); //always remove all potential listeners, regardless
}
//specific make event functions
function makeDraggingEvent(name, trigger) {
const composedEvent = new CustomEvent("dragging-" + name, {bubbles: true, composed: true});
composedEvent.x = trigger.x;
composedEvent.y = trigger.y;
return composedEvent;
}
function makeFlingEvent(target, trigger) {
const minDuration = target.hasAttribute("fling-duration") ? parseInt(target.getAttribute("fling-duration")) : 200;
const flingTime = trigger.timeStamp - minDuration;
const flingStart = findLastEventOlderThan(recorded, flingTime);
if (!flingStart)
return;
const detail = flingDetails(trigger, flingStart);
const minDistance = target.hasAttribute("fling-distance") ? parseInt(target.getAttribute("fling-distance")) : 50;
if (detail.distDiag < minDistance)
return;
detail.angle = flingAngle(detail.distX, detail.distY);
return new CustomEvent("fling", {bubbles: true, composed: true, detail});
}
function findLastEventOlderThan(events, timeTest) {
for (let i = events.length - 1; i >= 0; i--) {
if (events[i].timeStamp < timeTest)
return events[i];
}
return null;
}
function flingDetails(flingEnd, flingStart) {
const distX = flingEnd.x - flingStart.x;
const distY = flingEnd.y - flingStart.y;
const distDiag = Math.sqrt(distX * distX + distY * distY);
const durationMs = flingEnd.timeStamp - flingStart.timeStamp;
return {distX, distY, distDiag, durationMs};
}
function flingAngle(x = 0, y = 0) {
return ((Math.atan2(y, -x) * 180 / Math.PI) + 270) % 360;
}
//specific listener functions
function onMousedown(trigger) {
//filter 1
if (recorded) {
const composedEvent = makeDraggingEvent("cancel", trigger);
const target = recorded[0].target;
stopRecordingEvent();
return dispatchTrailingEvent(target, composedEvent, trigger, true);
}
//filter 2
if (trigger.button !== 0)
return;
//filter 3
const newTarget = filterOnAttribute(trigger, "draggable");
if (!newTarget)
return;
trigger.preventDefault();
//make event
const composedEvent = makeDraggingEvent("start", trigger);
//record
startRecordingEvent(composedEvent, newTarget.hasAttribute("draggable-cancel-mouseout"));
//dispatch event
dispatchTrailingEvent(newTarget, composedEvent, trigger, true);
}
function onMousemove(trigger) {
const newTarget = recorded[0].target;
trigger.preventDefault();
//make event
const composedEvent = makeDraggingEvent("move", trigger);
//record
recorded.push(composedEvent);
//dispatch event
dispatchTrailingEvent(newTarget, composedEvent, trigger, true);
}
function onMouseup(trigger) {
const newTarget = recorded[0].target;
trigger.preventDefault();
//make events
const stopEvent = makeDraggingEvent("stop", trigger);
const flingEvent = makeFlingEvent(recorded[0].target, trigger);
//record
stopRecordingEvent();
//dispatch event
dispatchTrailingEvent(newTarget, stopEvent, trigger, true);
dispatchTrailingEvent(newTarget, flingEvent, trigger, true);
}
function onMouseout(trigger) {
//filter
const eY = event.clientY;
const eX = event.clientX;
if(eY > 0 && eX > 0 && eX < window.innerWidth && eY < window.innerHeight)
return; //The mouse is not leaving the window
trigger.preventDefault();
const newTarget = recorded[0].target;
//make events
const cancelEvent = new CustomEvent("dragging-cancel", {bubbles: true, composed: true, triggerEvent: trigger});
//record
stopRecordingEvent();
//dispatch event
dispatchTrailingEvent(newTarget, cancelEvent, trigger, true);
}
window.addEventListener("mousedown", e => onMousedown(e));
})();