@lumjs/web-draggable-list 1.1.0 → 1.2.1

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 +251 -20
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1,11 +1,19 @@
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 { enableDragDropTouch, WC };
5
9
  export const { indexOf, on } = WC.ez;
6
10
 
11
+ const { isObj } = core.types;
12
+
7
13
  const DEF_OPTS = {
14
+ autoRegister: null,
8
15
  delegated: true,
16
+ enableTouch: true,
9
17
  itemMatch: 'li',
10
18
  itemSelector: '& > li',
11
19
  onDrop() {},
@@ -26,9 +34,98 @@ const DRAG_METHODS = [
26
34
  'handleDragEnd',
27
35
  ]
28
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
+ */
29
41
  export class DraggableList {
30
- constructor(listElem, opts) {
31
- 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);
32
129
  if (typeof listElem === 'string') {
33
130
  listElem = document.querySelector(listElem);
34
131
  }
@@ -39,26 +136,100 @@ export class DraggableList {
39
136
 
40
137
  this.listElem = listElem;
41
138
  this.dragging = null;
42
- 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
+ }
152
+ }
153
+
154
+ /**
155
+ * Accessor to get a NodeList containing the list items.
156
+ */
157
+ get listItems() {
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
+ }
43
190
 
44
- if (opts.delegated) {
45
- for (let meth of DRAG_METHODS)
46
- {
47
- let eventName = meth.replace('handle','').toLowerCase();
48
- on(listElem, eventName, opts.itemMatch, ev => {
191
+ if (enabled) {
192
+ registry[eventName] = on(elem, eventName, opts.itemMatch, ev => {
49
193
  if (opts.verbose) {
50
- console.debug('delegated', {eventName, meth, event: ev, opts, list: this});
194
+ console.debug('delegated',
195
+ {eventName, meth, event: ev, opts, list: this});
51
196
  }
52
197
  this[meth](ev);
53
- });
198
+ }, {off: true});
54
199
  }
55
200
  }
56
- }
57
201
 
58
- get listItems() {
59
- return this.listElem.querySelectorAll(this.opts.itemSelector);
202
+ this.registration.set(elem, registry);
203
+
204
+ return this;
60
205
  }
61
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
+ */
62
233
  makeDraggable(enabled=true, ...elems) {
63
234
  if (elems.length === 0) {
64
235
  elems = this.listItems;
@@ -79,16 +250,29 @@ export class DraggableList {
79
250
  elem.setAttribute('draggable', enabled.toString());
80
251
 
81
252
  if (!delegated) {
253
+ let registry = this.registration.get(elem) ?? {};
254
+
82
255
  for (let meth of DRAG_METHODS)
83
256
  {
84
257
  let eventName = meth.replace('handle','').toLowerCase();
85
- on(elem, eventName, ev => {
86
- if (verbose) {
87
- console.debug('direct', {eventName, meth, event: ev, elem, list: this});
88
- }
89
- this[meth](ev);
90
- });
258
+
259
+ if (isObj(registry[eventName])) {
260
+ // Unregister an existing event handler.
261
+ registry[eventName].off();
262
+ delete registry[eventName];
263
+ }
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
+ }
91
274
  }
275
+ this.registration.set(elem, registry);
92
276
  }
93
277
 
94
278
  }
@@ -158,10 +342,56 @@ export class DraggableList {
158
342
 
159
343
  export default DraggableList;
160
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
+ * let dlist = new DraggableList('#my-list', ClassyPreset, {
359
+ * dragOverClass: 'drop-target',
360
+ * onEnter(event) {
361
+ * // Run stuff before calling the preset onEnter.
362
+ * ClassyPreset.onEnter.call(this, event);
363
+ * // Run more stuff after the preset onEnter.
364
+ * },
365
+ * });
366
+ * ```
367
+ *
368
+ */
369
+ export const ClassyPreset = {
370
+ dragOverClass: 'drag-over',
371
+ onStart() {
372
+ this.entered = 0;
373
+ },
374
+ onEnter(ev) {
375
+ if (!this.entered++) {
376
+ ev.target.classList.add(this.opts.dragOverClass);
377
+ }
378
+ },
379
+ onLeave(ev) {
380
+ if (!--this.entered) {
381
+ ev.target.classList.remove(this.opts.dragOverClass);
382
+ }
383
+ },
384
+ onEnd() {
385
+ let doclass = this.opts.dragOverClass;
386
+ this.listItems.forEach(item => item.classList.remove(doclass));
387
+ },
388
+ }
389
+
161
390
  /**
162
391
  * A Drag Event handler
163
392
  * @callback DragEventHandler
164
393
  * @param {DragEvent} event - The event being handled.
394
+ * @this {DraggableList}
165
395
  */
166
396
 
167
397
  /**
@@ -170,4 +400,5 @@ export default DraggableList;
170
400
  * @param {DragEvent} event - The event being handled; with extra metadata.
171
401
  * @param {number} event.oldpos - The original position of the dragged item.
172
402
  * @param {number} event.newpso - The new position of the dragged item.
403
+ * @this {DraggableList}
173
404
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumjs/web-draggable-list",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./index.js",