bullet_train-sortable 1.28.0 → 1.30.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50f35482507455ea2ceb8aeb407f6171670247946618feaa145486eb85a8eeb2
4
- data.tar.gz: ef3b0ac7cd89d8f912d3a47689a4fd7a780d1171455627f14e369ae9a6e27013
3
+ metadata.gz: a042c5807b320f70fa014128ac0e84f4a3e6e3c58bb61d43a7c4a0ef8647bfec
4
+ data.tar.gz: a7389b72a4490bbd42d11e0ff56b44d1e40c621858a5c9f262fe066beb7b295c
5
5
  SHA512:
6
- metadata.gz: 1d52527697a9a158bdfdca8539db42080f3922689d92efe8883e51f8567bbc5e40f9beced84b510a82cdd13e9c675504e87c849c051431c39fd09628c1e2b858
7
- data.tar.gz: 30632f1c7b22539ecb7527d82bb84ec82b307db424f53bb7e2546e80d3542a0ae0f949ef117469cdf58380c3c6b008a84adc22d9f8764bc8a1e21ca0c87027ec
6
+ metadata.gz: 01131f75504043564573448aa18d84cf7130d64964f754f7f4861ef6fd1d6f1baa635d3775a807ccf8e91bb1068ec875c8207d3ef78b12810e50574bb26f2849
7
+ data.tar.gz: e70c3d68ec7b2df0c237267eab2a917e13d4e0cd0b9f58ec019a6d5f27b5b747e2fdac73d6e8db4e49b9a2a9289cd9ae08be3f78c7a092f01efe186d5efd4fea
@@ -0,0 +1,88 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { post } from '@rails/request.js'
3
+ import jquery from "jquery";
4
+ require("dragula/dist/dragula.min.css")
5
+
6
+ import dragula from 'dragula';
7
+
8
+ export default class extends Controller {
9
+ static values = {
10
+ reorderPath: String,
11
+ saveOnReorder: { type: Boolean, default: true }
12
+ }
13
+
14
+ // will be reissued as native dom events name prepended with 'sortable:' e.g. 'sortable:drag', 'sortable:drop', etc
15
+ static pluginEventsToReissue = [ "drag", "dragend", "drop", "cancel", "remove", "shadow", "over", "out", "cloned" ]
16
+
17
+ initialize() {
18
+ if (window.jQuery === undefined) {
19
+ window.jQuery = jquery // required for select2 used for time zone select, but we also use global jQuery throughout below
20
+ }
21
+ }
22
+
23
+ connect() {
24
+ if (!this.hasReorderPathValue) { return }
25
+ this.initPluginInstance()
26
+ }
27
+
28
+ disconnect() {
29
+ this.teardownPluginInstance()
30
+ }
31
+
32
+ initPluginInstance() {
33
+ const self = this
34
+ this.plugin = dragula([this.element], {
35
+ moves: function(el, container, handle) {
36
+ var $handles = jQuery(el).find('.reorder-handle')
37
+ if ($handles.length) {
38
+ return !!jQuery(handle).closest('.reorder-handle').length
39
+ } else {
40
+ if (!jQuery(handle).closest('.undraggable').length) {
41
+ return self.element === container
42
+ } else {
43
+ return false
44
+ }
45
+ }
46
+ },
47
+ accepts: function (el, target, source, sibling) {
48
+ if (jQuery(sibling).hasClass('undraggable') && jQuery(sibling).prev().hasClass('undraggable')) {
49
+ return false
50
+ } else {
51
+ return true
52
+ }
53
+ },
54
+ }).on('drop', function (el) {
55
+ // save order here.
56
+ if (self.saveOnReorderValue) {
57
+ self.saveSortOrder()
58
+ }
59
+ }).on('over', function (el, container) {
60
+ // deselect any text fields, or else things go slow!
61
+ jQuery(document.activeElement).blur()
62
+ })
63
+
64
+ this.initReissuePluginEventsAsNativeEvents()
65
+ }
66
+
67
+ initReissuePluginEventsAsNativeEvents() {
68
+ this.constructor.pluginEventsToReissue.forEach((eventName) => {
69
+ this.plugin.on(eventName, (...args) => {
70
+ this.dispatch(eventName, { detail: { plugin: 'dragula', type: eventName, args: args }})
71
+ })
72
+ })
73
+ }
74
+
75
+ teardownPluginInstance() {
76
+ if (this.plugin === undefined) { return }
77
+
78
+ // revert to original markup, remove any event listeners
79
+ this.plugin.destroy()
80
+ }
81
+
82
+ saveSortOrder() {
83
+ var idsInOrder = Array.from(this.element.childNodes).map((el) => { return parseInt(el.dataset?.id) });
84
+
85
+ post(this.reorderPathValue, { body: JSON.stringify({ids_in_order: idsInOrder}) })
86
+ }
87
+
88
+ }
@@ -15,4 +15,4 @@ export const controllerDefinitions = [
15
15
 
16
16
  export {
17
17
  SortableController
18
- }
18
+ }
@@ -1,88 +1,327 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
2
  import { post } from '@rails/request.js'
3
- import jquery from "jquery";
4
- require("dragula/dist/dragula.min.css")
5
-
6
- import dragula from 'dragula';
7
3
 
4
+ // Connects to data-controller="sortable"
8
5
  export default class extends Controller {
9
6
  static values = {
10
7
  reorderPath: String,
11
8
  saveOnReorder: { type: Boolean, default: true }
12
9
  }
13
-
14
- // will be reissued as native dom events name prepended with 'sortable:' e.g. 'sortable:drag', 'sortable:drop', etc
15
- static pluginEventsToReissue = [ "drag", "dragend", "drop", "cancel", "remove", "shadow", "over", "out", "cloned" ]
10
+ static classes = ["activeDropzone", "activeItem", "dropTarget"];
11
+ static targets = [ "handle"];
16
12
 
17
- initialize() {
18
- if (window.jQuery === undefined) {
19
- window.jQuery = jquery // required for select2 used for time zone select, but we also use global jQuery throughout below
20
- }
13
+ async saveSortOrder(idsInOrder) {
14
+ await post(this.reorderPathValue, { body: JSON.stringify({ids_in_order: idsInOrder}) })
21
15
  }
22
16
 
23
17
  connect() {
24
- if (!this.hasReorderPathValue) { return }
25
- this.initPluginInstance()
18
+ const saveOrderCallback = this.saveOnReorderValue ? this.saveSortOrder.bind(this) : null;
19
+ this.sortingPlugin = new SortableTable(
20
+ this.element,
21
+ saveOrderCallback,
22
+ this.handleTargets,
23
+ {
24
+ activeDropzoneClasses: this.activeDropzoneClasses,
25
+ activeItemClasses: this.activeItemClasses,
26
+ dropTargetClasses: this.dropTargetClasses
27
+ }
28
+ );
26
29
  }
27
30
 
28
31
  disconnect() {
29
- this.teardownPluginInstance()
30
- }
31
-
32
- initPluginInstance() {
33
- const self = this
34
- this.plugin = dragula([this.element], {
35
- moves: function(el, container, handle) {
36
- var $handles = jQuery(el).find('.reorder-handle')
37
- if ($handles.length) {
38
- return !!jQuery(handle).closest('.reorder-handle').length
39
- } else {
40
- if (!jQuery(handle).closest('.undraggable').length) {
41
- return self.element === container
42
- } else {
43
- return false
44
- }
32
+ this.sortingPlugin.destroy();
33
+ }
34
+ }
35
+
36
+ function getDataNode(node) {
37
+ return node.closest("[data-id]");
38
+ }
39
+
40
+ function getHandleNode(node) {
41
+ return node.closest("[data-sortable-target='handle']");
42
+ }
43
+
44
+ function getMetaValue(name) {
45
+ const element = document.head.querySelector(`meta[name="${name}"]`);
46
+ return element.getAttribute("content");
47
+ }
48
+
49
+ class SortableTable{
50
+ // We'll emit events using this prefix. `sortable:drag` & `sortable:drop` for instance.
51
+ static eventPrefix = "sortable";
52
+ // These are defaults so that we don't have to require people to update their templates.
53
+ // If the template does contain values for any of these the template values will be used instead.
54
+ static defaultClasses = {
55
+ "activeDropzoneClasses": "border-dashed bg-gray-50 border-slate-400",
56
+ "activeItemClasses": "shadow bg-white bg-white *:bg-white opacity-100 *:opacity-100",
57
+ "dropTargetClasses": "shadow-inner shadow-gray-500 hover:shadow-inner bg-gray-100 *:opacity-0 *:bg-gray-100"
58
+ };
59
+
60
+ constructor(tbodyElement, saveSortOrder, handleTargets, styles, customEventPrefix){
61
+ this.element = tbodyElement;
62
+ this.saveSortOrder = saveSortOrder;
63
+ this.handleTargets = handleTargets;
64
+
65
+ this.activeDropzoneClassesWithDefaults = styles.activeDropzoneClasses.length == 0 ? this.constructor.defaultClasses["activeDropzoneClasses"].split(" ") : styles.activeDropzoneClasses;
66
+ this.activeItemClassesWithDefaults = styles.activeItemClasses.length == 0 ? this.constructor.defaultClasses["activeItemClasses"].split(" ") : styles.activeItemClasses;
67
+ this.dropTargetClassesWithDefaults = styles.dropTargetClasses.length == 0 ? this.constructor.defaultClasses["dropTargetClasses"].split(" ") : styles.dropTargetClasses;
68
+ this.eventPrefixWithDefaults = customEventPrefix ? customEventPrefix : this.constructor.eventPrefix;
69
+
70
+ this.element.addEventListener('dragstart', this.dragstart.bind(this));
71
+ this.element.addEventListener('dragover', this.dragover.bind(this));
72
+ this.element.addEventListener('dragenter', this.dragenter.bind(this));
73
+ this.element.addEventListener('dragleave', this.dragleave.bind(this));
74
+ this.element.addEventListener('dragend', this.dragend.bind(this));
75
+ this.element.addEventListener('drop', this.drop.bind(this));
76
+
77
+ if(this.handleTargets.length == 0){
78
+ this.addDragHandles();
79
+ }
80
+
81
+ this.element.addEventListener('mousedown', this.dragHandleMouseDown.bind(this));
82
+ this.element.addEventListener('mouseup', this.dragHandleMouseUp.bind(this));
83
+
84
+ // We set this when we've detected `mousedown` in a handle. Then all of the `drag*` handlers
85
+ // bail out if we don't have a draggable row. This prevents problems and weird behavior if you
86
+ // drag something other than the handle. Like highlighted text, or a link, for instance.
87
+ this.aRowIsDraggable = false;
88
+ }
89
+
90
+ destroy(){
91
+ this.element.removeEventListener('dragstart', this.dragstart.bind(this));
92
+ this.element.removeEventListener('dragover', this.dragover.bind(this));
93
+ this.element.removeEventListener('dragenter', this.dragenter.bind(this));
94
+ this.element.removeEventListener('dragleave', this.dragleave.bind(this));
95
+ this.element.removeEventListener('dragend', this.dragend.bind(this));
96
+ this.element.removeEventListener('drop', this.drop.bind(this));
97
+
98
+ this.element.removeEventListener('mousedown', this.dragHandleMouseDown.bind(this));
99
+ this.element.removeEventListener('mouseup', this.dragHandleMouseUp.bind(this));
100
+ }
101
+
102
+ dragHandleMouseDown(event){
103
+ const handle = getHandleNode(event.target);
104
+ if(!handle){
105
+ return;
106
+ }
107
+ const draggableItem = getDataNode(event.target);
108
+ if(draggableItem){
109
+ draggableItem.setAttribute('draggable', true);
110
+ this.aRowIsDraggable = true;
111
+ }
112
+ }
113
+
114
+ dragHandleMouseUp(event){
115
+ const handle = getHandleNode(event.target);
116
+ if(!handle){
117
+ return;
118
+ }
119
+ const draggableItem = getDataNode(event.target);
120
+ if(draggableItem){
121
+ draggableItem.setAttribute('draggable', false);
122
+ this.aRowIsDraggable = false;
123
+ }
124
+ }
125
+
126
+ dragstart(event) {
127
+ if(!this.aRowIsDraggable){
128
+ return;
129
+ }
130
+ this.element.classList.add(...this.activeDropzoneClassesWithDefaults);
131
+ const draggableItem = getDataNode(event.target);
132
+ draggableItem.classList.add(...this.activeItemClassesWithDefaults);
133
+ event.dataTransfer.setData(
134
+ "application/drag-key",
135
+ draggableItem.dataset.id
136
+ );
137
+ event.dataTransfer.effectAllowed = "move";
138
+ // For most browsers we could rely on the dataTransfer.setData call above,
139
+ // but Safari doesn't seem to allow us access to that data at any time other
140
+ // than during a drop. But we need it during dragenter to reorder the list
141
+ // as the drag happens. So, we just stash the value here and then use it later.
142
+ this.draggingDataId = draggableItem.dataset.id;
143
+
144
+ this.dispatch('start', { detail: { type: 'start', args: [draggableItem, this.element] }})
145
+ // We're dispatching drag here in addition to start to retain backwards compatibility with dragula.js.
146
+ // It emits a single 'drag' event when an item is first dragged, but not on each movement thereafter.
147
+ this.dispatch('drag', { detail: { type: 'drag', args: [draggableItem, this.element] }})
148
+ }
149
+
150
+ dragover(event) {
151
+ if(!this.aRowIsDraggable){
152
+ return;
153
+ }
154
+ event.preventDefault();
155
+ return true;
156
+ }
157
+
158
+ dragenter(event) {
159
+ if(!this.aRowIsDraggable){
160
+ return;
161
+ }
162
+ let parent = getDataNode(event.target);
163
+
164
+ // We keep a count of the `dragenter` events for the row being dragged to fix jank. When dragging between cells
165
+ // (or cell content) within a row a dragenter event is fired before the dragleave event from the previous cell.
166
+ // If we removed the activeItemClasses when the dragleave happens then the UI doesn't match expectations.
167
+ if(parent.dataset.dragEnterCount){
168
+ parent.dataset.dragEnterCount = parseInt(parent.dataset.dragEnterCount) + 1;
169
+ }else{
170
+ parent.dataset.dragEnterCount = 1;
171
+ }
172
+
173
+ if (parent != null && parent.dataset.id != null) {
174
+ parent.classList.add(...this.dropTargetClassesWithDefaults);
175
+ var data = this.draggingDataId;
176
+ const draggedItem = this.element.querySelector(
177
+ `[data-id='${data}']`
178
+ );
179
+
180
+ if (draggedItem) {
181
+ draggedItem.classList.remove(...this.activeItemClassesWithDefaults);
182
+
183
+ let dispatchEvent = false;
184
+
185
+ if (parent.compareDocumentPosition(draggedItem) & Node.DOCUMENT_POSITION_FOLLOWING) {
186
+ let result = parent.insertAdjacentElement( "beforebegin", draggedItem);
187
+ dispatchEvent = true;
188
+ } else if (parent.compareDocumentPosition(draggedItem) & Node.DOCUMENT_POSITION_PRECEDING) {
189
+ let result = parent.insertAdjacentElement("afterend", draggedItem);
190
+ dispatchEvent = true;
45
191
  }
46
- },
47
- accepts: function (el, target, source, sibling) {
48
- if (jQuery(sibling).hasClass('undraggable') && jQuery(sibling).prev().hasClass('undraggable')) {
49
- return false
50
- } else {
51
- return true
192
+
193
+ if(dispatchEvent){
194
+ this.dispatch('reordered', { detail: { type: 'shadow', args: [draggedItem, this.element, this.element] }});
195
+ // We're dispatching 'shadow' here to retain backwards compatibility with dragula.js.
196
+ // It emits a 'shadow' event when the items in the list are rearranged mid-drag.
197
+ // TODO: This is firing more often than dragula fires it. Is that a problem?
198
+ this.dispatch('shadow', { detail: { type: 'shadow', args: [draggedItem, this.element, this.element] }});
52
199
  }
53
- },
54
- }).on('drop', function (el) {
55
- // save order here.
56
- if (self.saveOnReorderValue) {
57
- self.saveSortOrder()
200
+
58
201
  }
59
- }).on('over', function (el, container) {
60
- // deselect any text fields, or else things go slow!
61
- jQuery(document.activeElement).blur()
62
- })
63
-
64
- this.initReissuePluginEventsAsNativeEvents()
202
+ event.preventDefault();
203
+ }
65
204
  }
66
-
67
- initReissuePluginEventsAsNativeEvents() {
68
- this.constructor.pluginEventsToReissue.forEach((eventName) => {
69
- this.plugin.on(eventName, (...args) => {
70
- this.dispatch(eventName, { detail: { plugin: 'dragula', type: eventName, args: args }})
71
- })
72
- })
205
+
206
+ dragleave(event) {
207
+ if(!this.aRowIsDraggable){
208
+ return;
209
+ }
210
+ let parent = getDataNode(event.target);
211
+
212
+ if(parent.dataset.dragEnterCount > 0){
213
+ parent.dataset.dragEnterCount = parseInt(parent.dataset.dragEnterCount) - 1;
214
+ }
215
+
216
+ if (parent != null && parent.dataset.id != null && parent.dataset.dragEnterCount == 0) {
217
+ parent.classList.remove(...this.dropTargetClassesWithDefaults);
218
+ event.preventDefault();
219
+ }
73
220
  }
74
221
 
75
- teardownPluginInstance() {
76
- if (this.plugin === undefined) { return }
222
+ async drop(event) {
223
+ if(!this.aRowIsDraggable){
224
+ return;
225
+ }
226
+ this.element.classList.remove(...this.activeDropzoneClassesWithDefaults);
227
+
228
+ const dropTarget = getDataNode(event.target);
229
+ dropTarget.classList.remove(...this.dropTargetClassesWithDefaults);
77
230
 
78
- // revert to original markup, remove any event listeners
79
- this.plugin.destroy()
231
+ var data = this.draggingDataId;
232
+ const draggedItem = this.element.querySelector(
233
+ `[data-id='${data}']`
234
+ );
235
+
236
+ if (draggedItem) {
237
+ draggedItem.classList.remove(...this.activeItemClassesWithDefaults);
238
+
239
+ if (
240
+ dropTarget.compareDocumentPosition(draggedItem) &
241
+ Node.DOCUMENT_POSITION_FOLLOWING
242
+ ) {
243
+ let result = dropTarget.insertAdjacentElement(
244
+ "beforebegin",
245
+ draggedItem
246
+ );
247
+ } else if (
248
+ dropTarget.compareDocumentPosition(draggedItem) &
249
+ Node.DOCUMENT_POSITION_PRECEDING
250
+ ) {
251
+ let result = dropTarget.insertAdjacentElement("afterend", draggedItem);
252
+ }
253
+
254
+ if (this.saveSortOrder) {
255
+ var idsInOrder = Array.from(this.element.childNodes).map((el) => { return el.dataset?.id ? parseInt(el.dataset?.id) : null });
256
+ idsInOrder = idsInOrder.filter(element => element !== null);
257
+ await this.saveSortOrder(idsInOrder);
258
+ this.dispatch('saved', { detail: { type: 'saved', args: [this.element] }})
259
+ }
260
+
261
+ // TODO: This fires more often than dragula fires it. Dragula does not fire this when an item was dragged but not moved/reorded.
262
+ // Instead dragula fires a `cancel` event. But we're firing a `drop` and no `cancel` in that situation.
263
+ this.dispatch('drop', { detail: { type: 'drop', args: [draggedItem, this.element, this.element, draggedItem.nextElementSibling] }})
264
+ }
265
+ event.preventDefault();
266
+ }
267
+
268
+ dragend(event) {
269
+ if(!this.aRowIsDraggable){
270
+ return;
271
+ }
272
+ this.element.classList.remove(...this.activeDropzoneClassesWithDefaults);
273
+
274
+ const draggableItem = getDataNode(event.target);
275
+ draggableItem.setAttribute('draggable', false);
276
+ draggableItem.dataset.dragEnterCount = 0;
277
+
278
+ this.dispatch('end', { detail: { type: 'end', args: [draggableItem, this.element] }})
279
+ // We emit dragend here as well to maintain some backwards compatiblity with the old dragula controller.
280
+ this.dispatch('dragend', { detail: { type: 'dragend', args: [draggableItem, this.element] }})
281
+ }
282
+
283
+ addDragHandles(){
284
+ // Here we assume that this controller is connected to a tbody element
285
+ const table = this.element.parentNode;
286
+ const thead = table.querySelector('thead');
287
+ const headRow = thead.querySelector('tr');
288
+ const newTh = document.createElement('th');
289
+ newTh.classList.add(...'w-6'.split(' '))
290
+ headRow.prepend(newTh);
291
+
292
+ const draggables = this.element.querySelectorAll('tr');
293
+ for (const draggable of draggables) {
294
+ const newCell = document.createElement('td');
295
+ newCell.dataset.sortableTarget = 'handle';
296
+ newCell.classList.add(...'cursor-grab active:cursor-grabbing'.split(' '));
297
+
298
+ const icon = document.createElement('i');
299
+ icon.classList.add(...'ti ti-menu opacity-25 group-hover:opacity-100'.split(' '));
300
+
301
+ newCell.append(icon);
302
+ draggable.prepend(newCell);
303
+ this.handleTargets.push(newCell);
304
+ }
305
+ }
306
+
307
+ dispatch(eventName,data){
308
+ const fullEventName = this.eventPrefixWithDefaults + ":" + eventName;
309
+ const event = new CustomEvent(fullEventName, data);
310
+ this.element.dispatchEvent(event);
80
311
  }
81
-
82
- saveSortOrder() {
83
- var idsInOrder = Array.from(this.element.childNodes).map((el) => { return parseInt(el.dataset?.id) });
84
-
85
- post(this.reorderPathValue, { body: JSON.stringify({ids_in_order: idsInOrder}) })
312
+
313
+ // TODO: I'm not sure this is adequate. I think we may need to "manually" dispatch these from within the
314
+ // approriate event handles so that we can add more info to `args`. For instance, the "drop" event may
315
+ // need to include the sibling that the dropped element was dropped in front of. Related, do people actually
316
+ // use these re-issued events?
317
+ /*
318
+ initReissuePluginEventsAsNativeEvents() {
319
+ this.constructor.pluginEventsToReissue.forEach((eventName) => {
320
+ this.element.addEventListener(eventName, (...args) => {
321
+ this.dispatch(eventName, { detail: { type: eventName, args: args }})
322
+ })
323
+ })
86
324
  }
325
+ */
87
326
 
88
- }
327
+ }
@@ -1,5 +1,5 @@
1
1
  module BulletTrain
2
2
  module Sortable
3
- VERSION = "1.28.0"
3
+ VERSION = "1.30.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bullet_train-sortable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.28.0
4
+ version: 1.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Culver
@@ -61,8 +61,8 @@ files:
61
61
  - MIT-LICENSE
62
62
  - README.md
63
63
  - Rakefile
64
- - app/assets/config/bullet_train_sortable_manifest.js
65
64
  - app/controllers/concerns/sortable_actions.rb
65
+ - app/javascript/controllers/dragula-sortable_controller.js
66
66
  - app/javascript/controllers/index.js
67
67
  - app/javascript/controllers/sortable_controller.js
68
68
  - app/javascript/index.js
File without changes