@lumjs/web-draggable-list 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +266 -19
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1,11 +1,21 @@
1
+ /**
2
+ * @module @lumjs/web-draggable-list
3
+ */
1
4
  import { enableDragDropTouch } from '@dragdroptouch/drag-drop-touch';
2
5
  import WC from '@lumjs/web-core';
6
+ import core from '@lumjs/core';
3
7
 
4
- export {WC};
8
+ export { WC };
5
9
  export const { indexOf, on } = WC.ez;
6
10
 
11
+ const { isObj } = core.types;
12
+
7
13
  const DEF_OPTS = {
8
- items: '& > li',
14
+ autoRegister: null,
15
+ delegated: true,
16
+ enableTouch: true,
17
+ itemMatch: 'li',
18
+ itemSelector: '& > li',
9
19
  onDrop() {},
10
20
  onEnd() {},
11
21
  onEnter() {},
@@ -24,9 +34,98 @@ const DRAG_METHODS = [
24
34
  'handleDragEnd',
25
35
  ]
26
36
 
37
+ /**
38
+ * A class for making it so you can move list items using
39
+ * drag and drop. Includes touch support by default.
40
+ */
27
41
  export class DraggableList {
28
- constructor(listElem, opts) {
29
- this.opts = opts = Object.assign({}, DEF_OPTS, opts);
42
+ /**
43
+ * Build a DraggableList instance.
44
+ *
45
+ * @param {(string|Element)} listElem - Parent element for the list.
46
+ *
47
+ * Usually this should be the container (e.g. `<ul>`) that is the direct
48
+ * parent of the list items, and most of the option defaults expect that.
49
+ *
50
+ * If you use an element above the container here (say for instance if you
51
+ * have multiple lists that you want to be controlled by the same
52
+ * DraggableList instance for reasons left to your own devices), you will
53
+ * need to adjust at least `opts.itemSelector` to reflect that.
54
+ *
55
+ * If this is a string, it will be used as a query selector on the top-level
56
+ * document to find the desired element.
57
+ *
58
+ * @param {object} [opts] Options.
59
+ *
60
+ * You may specify this argument more than once. If the same options are
61
+ * set in multiple opts arguments, the ones specified last will be used.
62
+ * This design was mostly just to make *presets* easier to use.
63
+ *
64
+ * In addition to the options listed here, some presets may add their own.
65
+ *
66
+ * @param {?boolean} [opts.autoRegister=null] Register items automatically?
67
+ *
68
+ * If this is true then makeDraggable() will be called with no arguments
69
+ * at the very end of the constructor. If this is false, it is left up to
70
+ * you to call it explicitly (if its needed at all) from your app's code.
71
+ *
72
+ * If this is null (the default value), then its value will be determined
73
+ * using: `!opts.delegated`; which I think is a decent default.
74
+ *
75
+ * @param {boolean} [opts.delegated=true] Use delegated events?
76
+ *
77
+ * If this is true (the default), then the event listeners will be
78
+ * registered on the `listElem` using delegation to dispatch to the
79
+ * appropriate item elements.
80
+ *
81
+ * @param {boolean} [opts.enableTouch=true] Enable Touch compatibility?
82
+ *
83
+ * If this is true (the default), then we will call enableDragDropTouch(),
84
+ * passing the `listElem` as the first two arguments, and `opts` as the last.
85
+ *
86
+ * See https://github.com/drag-drop-touch-js/dragdroptouch for details on
87
+ * that function. Any of its options may be specified in `opts` as well.
88
+ *
89
+ * If you have custom requirements of any kind, you can set this to
90
+ * false and you'll be responsible for setting up DragDropTouch yourself.
91
+ *
92
+ * @param {string} [opts.itemMatch='li'] Delegation selector for items.
93
+ *
94
+ * This will be used to find matching list items using `element.match()`.
95
+ * The default `'li'` assumes you are using a <ul>, <ol>, or <menu> for
96
+ * your list container element.
97
+ *
98
+ * @param {string} [opts.itemSelector='& > li'] Selector to find list items.
99
+ *
100
+ * This is only ever used by the `listItems` accessor property to find
101
+ * list items using `listElem.querySelectorAll(opts.itemSelector)`.
102
+ *
103
+ * The only time this would be used by the class itself is if you
104
+ * call makeDraggable() without specifying any elements manually,
105
+ * which is the case if `opts.autoRegister` was enabled.
106
+ *
107
+ * The default `'& > li'` only matches <li> elements that are *direct*
108
+ * children of the listElem itself. So if that is not the case, you will
109
+ * need to adjust this option.
110
+ *
111
+ * @param {DropEventHandler} [opts.onDrop] Handler for `drop` events.
112
+ *
113
+ * The DragEvent object passed to this handler has two extra properties
114
+ * added; one representing the original position of the dragged element,
115
+ * and another for the position it was moved to after dropping it.
116
+ *
117
+ * Of all the handlers, this one is the one you'll likely want to define
118
+ * yourself and do something with.
119
+ *
120
+ * @param {DragEventHandler} [opts.onEnd] Handler for `dragend` events.
121
+ * @param {DragEventHandler} [opts.onEnter] Handler for `dragenter` events.
122
+ * @param {DragEventHandler} [opts.onLeave] Handler for `dragleave` events.
123
+ * @param {DragEventHandler} [opts.onOver] Handler for `dragover` events.
124
+ * @param {DragEventHandler} [opts.onStart] Handler for `dragstart` events.
125
+ * @param {boolean} [opts.verbose=true] Enable verbose debugging info.
126
+ */
127
+ constructor(listElem, ...opts) {
128
+ this.opts = opts = Object.assign({}, DEF_OPTS, ...opts);
30
129
  if (typeof listElem === 'string') {
31
130
  listElem = document.querySelector(listElem);
32
131
  }
@@ -37,18 +136,106 @@ export class DraggableList {
37
136
 
38
137
  this.listElem = listElem;
39
138
  this.dragging = null;
40
- enableDragDropTouch(listElem, listElem, opts);
139
+ this.registration = new Map();
140
+
141
+ if (opts.enableTouch) {
142
+ enableDragDropTouch(listElem, listElem, opts);
143
+ }
144
+
145
+ if (opts.delegated) { // Delegated event registration is the best IMHO!
146
+ this.registerDelegation();
147
+ }
148
+
149
+ if (opts.autoRegister ?? !opts.delegated) {
150
+ this.makeDraggable();
151
+ }
41
152
  }
42
153
 
154
+ /**
155
+ * Accessor to get a NodeList containing the list items.
156
+ */
43
157
  get listItems() {
44
- return this.listElem.querySelectorAll(this.opts.items);
158
+ return this.listElem.querySelectorAll(this.opts.itemSelector);
159
+ }
160
+
161
+ /**
162
+ * The method used to register delegated event listeners.
163
+ *
164
+ * You shouldn't normally have to call this manually, however
165
+ * you can use it to remove the delegated event listeners if you
166
+ * want to disable the drag and drop code for some reason, you
167
+ * can use this to do so.
168
+ *
169
+ * @param {boolean} [enabled=true] Enable delegated events?
170
+ *
171
+ * If true the events will be registered. If false, they will
172
+ * be removed. You can use this to toggle draggability on and off.
173
+ *
174
+ * @returns {DraggableList} this instance.
175
+ */
176
+ registerDelegation(enabled=true) {
177
+ let elem = this.listElem;
178
+ let opts = this.opts;
179
+ let registry = this.registration.get(elem) ?? {};
180
+
181
+ for (let meth of DRAG_METHODS)
182
+ {
183
+ let eventName = meth.replace('handle','').toLowerCase();
184
+
185
+ if (isObj(registry[eventName])) {
186
+ // Unregister an existing event handler.
187
+ registry[eventName].off();
188
+ delete registry[eventName];
189
+ }
190
+
191
+ if (enabled) {
192
+ registry[eventName] = on(elem, eventName, opts.itemMatch, ev => {
193
+ if (opts.verbose) {
194
+ console.debug('delegated',
195
+ {eventName, meth, event: ev, opts, list: this});
196
+ }
197
+ this[meth](ev);
198
+ }, {off: true});
199
+ }
200
+ }
201
+
202
+ this.registration.set(elem, registry);
203
+
204
+ return this;
45
205
  }
46
206
 
207
+ /**
208
+ * Make one or more items draggable.
209
+ *
210
+ * If you are using event delegation and your item elements already have the
211
+ * `draggable` attribute set, you shouldn't have to call this at all.
212
+ *
213
+ * If you are **NOT** using delegation this method will have to be called
214
+ * for every item you want to be able to be dragged!
215
+ *
216
+ * @param {boolean} [enabled=true] Make the items draggable?
217
+ *
218
+ * The string of this value will be used to set the `draggable` attribute
219
+ * on each of the items.
220
+ *
221
+ * If `this.opts.delegated` is false then this argument will determine
222
+ * if direct event handlers should be added or removed from each item.
223
+ *
224
+ * @param {...(Element|string)} [elems] Item elements to register.
225
+ *
226
+ * This can be used if you are adding dynamic items.
227
+ *
228
+ * If you don't pass any arguments here then `this.listItems` will be
229
+ * used to find the items elements.
230
+ *
231
+ * @returns {DraggableList} this instance.
232
+ */
47
233
  makeDraggable(enabled=true, ...elems) {
48
234
  if (elems.length === 0) {
49
235
  elems = this.listItems;
50
236
  }
51
237
 
238
+ let delegated = this.opts.delegated;
52
239
  let verbose = this.opts.verbose;
53
240
 
54
241
  for (let elem of elems) {
@@ -62,21 +249,32 @@ export class DraggableList {
62
249
 
63
250
  elem.setAttribute('draggable', enabled.toString());
64
251
 
65
- // Doing this here as delegated events on the list element didn't work.
66
- // which is kind of annoying to be honest, but everything about the
67
- // drag-and-drop API is. Like having to use a middleware to make it
68
- // work with touch screens, how did that get fucked up so badly?
69
-
70
- for (let meth of DRAG_METHODS)
71
- {
72
- let eventName = meth.replace('handle','').toLowerCase();
73
- on(elem, eventName, ev => {
74
- if (verbose) {
75
- console.debug({eventName, meth, event: ev, elem, list: this});
252
+ if (!delegated) {
253
+ let registry = this.registration.get(elem) ?? {};
254
+
255
+ for (let meth of DRAG_METHODS)
256
+ {
257
+ let eventName = meth.replace('handle','').toLowerCase();
258
+
259
+ if (isObj(registry[eventName])) {
260
+ // Unregister an existing event handler.
261
+ registry[eventName].off();
262
+ delete registry[eventName];
76
263
  }
77
- this[meth](ev);
78
- });
264
+
265
+ if (enabled) { // Register an event handler.
266
+ registry[eventName] = on(elem, eventName, ev => {
267
+ if (verbose) {
268
+ console.debug('direct',
269
+ {eventName, meth, event: ev, elem, list: this});
270
+ }
271
+ this[meth](ev);
272
+ }, {off: true});
273
+ }
274
+ }
275
+ this.registration.set(elem, registry);
79
276
  }
277
+
80
278
  }
81
279
 
82
280
  return this;
@@ -144,10 +342,58 @@ export class DraggableList {
144
342
 
145
343
  export default DraggableList;
146
344
 
345
+ /**
346
+ * A Preset that adds a CSS class to list items when another items is being
347
+ * dragged over it. It provides `onEnter`, `onLeave`, and `onEnd` handlers.
348
+ *
349
+ * To set the CSS class, it adds an extra option called `dragOverClass`,
350
+ * which is a string, and defaults to `'drag-over'`.
351
+ *
352
+ * If you want to use a Preset but also add your own functionality,
353
+ * you can chain handlers by using the call() method. For example:
354
+ *
355
+ * ```js
356
+ * import {DraggableList, ClassyPreset} from '@lumjs/web-draggable-list';
357
+ *
358
+ * // A test to see if the element is NOT a drop target.
359
+ * const noDrop = event => event.target.classList.has('no-drop');
360
+ *
361
+ * let dlist = new DraggableList('#my-list', ClassyPreset, {
362
+ * dragOverClass: 'drop-target',
363
+ * onEnter(event) {
364
+ * if (noDrop(event)) return;
365
+ * ClassyPreset.onEnter.call(this, event);
366
+ * },
367
+ * onDrop(event) {
368
+ * if (noDrop(event)) return;
369
+ * myApp.itemDropped(event)
370
+ * },
371
+ * });
372
+ * ```
373
+ * That example is rather limited, as you'd also want to change the dropEffect
374
+ * in the `onOver()` handler, but anyway, hopefully it gives you an idea.
375
+ *
376
+ * See `demo/main.src.js` for a more realistic example.
377
+ */
378
+ export const ClassyPreset = {
379
+ dragOverClass: 'drag-over',
380
+ onEnter(ev) {
381
+ ev.target.classList.add(this.opts.dragOverClass);
382
+ },
383
+ onLeave(ev) {
384
+ ev.target.classList.remove(this.opts.dragOverClass);
385
+ },
386
+ onEnd() {
387
+ let doclass = this.opts.dragOverClass;
388
+ this.listItems.forEach(item => item.classList.remove(doclass));
389
+ },
390
+ }
391
+
147
392
  /**
148
393
  * A Drag Event handler
149
394
  * @callback DragEventHandler
150
395
  * @param {DragEvent} event - The event being handled.
396
+ * @this {DraggableList}
151
397
  */
152
398
 
153
399
  /**
@@ -156,4 +402,5 @@ export default DraggableList;
156
402
  * @param {DragEvent} event - The event being handled; with extra metadata.
157
403
  * @param {number} event.oldpos - The original position of the dragged item.
158
404
  * @param {number} event.newpso - The new position of the dragged item.
405
+ * @this {DraggableList}
159
406
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumjs/web-draggable-list",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./index.js",