UNPKG

15.9 kBJavaScriptView Raw
1const actions = require('./base');
2const utils = require('../utils');
3const scope = require('../scope');
4/** @lends module:interact */
5const interact = require('../interact');
6const InteractEvent = require('../InteractEvent');
7/** @lends Interactable */
8const Interactable = require('../Interactable');
9const Interaction = require('../Interaction');
10const defaultOptions = require('../defaultOptions');
11
12const drop = {
13 defaults: {
14 enabled: false,
15 accept : null,
16 overlap: 'pointer',
17 },
18};
19
20let dynamicDrop = false;
21
22Interaction.signals.on('action-start', function ({ interaction, event }) {
23 if (interaction.prepared.name !== 'drag') { return; }
24
25 // reset active dropzones
26 interaction.activeDrops.dropzones = [];
27 interaction.activeDrops.elements = [];
28 interaction.activeDrops.rects = [];
29
30 interaction.dropEvents = null;
31
32 if (!interaction.dynamicDrop) {
33 setActiveDrops(interaction.activeDrops, interaction.element);
34 }
35
36 const dragEvent = interaction.prevEvent;
37 const dropEvents = getDropEvents(interaction, event, dragEvent);
38
39 if (dropEvents.activate) {
40 fireActiveDrops(interaction.activeDrops, dropEvents.activate);
41 }
42});
43
44InteractEvent.signals.on('new', function ({ interaction, iEvent, event }) {
45 if (iEvent.type !== 'dragmove' && iEvent.type !== 'dragend') { return; }
46
47 const draggableElement = interaction.element;
48 const dragEvent = iEvent;
49 const dropResult = getDrop(dragEvent, event, draggableElement);
50
51 interaction.dropTarget = dropResult.dropzone;
52 interaction.dropElement = dropResult.element;
53
54 interaction.dropEvents = getDropEvents(interaction, event, dragEvent);
55});
56
57Interaction.signals.on('action-move', function ({ interaction }) {
58 if (interaction.prepared.name !== 'drag') { return; }
59
60 fireDropEvents(interaction, interaction.dropEvents);
61});
62
63Interaction.signals.on('action-end', function ({ interaction }) {
64 if (interaction.prepared.name === 'drag') {
65 fireDropEvents(interaction, interaction.dropEvents);
66 }
67});
68
69Interaction.signals.on('stop-drag', function ({ interaction }) {
70 interaction.activeDrops = {
71 dropzones: null,
72 elements: null,
73 rects: null,
74 };
75
76 interaction.dropEvents = null;
77});
78
79function collectDrops (activeDrops, element) {
80 const drops = [];
81 const elements = [];
82
83 // collect all dropzones and their elements which qualify for a drop
84 for (const current of scope.interactables) {
85 if (!current.options.drop.enabled) { continue; }
86
87 const accept = current.options.drop.accept;
88
89 // test the draggable element against the dropzone's accept setting
90 if ((utils.is.element(accept) && accept !== element)
91 || (utils.is.string(accept)
92 && !utils.matchesSelector(element, accept))) {
93
94 continue;
95 }
96
97 // query for new elements if necessary
98 const dropElements = utils.is.string(current.target)
99 ? current._context.querySelectorAll(current.target)
100 : [current.target];
101
102 for (const currentElement of dropElements) {
103 if (currentElement !== element) {
104 drops.push(current);
105 elements.push(currentElement);
106 }
107 }
108 }
109
110 return {
111 elements,
112 dropzones: drops,
113 };
114}
115
116function fireActiveDrops (activeDrops, event) {
117 let prevElement;
118
119 // loop through all active dropzones and trigger event
120 for (let i = 0; i < activeDrops.dropzones.length; i++) {
121 const current = activeDrops.dropzones[i];
122 const currentElement = activeDrops.elements [i];
123
124 // prevent trigger of duplicate events on same element
125 if (currentElement !== prevElement) {
126 // set current element as event target
127 event.target = currentElement;
128 current.fire(event);
129 }
130 prevElement = currentElement;
131 }
132}
133
134// Collect a new set of possible drops and save them in activeDrops.
135// setActiveDrops should always be called when a drag has just started or a
136// drag event happens while dynamicDrop is true
137function setActiveDrops (activeDrops, dragElement) {
138 // get dropzones and their elements that could receive the draggable
139 const possibleDrops = collectDrops(activeDrops, dragElement);
140
141 activeDrops.dropzones = possibleDrops.dropzones;
142 activeDrops.elements = possibleDrops.elements;
143 activeDrops.rects = [];
144
145 for (let i = 0; i < activeDrops.dropzones.length; i++) {
146 activeDrops.rects[i] = activeDrops.dropzones[i].getRect(activeDrops.elements[i]);
147 }
148}
149
150function getDrop (dragEvent, event, dragElement) {
151 const interaction = dragEvent.interaction;
152 const validDrops = [];
153
154 if (dynamicDrop) {
155 setActiveDrops(interaction.activeDrops, dragElement);
156 }
157
158 // collect all dropzones and their elements which qualify for a drop
159 for (let j = 0; j < interaction.activeDrops.dropzones.length; j++) {
160 const current = interaction.activeDrops.dropzones[j];
161 const currentElement = interaction.activeDrops.elements [j];
162 const rect = interaction.activeDrops.rects [j];
163
164 validDrops.push(current.dropCheck(dragEvent, event, interaction.target, dragElement, currentElement, rect)
165 ? currentElement
166 : null);
167 }
168
169 // get the most appropriate dropzone based on DOM depth and order
170 const dropIndex = utils.indexOfDeepestElement(validDrops);
171
172 return {
173 dropzone: interaction.activeDrops.dropzones[dropIndex] || null,
174 element : interaction.activeDrops.elements [dropIndex] || null,
175 };
176}
177
178function getDropEvents (interaction, pointerEvent, dragEvent) {
179 const dropEvents = {
180 enter : null,
181 leave : null,
182 activate : null,
183 deactivate: null,
184 move : null,
185 drop : null,
186 };
187
188 const tmpl = {
189 dragEvent,
190 interaction,
191 target : interaction.dropElement,
192 dropzone : interaction.dropTarget,
193 relatedTarget: dragEvent.target,
194 draggable : dragEvent.interactable,
195 timeStamp : dragEvent.timeStamp,
196 };
197
198 if (interaction.dropElement !== interaction.prevDropElement) {
199 // if there was a prevDropTarget, create a dragleave event
200 if (interaction.prevDropTarget) {
201 dropEvents.leave = utils.extend({ type: 'dragleave' }, tmpl);
202
203 dragEvent.dragLeave = dropEvents.leave.target = interaction.prevDropElement;
204 dragEvent.prevDropzone = dropEvents.leave.dropzone = interaction.prevDropTarget;
205 }
206 // if the dropTarget is not null, create a dragenter event
207 if (interaction.dropTarget) {
208 dropEvents.enter = {
209 dragEvent,
210 interaction,
211 target : interaction.dropElement,
212 dropzone : interaction.dropTarget,
213 relatedTarget: dragEvent.target,
214 draggable : dragEvent.interactable,
215 timeStamp : dragEvent.timeStamp,
216 type : 'dragenter',
217 };
218
219 dragEvent.dragEnter = interaction.dropElement;
220 dragEvent.dropzone = interaction.dropTarget;
221 }
222 }
223
224 if (dragEvent.type === 'dragend' && interaction.dropTarget) {
225 dropEvents.drop = utils.extend({ type: 'drop' }, tmpl);
226
227 dragEvent.dropzone = interaction.dropTarget;
228 dragEvent.relatedTarget = interaction.dropElement;
229 }
230 if (dragEvent.type === 'dragstart') {
231 dropEvents.activate = utils.extend({ type: 'dropactivate' }, tmpl);
232
233 dropEvents.activate.target = null;
234 dropEvents.activate.dropzone = null;
235 }
236 if (dragEvent.type === 'dragend') {
237 dropEvents.deactivate = utils.extend({ type: 'dropdeactivate' }, tmpl);
238
239 dropEvents.deactivate.target = null;
240 dropEvents.deactivate.dropzone = null;
241 }
242 if (dragEvent.type === 'dragmove' && interaction.dropTarget) {
243 dropEvents.move = utils.extend({
244 dragmove : dragEvent,
245 type : 'dropmove',
246 }, tmpl);
247
248 dragEvent.dropzone = interaction.dropTarget;
249 }
250
251 return dropEvents;
252}
253
254function fireDropEvents (interaction, dropEvents) {
255 const {
256 activeDrops,
257 prevDropTarget,
258 dropTarget,
259 dropElement,
260 } = interaction;
261
262 if (dropEvents.leave) { prevDropTarget.fire(dropEvents.leave); }
263 if (dropEvents.move ) { dropTarget.fire(dropEvents.move ); }
264 if (dropEvents.enter) { dropTarget.fire(dropEvents.enter); }
265 if (dropEvents.drop ) { dropTarget.fire(dropEvents.drop ); }
266 if (dropEvents.deactivate) {
267 fireActiveDrops(activeDrops, dropEvents.deactivate);
268 }
269
270 interaction.prevDropTarget = dropTarget;
271 interaction.prevDropElement = dropElement;
272}
273
274/**
275 * ```js
276 * interact(target)
277 * .dropChecker(function(dragEvent, // related dragmove or dragend event
278 * event, // TouchEvent/PointerEvent/MouseEvent
279 * dropped, // bool result of the default checker
280 * dropzone, // dropzone Interactable
281 * dropElement, // dropzone elemnt
282 * draggable, // draggable Interactable
283 * draggableElement) {// draggable element
284 *
285 * return dropped && event.target.hasAttribute('allow-drop');
286 * }
287 * ```
288 *
289 * ```js
290 * interact('.drop').dropzone({
291 * accept: '.can-drop' || document.getElementById('single-drop'),
292 * overlap: 'pointer' || 'center' || zeroToOne
293 * }
294 * ```
295 *
296 * Returns or sets whether draggables can be dropped onto this target to
297 * trigger drop events
298 *
299 * Dropzones can receive the following events:
300 * - `dropactivate` and `dropdeactivate` when an acceptable drag starts and ends
301 * - `dragenter` and `dragleave` when a draggable enters and leaves the dropzone
302 * - `dragmove` when a draggable that has entered the dropzone is moved
303 * - `drop` when a draggable is dropped into this dropzone
304 *
305 * Use the `accept` option to allow only elements that match the given CSS
306 * selector or element. The value can be:
307 *
308 * - **an Element** - only that element can be dropped into this dropzone.
309 * - **a string**, - the element being dragged must match it as a CSS selector.
310 * - **`null`** - accept options is cleared - it accepts any element.
311 *
312 * Use the `overlap` option to set how drops are checked for. The allowed
313 * values are:
314 *
315 * - `'pointer'`, the pointer must be over the dropzone (default)
316 * - `'center'`, the draggable element's center must be over the dropzone
317 * - a number from 0-1 which is the `(intersection area) / (draggable area)`.
318 * e.g. `0.5` for drop to happen when half of the area of the draggable is
319 * over the dropzone
320 *
321 * Use the `checker` option to specify a function to check if a dragged element
322 * is over this Interactable.
323 *
324 * @param {boolean | object | null} [options] The new options to be set.
325 * @return {boolean | Interactable} The current setting or this Interactable
326 */
327Interactable.prototype.dropzone = function (options) {
328 if (utils.is.object(options)) {
329 this.options.drop.enabled = options.enabled === false? false: true;
330
331 if (utils.is.function(options.ondrop) ) { this.events.ondrop = options.ondrop ; }
332 if (utils.is.function(options.ondropactivate) ) { this.events.ondropactivate = options.ondropactivate ; }
333 if (utils.is.function(options.ondropdeactivate)) { this.events.ondropdeactivate = options.ondropdeactivate; }
334 if (utils.is.function(options.ondragenter) ) { this.events.ondragenter = options.ondragenter ; }
335 if (utils.is.function(options.ondragleave) ) { this.events.ondragleave = options.ondragleave ; }
336 if (utils.is.function(options.ondropmove) ) { this.events.ondropmove = options.ondropmove ; }
337
338 if (/^(pointer|center)$/.test(options.overlap)) {
339 this.options.drop.overlap = options.overlap;
340 }
341 else if (utils.is.number(options.overlap)) {
342 this.options.drop.overlap = Math.max(Math.min(1, options.overlap), 0);
343 }
344 if ('accept' in options) {
345 this.options.drop.accept = options.accept;
346 }
347 if ('checker' in options) {
348 this.options.drop.checker = options.checker;
349 }
350
351
352 return this;
353 }
354
355 if (utils.is.bool(options)) {
356 this.options.drop.enabled = options;
357
358 if (!options) {
359 this.ondragenter = this.ondragleave = this.ondrop
360 = this.ondropactivate = this.ondropdeactivate = null;
361 }
362
363 return this;
364 }
365
366 return this.options.drop;
367};
368
369Interactable.prototype.dropCheck = function (dragEvent, event, draggable, draggableElement, dropElement, rect) {
370 let dropped = false;
371
372 // if the dropzone has no rect (eg. display: none)
373 // call the custom dropChecker or just return false
374 if (!(rect = rect || this.getRect(dropElement))) {
375 return (this.options.drop.checker
376 ? this.options.drop.checker(dragEvent, event, dropped, this, dropElement, draggable, draggableElement)
377 : false);
378 }
379
380 const dropOverlap = this.options.drop.overlap;
381
382 if (dropOverlap === 'pointer') {
383 const origin = utils.getOriginXY(draggable, draggableElement, 'drag');
384 const page = utils.getPageXY(dragEvent);
385
386 page.x += origin.x;
387 page.y += origin.y;
388
389 const horizontal = (page.x > rect.left) && (page.x < rect.right);
390 const vertical = (page.y > rect.top ) && (page.y < rect.bottom);
391
392 dropped = horizontal && vertical;
393 }
394
395 const dragRect = draggable.getRect(draggableElement);
396
397 if (dragRect && dropOverlap === 'center') {
398 const cx = dragRect.left + dragRect.width / 2;
399 const cy = dragRect.top + dragRect.height / 2;
400
401 dropped = cx >= rect.left && cx <= rect.right && cy >= rect.top && cy <= rect.bottom;
402 }
403
404 if (dragRect && utils.is.number(dropOverlap)) {
405 const overlapArea = (Math.max(0, Math.min(rect.right , dragRect.right ) - Math.max(rect.left, dragRect.left))
406 * Math.max(0, Math.min(rect.bottom, dragRect.bottom) - Math.max(rect.top , dragRect.top )));
407
408 const overlapRatio = overlapArea / (dragRect.width * dragRect.height);
409
410 dropped = overlapRatio >= dropOverlap;
411 }
412
413 if (this.options.drop.checker) {
414 dropped = this.options.drop.checker(dragEvent, event, dropped, this, dropElement, draggable, draggableElement);
415 }
416
417 return dropped;
418};
419
420Interactable.signals.on('unset', function ({ interactable }) {
421 interactable.dropzone(false);
422});
423
424Interactable.settingsMethods.push('dropChecker');
425
426Interaction.signals.on('new', function (interaction) {
427 interaction.dropTarget = null; // the dropzone a drag target might be dropped into
428 interaction.dropElement = null; // the element at the time of checking
429 interaction.prevDropTarget = null; // the dropzone that was recently dragged away from
430 interaction.prevDropElement = null; // the element at the time of checking
431 interaction.dropEvents = null; // the dropEvents related to the current drag event
432
433 interaction.activeDrops = {
434 dropzones: [], // the dropzones that are mentioned below
435 elements : [], // elements of dropzones that accept the target draggable
436 rects : [], // the rects of the elements mentioned above
437 };
438
439});
440
441Interaction.signals.on('stop', function ({ interaction }) {
442 interaction.dropTarget = interaction.dropElement =
443 interaction.prevDropTarget = interaction.prevDropElement = null;
444});
445
446/**
447 * Returns or sets whether the dimensions of dropzone elements are calculated
448 * on every dragmove or only on dragstart for the default dropChecker
449 *
450 * @param {boolean} [newValue] True to check on each move. False to check only
451 * before start
452 * @return {boolean | interact} The current setting or interact
453 */
454interact.dynamicDrop = function (newValue) {
455 if (utils.is.bool(newValue)) {
456 //if (dragging && dynamicDrop !== newValue && !newValue) {
457 //calcRects(dropzones);
458 //}
459
460 dynamicDrop = newValue;
461
462 return interact;
463 }
464 return dynamicDrop;
465};
466
467utils.merge(Interactable.eventTypes, [
468 'dragenter',
469 'dragleave',
470 'dropactivate',
471 'dropdeactivate',
472 'dropmove',
473 'drop',
474]);
475actions.methodDict.drop = 'dropzone';
476
477defaultOptions.drop = drop.defaults;
478
479module.exports = drop;