Implement drag and drop in angular 2/4/5/6

Properties

const dragonDrop = new DragonDrop(container);

Array

An array of each of the sortable item element references.

Array

An array of each of the handle item element references. If instance doesn’t have handles, this will be identical to .

A direct handle on the instance created by

Example with options

const list = document.getElementById('dragon-list');
const dragonDrop = new DragonDrop(list, {
  item 'li',
  handle '.handle',
  announcement {
    grabbed el => `The dragon has grabbed ${el.innerText}`,
    dropped el => `The dragon has dropped ${el.innerText}`,
    reorder (el, items) => {
      const pos = items.indexOf(el) + 1;
      const text = el.innerText;
      return `The dragon's list has been reordered, ${text} is now item ${pos} of ${items.length}`;
    },
    cancel 'The dragon cancelled the reorder'
  }
});

Аффордансы, которые зависят от ситуации

Единственное, что зависит от ситуации — это отображать маркер перетаскивания или нет. Маркеры перетаскивания — это небольшие иконки, которые показывают, что элемент можно перетаскивать. В интерфейсах Gmail в качестве маркера используют иконку из 12 точек, а мы выбрали иконку из 6 точек:

Когда маркер перетаскивания нужно отображать, а когда — нет? Это зависит от контекста. Если drag-and-drop доступен для элемента, который обычно нельзя перетаскивать, то обязательно нужно добавить маркер перетаскивания. А если drag-and-drop взаимодействие и так подразумевается, можно обойтись и без маркеров.

К примеру, в древовидном меню мы обошлись без маркеров, потому что и так очевидно, что пункты меню можно менять местами. А вот на карточках есть маркеры — потому что не все карточки можно перетаскивать.

Есть много разных иконок для обозначения маркеров перетаскивания. Нужно выбрать одну — и использовать ее везде.

Одна большая drag-and-drop семья

Благодаря этим стандартам можно добиться согласованности между разными сценариями использования drag-and-drop.

Здесь вы можете посмотреть все сценарии использования drag-and-drop, о которых я рассказывала.

Команда Clarity только начинает процесс разработки для всех этих drag-and-drop паттернов, начиная с упорядочивания колонок в таблице данных. Можете следить за процессом разработки на GitHub.

DropTarget

Именно на ложится работа по отображению предполагаемой «точки приземления» аватара, а также, по завершению переноса, обработка результата.

Как правило, принимает переносимый узел в себя, а вот как конкретно организован процесс вставки – нужно описать в классе-наследнике. Разные типы зон делают разное при вставке: вставляет элемент в качестве потомка, а – удаляет.

Как видно, из кода выше, по умолчанию занимается только отслеживанием и индикацией «точки приземления». По умолчанию, единственной возможной «точкой приземления» является сам элемент зоны. В более сложных ситуациях это может быть подэлемент.

Для применения в реальности необходимо как минимум переопределить обработку результата переноса в .

содержит код, специфичный для дерева:

  • Индикацию переноса над элементом: методы и .
  • Получение текущей точки приземления в методе . Ей может быть только заголовок узла дерева, причём дополнительно проверяется, что это не потомок переносимого узла.
  • Обработка успешного переноса в , вставка исходного узла в узел, соответствующий .

API

The selector for the drag items (qualified within container). Defaults to

'li'

The selector for the keyboard handle (qualified within the container and the selector provided for ). If set to , the entire item will be used as the handle. Defaults to

The class to be added to the item being dragged. Defaults to

'dragon-active'

The class to be added to all of the other items when an item is being dragged. Defaults

'dragon-inactive'
Boolean

Set to true if nested lists are being used (click and keydown events will not bubble up ( will be applied)). For nested lists, you MUST pass an array of containers as the 1st parameter (see example below).

NOTE:

const lists = Array.from(document.querySelectorAll('.dragon-list'));
const dragons = new DragonDrop(lists, {
  nested true,
  handle false,
  item ':scope > li' // IMPORTANT! a selector that targets only a single list's items
});
const  = dragons;

topLevel.on('grabbed', () => console.log('top-most container item grabbed'));
sublist1.on('grabbed', () => console.log('sublist 1 item grabbed'));
sublist2.on('grabbed', () => console.log('sublist 1 item grabbed'));
Object

An options object passed through to dragula.

NOTE: will be ignored given a DragonDrop instance with and a truthy

NOTE: AND will be ignored given a DragonDrop instance with

Object

The live region announcement configuration object containing the following properties:

Function

The function called when an item is picked up. The currently grabbed element along with an array of all items are passed as arguments respectively. The function should return a string of text to be announced in the live region. Defaults to

el => `Item ${el.innerText} grabbed`
Function

The function called when an item is dropped. The newly dropped item along with an array of all items are passed as arguments respectively. The function should return a string of text to be announced in the live region. Defaults to

el => `Item ${el.innerText} dropped`
Function

The function called when the list has been reordered. The newly dropped item along with an array of items are passed as arguments respectively. The function should return a string of text to be announced in the live region. Defaults to

(el, items) => {
  const pos = items.indexOf(el) + 1;
  const text = el.innerText;
  return `The list has been reordered, ${text} is now item ${pos} of ${items.length}`;
}
Function

The function called when the reorder is cancelled (via ESC). No arguments passed in. Defaults to

() => 'Reordering cancelled'

Usage

div ="draggable.data"
     ="draggable.effectAllowed"
     ="draggable.disable"
     (dndStart)="onDragStart($event)"
     (dndCopied)="onDraggableCopied($event)"
     (dndLinked)="onDraggableLinked($event)"
     (dndMoved)="onDraggableMoved($event)"
     (dndCanceled)="onDragCanceled($event)"
     (dndEnd)="onDragEnd($event)">
      
    
    div *ngIf="draggable.handle"
         dndHandle>HANDLE
    div>
    
    draggable ({{draggable.effectAllowed}}) span ="!draggable.disable">DISABLEDspan>
    
    
    div dndDragImageRef>DRAG_IMAGEdiv>
    
div>



section dndDropzone
         (dndDragover)="onDragover($event)"
         (dndDrop)="onDrop($event)">
      
    dropzone 
    
    
    
    div style="border: 1px orangered solid; border-radius: 5px; padding: 15px;"
         dndPlaceholderRef>
        placeholder
    div>

section>
import { Component } from '@angular/core';

import { DndDropEvent } from 'ngx-drag-drop';

@Component()
export class AppComponent {
  
  draggable = {
    // note that data is handled with JSON.stringify/JSON.parse
    // only set simple data or POJO's as methods will be lost 
    data: "myDragData",
    effectAllowed: "all",
    disable: false,
    handle: false
  };
  
  onDragStart(event:DragEvent) {

    console.log("drag started", JSON.stringify(event, null, 2));
  }
  
  onDragEnd(event:DragEvent) {
    
    console.log("drag ended", JSON.stringify(event, null, 2));
  }
  
  onDraggableCopied(event:DragEvent) {
    
    console.log("draggable copied", JSON.stringify(event, null, 2));
  }
  
  onDraggableLinked(event:DragEvent) {
      
    console.log("draggable linked", JSON.stringify(event, null, 2));
  }
    
  onDraggableMoved(event:DragEvent) {
    
    console.log("draggable moved", JSON.stringify(event, null, 2));
  }
      
  onDragCanceled(event:DragEvent) {
    
    console.log("drag cancelled", JSON.stringify(event, null, 2));
  }
  
  onDragover(event:DragEvent) {
    
    console.log("dragover", JSON.stringify(event, null, 2));
  }
  
  onDrop(event:DndDropEvent) {
  
    console.log("dropped", JSON.stringify(event, null, 2));
  }
}
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { DndModule } from 'ngx-drag-drop';

import { AppComponent } from './app.component';

@NgModule({
  declarations: 
    AppComponent
  ,
  imports: 
    BrowserModule,
    DndModule
  ,
  providers: ,
  bootstrap: AppComponent
})
export class AppModule { 
}

Окончание переноса

Окончание переноса происходит по событию .

Его обработчик можно поставить на аватаре, т.к. аватар всегда под курсором и происходит на нем. Но для универсальности и большей гибкости (вдруг мы захотим перемещать аватар рядом с курсором?) поставим его, как и остальные, на .

Задача обработчика :

  1. Обработать успешный перенос, если он идёт (существует аватар)
  2. Очистить данные .

Это даёт нам следующий код:

Для завершения переноса в функции нам нужно понять, на каком элементе мы находимся, и если над – обработать перенос, а нет – откатиться:

Чтобы понять, над каким элементом мы остановились – используем метод document.elementFromPoint(clientX, clientY), который мы обсуждали в разделе . Этот метод получает координаты относительно окна и возвращает самый глубокий элемент, который там находится.

Функция , описанная ниже, использует его и находит самый глубокий элемент с атрибутом под курсором мыши:

Обратите внимание – для нужны координаты относительно окна , а не. Вариант выше – предварительный

Он не будет работать. Если попробовать применить эту функцию, будет все время возвращать один и тот же элемент! А именно – текущий переносимый. Почему так?

Вариант выше – предварительный. Он не будет работать. Если попробовать применить эту функцию, будет все время возвращать один и тот же элемент! А именно – текущий переносимый. Почему так?

…Дело в том, что в процессе переноса под мышкой находится именно аватар. При начале переноса ему даже ставится большой, чтобы он был поверх всех остальных.

Аватар перекрывает остальные элементы. Поэтому функция увидит на текущих координатах именно его.

Чтобы это изменить, нужно либо поправить код переноса, чтобы аватар двигался рядом с курсором мыши, либо дать аватару стиль (кроме IE10-), либо:

  1. Спрятать аватар.
  2. Вызывать .
  3. Показать аватар.

Напишем функцию , которая это делает:

Solution for drag-n-drop with v-tabs ( from part — I )

When we dragged the active tab from one place to another, it was loosing the focus. Also if another tab was dragged anywhere, the active tab also was loosing the focus as seen here. To solve this problems, we have to see the events provided by the . There is support for , , , , , , , , . That’s are lot to choose from, but we are going to use the update event. Other events are also useful. You can play with them.

The update event gets fired when user drops the tab from one place to another. So on the event we fire our function in . The methods provide an with and but we only need the indexes. We have on for changing active tab position. The on should be same as of for changing active tab from code. We also use from both the which gives us flexibility of making sure we get tab at that index regardless of drag-n-drop.

Code for Tabs.

The function has logic related to how should drag-n-drop of the tab should be handled. The comment in gist are enough to explain that function so I will not repeat it here. Here is also a working codepen. And we have done it. The now show the correct active tab without any problem as shown below.

We fixed the v-tabs problem of active tab. Now we can relax???

Correct positioning

In the examples above the ball is always moved so, that it’s center is under the pointer:

Not bad, but there’s a side-effect. To initiate the drag’n’drop, we can anywhere on the ball. But if “take” it from its edge, then the ball suddenly “jumps” to become centered under the mouse pointer.

It would be better if we keep the initial shift of the element relative to the pointer.

For instance, if we start dragging by the edge of the ball, then the pointer should remain over the edge while dragging.

Let’s update our algorithm:

  1. When a visitor presses the button () – remember the distance from the pointer to the left-upper corner of the ball in variables . We’ll keep that distance while dragging.

    To get these shifts we can substract the coordinates:

  2. Then while dragging we position the ball on the same shift relative to the pointer, like this:

The final code with better positioning:

In action (inside ):

The difference is especially noticeable if we drag the ball by its right-bottom corner. In the previous example the ball “jumps” under the pointer. Now it fluently follows the pointer from the current position.

Расширения

Существует масса возможных применений Drag’n’Drop. Здесь мы не будем реализовывать их все, поскольку не стоит цель создать фреймворк-монстр.

Однако, мы рассмотрим их, чтобы, при необходимости, легко было написать то, что нужно.

Часто бывает, что перенос должен быть инициирован только при захвате за определённую зону элемента. К примеру, модальное окно можно «взять», только захватив его за заголовок.

Для этого достаточно добавить необходимую проверку, к примеру, в функцию или перед её запуском.

Если был внутри элемента, помеченного, к примеру, классом , то начинаем перенос, иначе – нет.

Бывает и так, что не на любое место в можно положить элемент.

Например: в админке есть дерево всех объектов сайта: статей, разделов, посетителей и т.п.

  • В этом дереве есть узлы различных типов: «статьи», «разделы» и «пользователи».
  • Все узлы являются переносимыми объектами.
  • Узел «статья» (draggable) можно переносить в «раздел» (droppable), а узел «пользователи» – нельзя. Но и то и другое можно поместить в «корзину».

Здесь решение: можно переносить или нельзя зависит от «типа» переносимого объекта.

Есть и более сложные варианты, когда решение зависит от конкретного места в , над которым посетитель отпустил кнопку мыши. К примеру, переносить в верхнюю часть можно, а в нижнюю – нет.

Эта задача решается добавлением проверки в . Эта функция знает и об аватаре и о событии, включая координаты. При попытке положить в «неправильное» место функция должна возвращать .

Однако, на практике бывают ситуации, когда решение «прямо сейчас» принять невозможно. Например, нужно сделать запрос на сервер: «А разрешено ли текущему посетителю производить такую операцию?»

Как при этом должен вести себя интерфейс? Можно, конечно сделать, чтобы элемент после отпускания кнопки мыши «завис» над , ожидая ответа. Однако, такое решение неудобно в реализации и странновато выглядит для посетителя.

Как правило, применяют «оптимистичный» алгоритм, по которому мы считаем, что перенос обычно успешен, но при необходимости можем отменить его.

При нём посетитель кладёт объект туда, куда он хочет, а затем, в коде :

  1. Визуально обрабатывается завершение переноса, как будто все ок.
  2. Производится асинхронный запрос к серверу, содержащий информацию о переносе.
  3. Сервер обрабатывает перенос и возвращает ответ, все ли в порядке.
  4. Если нет – выводится ошибка и возвращается . Аватар в этом случае должен предусматривать возможность отката после успешного завершения.

Процесс общения с сервером сопровождается индикацией загрузки и, при необходимости, блокировкой новых операций переноса до получения подтверждения.

Удобно, когда пользователь во время переноса наглядно видит, куда он сейчас положит draggable. Например, текущий droppable (или его часть) подсвечиваются.

Для этого в можно добавить дополнительные методы интеграции с внешним кодом:

  • – будет вызываться при заходе на , из .
  • – при каждом передвижении внутри , из .
  • – при выходе с , из и .

Возможен более сложный вариант, когда нужно поддерживать не только перенос в элемент, но и перенос между элементами, например вставку одной статьи между двумя другими.

Для этого код, который обрабатывает перенос, может «делить на части» droppable, к примеру, в соотношении 25% – 50% – 25%, и смотреть:

  • Если перенос в верхнюю четверть, то это – «над».
  • Если перенос в середину, то это «внутрь».
  • Если перенос в нижнюю четверть, то это – «под».

Текущий и позиция относительно него при этом могут помечаться подсветкой и жирной чертой над/под, если требуется.

Пример индикации из Firefox:

Отмену переноса и возврат аватара на место можно красиво анимировать.

Один из частых вариантов – скольжение объекта обратно к исходному месту, откуда его взяли. Для этого достаточно поправить .

Events

All event are fired with the same arguments:

  • any
    This is the data set on the ‘s prop. It is available on all -fired events, despite the official spec only permitting it on .

If you need to pass additional arguments in your event listener, the preferred method is to use the ES6 spread operator with :

drag @drag="myListener('foo', ...arguments)">Drag Medrag>
myListener(myArg, transferData, nativeEvent) {
  // myArg === 'foo'
}

If you don’t have the spread operator in your environment, you can use a wrapping function:

drag @drag="function(transferData, nativeEvent) { myListener('foo', transferData, nativeEvent) }">
  Drag Me
drag>

components:
Fired once when dragging starts.

components:
Repeatedly fired for the entire duration of the drag operation.

components: ,
Fired once every time a is dragged over a .

components: ,
Repeatedly fired while a is over a .

components: ,
Fired once every time a leaves a .

components:
Fired once when a is dropped on a .

components:
Fired once when the drag operation is completed. Occurs after .

Перетаскивание данныхDragging Data

Все операции перетаскивания начинаются с переноса данных.All drag-and-drop operations begin with dragging. Функция, позволяющая собирать данные при начале перетаскивания, реализуется в методе DoDragDrop.The functionality to enable data to be collected when dragging begins is implemented in the DoDragDrop method.

В следующем примере событие MouseDown используется для запуска операции перетаскивания, поскольку оно является наиболее интуитивно понятным (большинство действий по перетаскиванию начинается с нажатия кнопки мыши).In the following example, the MouseDown event is used to start the drag operation because it is the most intuitive (most drag-and-drop actions begin with the mouse button being depressed). Однако не забывайте, что любое событие может использоваться для инициализации процедуры перетаскивания.However, remember that any event could be used to initiate a drag-and-drop procedure.

Примечание

Некоторые элементы управления имеют собственные события перетаскивания.Certain controls have custom drag-specific events. Например, для элементов управления ListView и TreeView есть событие ItemDrag.The ListView and TreeView controls, for example, have an ItemDrag event.

Начало операции перетаскиванияTo start a drag operation

  1. В событии MouseDown для элемента управления, в котором начнется перетаскивание, используйте метод , чтобы задать перетаскиваемые данные и разрешить перетаскивание разрешенных эффектов.In the MouseDown event for the control where the drag will begin, use the method to set the data to be dragged and the allowed effect dragging will have. Дополнительные сведения см. в разделе Data и AllowedEffect.For more information, see Data and AllowedEffect.

    В следующем примере показан запуск операции перетаскивания.The following example shows how to initiate a drag operation. Элемент управления, в котором начинается перетаскивание, является элементом управления Button, перетаскиваемые данные — это строка, представляющая свойство Text элемента управления Button, а допустимые эффекты — копирование или перемещение.The control where the drag begins is a Button control, the data being dragged is the string representing the Text property of the Button control, and the allowed effects are either copying or moving.

    Примечание

    Любые данные можно использовать в качестве параметра в методе ; в приведенном выше примере использовалось свойство Text элемента управления Button (вместо того, чтобы жестко кодировать значение или извлечь данные из набора данных), так как свойство было связано с расположением, которое перетаскивается из (элемент управления Button).Any data can be used as a parameter in the method; in the example above, the Text property of the Button control was used (rather than hard-coding a value or retrieving data from a dataset) because the property was related to the location being dragged from (the Button control). Учитывайте это при реализации операций перетаскивания в приложениях Windows.Keep this in mind as you incorporate drag-and-drop operations into your Windows-based applications.

Пока действует операция перетаскивания, можно выполнить обработку события QueryContinueDrag, которое «запрашивает разрешение», чтобы продолжить операцию перетаскивания.While a drag operation is in effect, you can handle the QueryContinueDrag event, which «asks permission» of the system to continue the drag operation. При обработке этого метода также можно вызвать методы, которые влияют на операцию перетаскивания, например, расширение TreeNode в элементе управления TreeView, когда курсор наведен на него.When handling this method, it is also the appropriate point for you to call methods that will have an effect on the drag operation, such as expanding a TreeNode in a TreeView control when the cursor hovers over it.

Summary

We considered a basic Drag’n’Drop algorithm.

The key components:

  1. Events flow: → → (don’t forget to cancel native ).
  2. At the drag start – remember the initial shift of the pointer relative to the element: and keep it during the dragging.
  3. Detect droppable elements under the pointer using .

We can lay a lot on this foundation.

  • On we can intellectually finalize the drop: change data, move elements around.
  • We can highlight the elements we’re flying over.
  • We can limit dragging by a certain area or direction.
  • We can use event delegation for . A large-area event handler that checks can manage Drag’n’Drop for hundreds of elements.
  • And so on.

Tasks

importance: 5

Create a slider:

Drag the blue thumb with the mouse and move it.

Important details:

  • When the mouse button is pressed, during the dragging the mouse may go over or below the slider. The slider will still work (convenient for the user).
  • If the mouse moves very fast to the left or to the right, the thumb should stop exactly at the edge.

solution

As we can see from HTML/CSS, the slider is a

importance: 5

This task can help you to check understanding of several aspects of Drag’n’Drop and DOM.

Make all elements with class – draggable. Like a ball in the chapter.

Requirements:

  • Use event delegation to track drag start: a single event handler on for .
  • If elements are dragged to top/bottom window edges – the page scrolls up/down to allow further dragging.
  • There is no horizontal scroll (this makes the task a bit simpler, adding it is easy).
  • Draggable elements or their parts should never leave the window, even after swift mouse moves.

The demo is too big to fit it here, so here’s the link.

solution

To drag the element we can use , it makes coordinates easier to manage. At the end we should switch it back to to lay the element into the document.

When coordinates are at window top/bottom, we use to scroll it.

More details in the code, in comments.

Previous lessonNext lesson

Tutorial map

Итого

Реализация Drag’n’Drop оказалась отличным способом применить ООП в JavaScript.

Исходный код примера целиком находится в песочнице.

  • Синглтон и классы задают общий фреймворк. От них наследуются конкретные объекты. Для создания новых зон достаточно унаследовать стандартные классы и переопределить их.

  • Мини-фреймворк для Drag’n’Drop, который здесь представлен, является переписанным и обновлённым вариантом реальной библиотеки, на основе которой было создано много успешных скриптов переноса.

    В зависимости от ваших потребностей, вы можете расширить его, добавить перенос нескольких объектов одновременно, поддержку событий и другие возможности.

  • На сегодняшний день в каждом серьёзном фреймворке есть библиотека для Drag’n’Drop. Она работает похожим образом, но сделать универсальный перенос – штука непростая. Зачастую он перегружен лишней функциональностью, либо наоборот – недостаточно расширяем в нужных местах.
    Понимание, как это все может быть устроено, на примере этой статьи, может помочь в адаптации существующего кода под ваши потребности.

Ссылка на основную публикацию