DragnDrop s událostmi myši

Drag’n’Drop je skvělé řešení rozhraní. Vzít něco a přetáhnout to je jasný a jednoduchý způsob, jak dělat mnoho věcí, od kopírování a přesouvání dokumentů (jako ve správcích souborů) až po objednávání (vkládání položek do košíku).

V moderním standardu HTML existuje sekce o přetažení se speciálními událostmi, jako je dragstart , dragend , a tak dále.

Tyto události nám umožňují podporovat speciální druhy drag'n'drop, jako je manipulace s přetažením souboru ze správce souborů OS a jeho vložením do okna prohlížeče. Potom může JavaScript přistupovat k obsahu takových souborů.

Ale nativní události přetažení mají také omezení. Nemůžeme například zabránit přetažení z určité oblasti. Také nemůžeme provést přetažení pouze „vodorovně“ nebo „svisle“. A existuje mnoho dalších úkolů přetažení, které pomocí nich nelze provést. Také podpora mobilních zařízení pro takové události je velmi slabá.

Zde tedy uvidíme, jak implementovat Drag’n’Drop pomocí událostí myši.

Algoritmus drag'n'drop

Základní algoritmus Drag’n’Drop vypadá takto:

  1. Dne mousedown – v případě potřeby připravte prvek pro přesun (možná vytvořte jeho klon, přidejte k němu třídu nebo cokoli jiného).
  2. Poté na mousemove přesunout změnou left/top s position:absolute .
  3. Na mouseup – provádět všechny akce související s dokončením přetažení.

Toto jsou základy. Později uvidíme, jak přidat další funkce, jako je zvýraznění aktuálních základních prvků, když přes ně přetáhneme.

Zde je implementace tažení míče:

ball.onmousedown = function(event) {
 // (1) prepare to moving: make absolute and on top by z-index
 ball.style.position = 'absolute';
 ball.style.zIndex = 1000;

 // move it out of any current parents directly into body
 // to make it positioned relative to the body
 document.body.append(ball);

 // centers the ball at (pageX, pageY) coordinates
 function moveAt(pageX, pageY) {
 ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
 ball.style.top = pageY - ball.offsetHeight / 2 + 'px';
 }

 // move our absolutely positioned ball under the pointer
 moveAt(event.pageX, event.pageY);

 function onMouseMove(event) {
 moveAt(event.pageX, event.pageY);
 }

 // (2) move the ball on mousemove
 document.addEventListener('mousemove', onMouseMove);

 // (3) drop the ball, remove unneeded handlers
 ball.onmouseup = function() {
 document.removeEventListener('mousemove', onMouseMove);
 ball.onmouseup = null;
 };

};

Pokud spustíme kód, můžeme si všimnout něčeho divného. Na začátku drag’n’drop se míč „rozdvojí“:začneme přetahovat jeho „klon“.

Zde je příklad v akci:

Zkuste přetáhnout myší a uvidíte takové chování.

Je to proto, že prohlížeč má vlastní podporu drag'n'drop pro obrázky a některé další prvky. Spouští se automaticky a je v konfliktu s naším.

Chcete-li jej zakázat:

ball.ondragstart = function() {
 return false;
};

Nyní bude vše v pořádku.

V akci:

Další důležitý aspekt – sledujeme mousemove na document , nikoli na ball . Na první pohled se může zdát, že myš je vždy nad míčem a můžeme dát mousemove na to.

Ale jak si pamatujeme, mousemove spouští často, ale ne pro každý pixel. Takže po rychlém pohybu může ukazatel vyskočit z koule někde uprostřed dokumentu (nebo dokonce mimo okno).

Takže bychom měli poslouchat na document abych to chytil.

Správné umístění

Ve výše uvedených příkladech se míč vždy pohybuje tak, aby jeho střed byl pod ukazatelem:

ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
ball.style.top = pageY - ball.offsetHeight / 2 + 'px';

Není to špatné, ale má to vedlejší účinek. Chcete-li zahájit přetahování, můžeme mousedown kdekoli na míči. Pokud jej však „vezmete“ z okraje, míček náhle „vyskočí“ a stane se středem pod ukazatelem myši.

Bylo by lepší, kdybychom zachovali počáteční posun prvku vzhledem k ukazateli.

Pokud například začneme táhnout za okraj koule, měl by ukazatel při tažení zůstat přes okraj.

Pojďme aktualizovat náš algoritmus:

  1. Když návštěvník stiskne tlačítko (mousedown ) – zapamatovat si vzdálenost od ukazatele k levému hornímu rohu koule v proměnných shiftX/shiftY . Tuto vzdálenost při přetahování zachováme.

    Abychom získali tyto posuny, můžeme odečíst souřadnice:

    // onmousedown
    let shiftX = event.clientX - ball.getBoundingClientRect().left;
    let shiftY = event.clientY - ball.getBoundingClientRect().top;
  2. Potom při přetahování umístíme míček na stejný posun vzhledem k ukazateli, takto:

    // onmousemove
    // ball has position:absolute
    ball.style.left = event.pageX - shiftX + 'px';
    ball.style.top = event.pageY - shiftY + 'px';

Konečný kód s lepším umístěním:

ball.onmousedown = function(event) {

 let shiftX = event.clientX - ball.getBoundingClientRect().left;
 let shiftY = event.clientY - ball.getBoundingClientRect().top;

 ball.style.position = 'absolute';
 ball.style.zIndex = 1000;
 document.body.append(ball);

 moveAt(event.pageX, event.pageY);

 // moves the ball at (pageX, pageY) coordinates
 // taking initial shifts into account
 function moveAt(pageX, pageY) {
 ball.style.left = pageX - shiftX + 'px';
 ball.style.top = pageY - shiftY + 'px';
 }

 function onMouseMove(event) {
 moveAt(event.pageX, event.pageY);
 }

 // move the ball on mousemove
 document.addEventListener('mousemove', onMouseMove);

 // drop the ball, remove unneeded handlers
 ball.onmouseup = function() {
 document.removeEventListener('mousemove', onMouseMove);
 ball.onmouseup = null;
 };

};

ball.ondragstart = function() {
 return false;
};

V akci (uvnitř <iframe> ):

Rozdíl je zvláště patrný, pokud míč táhneme za jeho pravý dolní roh. V předchozím příkladu míč „skočí“ pod ukazatel. Nyní plynule sleduje ukazatel z aktuální pozice.

Potenciální cíle poklesu (odpaditelné položky)

V předchozích příkladech mohl míč padnout „kamkoli“, aby zůstal. V reálném životě obvykle vezmeme jeden prvek a pustíme ho na jiný. Například „soubor“ do „složky“ nebo něco jiného.

Když mluvíme abstraktně, vezmeme „přetahovací“ prvek a pustíme jej na „droppitelný“ prvek.

Potřebujeme vědět:

  • kde byl prvek na konci funkce Drag’n’Drop upuštěn – k provedení příslušné akce,
  • a pokud možno znát rozkládací soubor, přes který přetahujeme, abychom jej zvýraznili.

Řešení je svým způsobem zajímavé a jen trochu složité, takže ho zde pokryjeme.

Jaká může být první myšlenka? Pravděpodobně nastavit mouseover/mouseup obslužné osoby na potenciálních droppables?

Ale to nefunguje.

Problém je v tom, že když přetahujeme, přetahovací prvek je vždy nad ostatními prvky. A události myši se dějí pouze na horním prvku, nikoli na těch pod ním.

Níže jsou například dvě <div> prvky, červený nad modrým (plně zakrývá). Na modrém není žádný způsob, jak zachytit událost, protože červená je nahoře:

<style>
 div {
 width: 50px;
 height: 50px;
 position: absolute;
 top: 0;
 }
</style>
<div style="background:blue" onmouseover="alert('never works')"></div>
<div style="background:red" onmouseover="alert('over red!')"></div>

Totéž s přetahovacím prvkem. Míč je vždy nahoře nad ostatními prvky, takže se na něm dějí události. Ať už na nižší prvky nastavíme jakékoli ovladače, nebudou fungovat.

To je důvod, proč původní myšlenka umístit handlery na potenciální shozové položky v praxi nefunguje. Nebudou běžet.

Takže, co dělat?

Existuje metoda nazvaná document.elementFromPoint(clientX, clientY) . Vrací nejvíce vnořený prvek na daných souřadnicích relativních k oknu (nebo null pokud jsou dané souřadnice mimo okno). Pokud existuje více překrývajících se prvků na stejných souřadnicích, vrátí se ten úplně nahoře.

Můžeme jej použít v kterémkoli z našich obslužných programů myši k detekci potenciálně přemístitelného pod ukazatelem, například takto:

// in a mouse event handler
ball.hidden = true; // (*) hide the element that we drag

let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
// elemBelow is the element below the ball, may be droppable

ball.hidden = false;

Poznámka:Před voláním (*) musíme míč schovat . Jinak budeme mít obvykle kouli na těchto souřadnicích, protože je to horní prvek pod ukazatelem:elemBelow=ball . Takže to skryjeme a hned znovu ukážeme.

Pomocí tohoto kódu můžeme kdykoli zkontrolovat, nad kterým prvkem „přelétáváme“. A zvládněte pokles, když k němu dojde.

Rozšířený kód onMouseMove k vyhledání „odpaditelných“ prvků:

// potential droppable that we're flying over right now
let currentDroppable = null;

function onMouseMove(event) {
 moveAt(event.pageX, event.pageY);

 ball.hidden = true;
 let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
 ball.hidden = false;

 // mousemove events may trigger out of the window (when the ball is dragged off-screen)
 // if clientX/clientY are out of the window, then elementFromPoint returns null
 if (!elemBelow) return;

 // potential droppables are labeled with the class "droppable" (can be other logic)
 let droppableBelow = elemBelow.closest('.droppable');

 if (currentDroppable != droppableBelow) {
 // we're flying in or out...
 // note: both values can be null
 // currentDroppable=null if we were not over a droppable before this event (e.g over an empty space)
 // droppableBelow=null if we're not over a droppable now, during this event

 if (currentDroppable) {
 // the logic to process "flying out" of the droppable (remove highlight)
 leaveDroppable(currentDroppable);
 }
 currentDroppable = droppableBelow;
 if (currentDroppable) {
 // the logic to process "flying in" of the droppable
 enterDroppable(currentDroppable);
 }
 }
}

V níže uvedeném příkladu, když je míč tažen přes fotbalovou branku, je branka zvýrazněna.

Resultsstyle.cssindex.html
#gate {
 cursor: pointer;
 margin-bottom: 100px;
 width: 83px;
 height: 46px;
}

#ball {
 cursor: pointer;
 width: 40px;
 height: 40px;
}
<!doctype html>
<html>

<head>
 <meta charset="UTF-8">
 <link rel="stylesheet" href="style.css">
</head>

<body>

 <p>Drag the ball.</p>

 <img src="https://en.js.cx/clipart/soccer-gate.svg" id="gate" class="droppable">

 <img src="https://en.js.cx/clipart/ball.svg" id="ball">

 <script>
 let currentDroppable = null;

 ball.onmousedown = function(event) {

 let shiftX = event.clientX - ball.getBoundingClientRect().left;
 let shiftY = event.clientY - ball.getBoundingClientRect().top;

 ball.style.position = 'absolute';
 ball.style.zIndex = 1000;
 document.body.append(ball);

 moveAt(event.pageX, event.pageY);

 function moveAt(pageX, pageY) {
 ball.style.left = pageX - shiftX + 'px';
 ball.style.top = pageY - shiftY + 'px';
 }

 function onMouseMove(event) {
 moveAt(event.pageX, event.pageY);

 ball.hidden = true;
 let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
 ball.hidden = false;

 if (!elemBelow) return;

 let droppableBelow = elemBelow.closest('.droppable');
 if (currentDroppable != droppableBelow) {
 if (currentDroppable) { // null when we were not over a droppable before this event
 leaveDroppable(currentDroppable);
 }
 currentDroppable = droppableBelow;
 if (currentDroppable) { // null if we're not coming over a droppable now
 // (maybe just left the droppable)
 enterDroppable(currentDroppable);
 }
 }
 }

 document.addEventListener('mousemove', onMouseMove);

 ball.onmouseup = function() {
 document.removeEventListener('mousemove', onMouseMove);
 ball.onmouseup = null;
 };

 };

 function enterDroppable(elem) {
 elem.style.background = 'pink';
 }

 function leaveDroppable(elem) {
 elem.style.background = '';
 }

 ball.ondragstart = function() {
 return false;
 };
 </script>


</body>
</html>

Nyní máme aktuální „cíl pádu“, nad kterým letíme, v proměnné currentDroppable během celého procesu a můžete jej použít ke zvýraznění nebo k jiným věcem.

Shrnutí

Zvažovali jsme základní algoritmus Drag’n’Drop.

Klíčové komponenty:

  1. Tok událostí:ball.mousedowndocument.mousemoveball.mouseup (nezapomeňte zrušit nativní ondragstart ).
  2. Na začátku přetažení – zapamatujte si počáteční posun ukazatele vzhledem k prvku:shiftX/shiftY a během přetahování jej ponechat.
  3. Pomocí document.elementFromPoint zjistěte pod ukazatelem prvky, které lze přetáhnout .

Na tento základ můžeme položit hodně.

  • Na mouseup můžeme tento pokles intelektuálně dokončit:změnit data, přesunout prvky.
  • Můžeme zvýraznit prvky, nad kterými letíme.
  • Můžeme omezit tažení o určitou oblast nebo směr.
  • Pro mousedown/up můžeme použít delegování události . Obslužná rutina události s velkou oblastí, která kontroluje event.target může spravovat Drag’n’Drop pro stovky prvků.
  • A tak dále.

Existují rámce, které na něm staví architekturu:DragZone , Droppable , Draggable a další třídy. Většina z nich dělá věci podobné tomu, co je popsáno výše, takže by jim nyní mělo být snadné porozumět. Nebo udělejte vlastní, jak vidíte, že je to snadné a někdy jednodušší než přizpůsobení řešení třetí strany.