Skip to content

Commit

Permalink
Improved multi-touch support
Browse files Browse the repository at this point in the history
* Partially handled #10.
* Still need to work on better mouse event support.
  • Loading branch information
yookoala committed Nov 16, 2023
1 parent 901167d commit 7deb866
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 16 deletions.
84 changes: 75 additions & 9 deletions src/dragdrop-child.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
* @description A draggable element.
*/

import DragDropContainer from "./dragdrop-container";

/**
* @const {HTMLTemplateElement} template
*/
Expand Down Expand Up @@ -255,6 +257,10 @@ export default class DragDropChild extends HTMLElement {

// Find all the dragdrop containers on the page to check if the drag point is witnin
// bound of any of them.

/**
* @type {NodeListOf<DragDropContainer>}
*/
const containers = document.querySelectorAll('dragdrop-container');
const enteredContainers = [];
for (const container of containers) {
Expand All @@ -269,7 +275,13 @@ export default class DragDropChild extends HTMLElement {
} else if (this.#touchCurrentContainer === container) {
// If the touch point is not within the container, but the container is the current
// container, then the touch point has left the container.
this.#touchCurrentContainer.dispatchEvent(new CustomEvent('dnd:dragleave', {bubbles: true}));
this.#touchCurrentContainer.dispatchEvent(new CustomEvent(
'dnd:dragleave',
{
bubbles: true,
detail: { target: this },
},
));
this.#touchCurrentContainer = null;
}
}
Expand All @@ -286,17 +298,36 @@ export default class DragDropChild extends HTMLElement {
});

// Trigger drag enter if the child has just entered this container.
const { clientX, clientY, pageX, pageY } = touch;
if (this.#touchCurrentContainer !== innerMostContainer) {
this.#touchCurrentContainer = innerMostContainer; // remember the container for touchmove.
innerMostContainer.dispatchEvent(new CustomEvent('dnd:dragenter', {bubbles: true}));
innerMostContainer.dispatchEvent(new CustomEvent(
'dnd:dragenter',
{
bubbles: true,
detail: {
target: this,
clientX,
clientY,
pageX,
pageY,
},
},
));
}
// Trigger drag over the innermost container anyway.
innerMostContainer.dispatchEvent(new CustomEvent('dnd:dragover', {bubbles: true, detail: {
clientX: touch.clientX,
clientY: touch.clientY,
pageX: touch.pageX,
pageY: touch.pageY,
}}));
innerMostContainer.dispatchEvent(new CustomEvent(
'dnd:dragover',
{
bubbles: true, detail: {
target: this,
clientX,
clientY,
pageX,
pageY,
},
},
));
}
}

Expand All @@ -310,6 +341,7 @@ export default class DragDropChild extends HTMLElement {
this.removeAttribute('dragging'); // for container to know wich element is being dragged
this.classList.remove('dragging'); // for styling
const touch = event.changedTouches[0];
const withinContainers = [];

// Clear simulation pointers.
if (this.#touchShadow && this.#touchShadow.parentNode) {
Expand All @@ -326,7 +358,41 @@ export default class DragDropChild extends HTMLElement {
if (touch.pageX > boundRect.left && touch.pageX < boundRect.right
&& touch.pageY > boundRect.top && touch.pageY < boundRect.bottom
) {
container.dispatchEvent(new CustomEvent('dnd:drop', {bubbles: true}));
if (!this.isAncestorOf(container)) {
// Only include containers that is not the ancestor of this item.
withinContainers.push(container);
}
}
}

if (withinContainers.length > 0) {
// Find the inner most container
const innerMostContainer = withinContainers.reduce((a, b) => {
const aBoundRect = a.getBoundingClientRect();
const bBoundRect = b.getBoundingClientRect();
if (aBoundRect.width * aBoundRect.height > bBoundRect.width * bBoundRect.height) {
return b;
}
return a;
});

// Trigger drag enter if the child has just entered this container.
const { clientX, clientY, pageX, pageY } = touch;
if (this.#touchCurrentContainer !== innerMostContainer) {
this.#touchCurrentContainer = innerMostContainer; // remember the container for touchmove.
innerMostContainer.dispatchEvent(new CustomEvent(
'dnd:drop',
{
bubbles: true,
detail: {
target: this,
clientX,
clientY,
pageX,
pageY,
},
},
));
}
}

Expand Down
29 changes: 22 additions & 7 deletions src/dragdrop-container.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
* @description A container that can accept dragdrop-child.
*/

import DragDropChild from "./dragdrop-child";

/**
* @const {HTMLTemplateElement} template
*/
Expand Down Expand Up @@ -67,6 +69,9 @@ export default class DragDropContainer extends HTMLElement {
canAcceptChild(child, alreadyChildren = false) {
const childrenLength = alreadyChildren ? this.children.length - 1 : this.children.length;
const result = (
// The child cannot be this container.
(child !== this) &&

// The child cannot be the ancestor (i.e. nested parent)
// of this container.
(!child.isAncestorOf || !child.isAncestorOf(this)) &&
Expand Down Expand Up @@ -110,7 +115,12 @@ export default class DragDropContainer extends HTMLElement {
onDragOver(event) {
event.preventDefault();
event.stopPropagation();
const dragged = this.getDraggedElement();

// Note
// 1. dragover event will provide the dragged element as event.target.
// 2. dnd:dragover will provide the container as event.target (because it is
// dispatched against the container) with dragged element as event.detail.target.
const dragged = event?.detail?.target || this.getDraggedElement();
if (!dragged) {
return;
}
Expand All @@ -123,7 +133,7 @@ export default class DragDropContainer extends HTMLElement {

// Use either the clientY or the detail.clientY value.
// Interoperable with both mouse and touch events.
const closest = this.getDragBeforeElement(dragged, event.clientY || event?.detail.clientY);
const closest = this.getDragBeforeElement(dragged, event?.detail?.clientY || event?.clientY);
if (closest.element) {
closest.element.before(dragged);
return;
Expand All @@ -143,21 +153,26 @@ export default class DragDropContainer extends HTMLElement {

// Clear the active style.
this.classList.remove('active');
const dragged = this.getDraggedElement();

// Note
// 1. dragover event will provide the dragged element as event.target.
// 2. dnd:dragover will provide the container as event.target (because it is
// dispatched against the container) with dragged element as event.detail.target.
const dragged = event?.detail?.target || this.getDraggedElement();
if (!dragged) {
return;
}

if (!this.canAcceptChild(dragged, true)) {
// If the dragged item is an ancestor of this container,
// ignore the drop event.
dragged?.bounce(); // dragdrop-child supports bounce() method.
if (dragged instanceof DragDropChild) {
dragged.bounce();
}
return;
}

// Get the closest element after the drop point.
// Put the dropped element before that element.
const closest = this.getDragBeforeElement(dragged, event.clientY);
const closest = this.getDragBeforeElement(dragged, event?.detail?.clientY || event?.clientY);
if (closest.element) {
closest.element.before(dragged);
return;
Expand Down

0 comments on commit 7deb866

Please sign in to comment.