Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Click n drag #286

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions mouseevents/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Examples:

* [ClickNDrag program](https://mdn.github.com/dom-examples/mouseevents/Click_N_Drag.html). This illustrates clicking and dragging of objects on a web page, without the Drag & Drop API.


57 changes: 57 additions & 0 deletions mouseevents/click-and-drag/Click_N_Drag.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/* without these, the page ends after the content.
I want to drag all over the page. */
html, body, main.arena {
width: 100%;
height: 100%;
}

html {
overflow: hidden;
background: white;
}

/* wrapper for either a physicist or a slot */
li {
list-style-type: none;
width: 400px;
padding: 2px;
margin: 13px;
border-radius: 5px;
}

/* a target for mousedown */
.physicist {
cursor: grab; // tells user they can grab it
line-height: 20px;
background-color: #488;
border: 4px outset #aca;
color: #fff;
text-align: center;
}

/* the physicist that the user is dragging around, after user has clicked to drag */
.physicist.ghost {
position: absolute;
background-color: #266;
opacity: 0.7; /* want to be able to see hilited slot underneath */
pointer-events: none; /* so pointer events go thru to slot */
}

/* physicist, hidden after it's been picked up and only the ghost is visible */
.physicist.original {
visibility: hidden;
}

/* where to drag a physicist */
.slot {
height: 15px;
background-color: #dfd;
border: 4px inset #aca;
/*color: black;*/
}

.slot.targeted {
/* When user moves ghost over a slot. A BIG change in appearance;
this only happens for a fraction of a second and is otherwise hard to see */
background-color: #f80;
}
36 changes: 36 additions & 0 deletions mouseevents/click-and-drag/Click_N_Drag.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<link rel="stylesheet" href="Click_N_Drag.css" />
<head>

<body>
<!-- you need to decide upon an outer enclosure, called the arena. The
user can drag the ghost around the arena, but if they move outside, it's
equivalent to a pointerUp event and dragging ends. Otherwise, the ghost
and dragging state get stuck on, it's a mess. Often, the body is the best choice.
-->
<main class="arena">
<h3>Click and Drag demo, using mouse events.
Drag a physicist's name into one of the green slots to rearrange</h3>
<ul>
<li class="slot" data-slot="0"> </li>
<li class="physicist" data-physicist="0"></li>
<li class="slot" data-slot="1"> </li>
<li class="physicist" data-physicist="1"></li>
<li class="slot" data-slot="2"> </li>
<li class="physicist" data-physicist="2"></li>
<li class="slot" data-slot="3"> </li>
<li class="physicist" data-physicist="3"></li>
<li class="slot" data-slot="4"> </li>
<li class="physicist" data-physicist="4"></li>
<li class="slot" data-slot="5"> </li>
</ul>
</main>

<script src=Click_N_Drag.js></script>

</body>
</html>
174 changes: 174 additions & 0 deletions mouseevents/click-and-drag/Click_N_Drag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@

let physicists = ['Noether', 'Dirac', 'Bohr', 'Curie', 'Pauli'];

// yes, using globals is a bad idea, in general. But in this case, there's only
// one mouse, so there'll never be two users of these variables at once.
// The arena is the whole area the user can drag around in.
// all these are element refs.
let arenaElement;
let arenaUl; // the <ul> element that encloses all the <li>s
let physicistElements;
let slotElements;
let originalElement;
let targetSlot;

// the image of the physicist being dragged. If ghostElement is falsy, we're not dragging
let ghostElement;
let ghostOffsetX;
let ghostOffsetY;

// generate html based on physicists
function populatePhysicists() {
physicistElements.forEach((physicist, ix) => {
physicist.textContent = physicists[ix];
});
}

// when you finally decide to move one, call this
function moveAPhysicist(physicistToMove, slotToMoveTo) {
// moved to a neighboring slot - nothing to do
if (slotToMoveTo == physicistToMove || slotToMoveTo == physicistToMove + 1) {
return;
}

// change the backing store.
if (slotToMoveTo < physicistToMove) {
[physicist] = physicists.splice(physicistToMove, 1);
physicists.splice(slotToMoveTo, 0, physicist);
}
else {
[physicist] = physicists.splice(physicistToMove, 1);
physicists.splice(slotToMoveTo - 1, 0, physicist);
}

// now recreate the screen elements according to the backing store
populatePhysicists();
}


////////////////////////////// event handlers.

// is called continuously while dragging is going on, to make ghost move around
function setGhostLocation(ev) {
ghostElement.style.left = `${ev.pageX - ghostOffsetX}px`;
ghostElement.style.top = `${ev.pageY - ghostOffsetY}px`;

// no text selections! we need this for all the events
ev.stopPropagation();
ev.preventDefault();
}

// as soon as the mouse clicks down over a physicist, everything changes
function mouseDownHandler(ev) {
// must be left mouse button
if (!(ev.buttons & 1)) {
return;
}

// clone the physicist element, for the user to drag around.
// Don't use the original, keep the existing layout.
originalElement = ev.target;
ghostElement = originalElement.cloneNode(true);
ghostElement.classList.add('ghost')
arenaUl.append(ghostElement);
originalElement.classList.add('original');

arenaElement.style.cursor = 'grabbing';

// we need the clickdown location, relative to the upper left corner of the physicist's margin
ghostOffsetX = ev.offsetX + 13;
ghostOffsetY = ev.offsetY + 13;

setGhostLocation(ev);
}

function mouseMoveHandler(ev) {
// Move the ghost as the user is dragging around, anywhere. If you don't
// check ghostElement, a user could start this by dragging into a physicist
// instead of clicking down. If you don't check ev.buttons, you could be
// confused by a mouseUp outside the window.
if (ghostElement && (ev.buttons & 1)) {
setGhostLocation(ev);
}
}

// after user lets go, or drags too far out, clean up the DOM
function mouseUpHandler(ev) {
// ev.buttons has already been set to zero
if (ghostElement) {
setGhostLocation(ev);

// do we do it? Only if they've been dragging over a slot
if (targetSlot) {
moveAPhysicist(originalElement.dataset.physicist, targetSlot.dataset.slot)

// Now's when you might want to trigger off some animation,
// depicting objects moving around
}

// remove any slot hiliting, like user dragged out of it
leaveSlotHandler(ev);

originalElement.classList.remove('original');

arenaElement.style.cursor = ''; // turns off grabbing hand

ghostElement.remove();
ghostElement = null;
}
}

// as user drags the ghost onto a target slot
function enterSlotHandler(ev) {
if (ghostElement) {
targetSlot = ev.target;
targetSlot.classList.add('targeted');
}
}

function leaveSlotHandler(ev) {
if (targetSlot && ghostElement) {
targetSlot.classList.remove('targeted');
targetSlot = null;
}
}

////////////////////////////// set event handlers.

// set ALL the event handlers
document.addEventListener('DOMContentLoaded', ev => {
arenaElement = document.querySelector('main');
arenaUl = arenaElement.querySelector('ul')
slotElements = arenaUl.querySelectorAll('.slot');

// The physicists need mousedown for the clickdown
physicistElements = arenaUl.querySelectorAll('.physicist');
physicistElements.forEach(physicist => {
physicist.addEventListener('mousedown', mouseDownHandler);
});
populatePhysicists();

// the slots need to hilite when dragged over, indicating that the user can drop.
// and then, unhilte when the user drags out
slotElements.forEach(slot => {
slot.addEventListener('mouseenter', enterSlotHandler);
slot.addEventListener('mouseleave', leaveSlotHandler);
});

// the user can move the mouse ANYWHERE. Here we listen through the arena.
arenaElement.addEventListener('mousemove', mouseMoveHandler);
arenaElement.addEventListener('mouseup', mouseUpHandler);
arenaElement.addEventListener('mouseleave', mouseUpHandler);

// an alternative is to add listeners in your mouseDown handler,
// and remove them in your mouseUp handler.
// This is better if you have multiple places that do a click n drag.

// A more modern way is to use setPointerCapture(). This will corral your
// mouse events so they arrive in your original click-down element.
// In mouseDownHandler():
// originalElement.setPointerCapture(ev.mouseId);

});


Loading