Skip to content

Commit 4bd7768

Browse files
committed
[FEATURE] Inplement cancelable dnd op
1 parent ecfe1ba commit 4bd7768

File tree

8 files changed

+201
-39
lines changed

8 files changed

+201
-39
lines changed

addon/components/sortable-js.js

+46-24
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import Component from '@glimmer/component';
22
import Sortable from 'sortablejs';
3-
import { bind, scheduleOnce } from '@ember/runloop';
3+
import { bind, next } from '@ember/runloop';
44
import { action } from '@ember/object';
5-
import { next } from '@ember/runloop';
65
import { tracked } from '@glimmer/tracking';
76
import { move, insertAt, removeFrom } from 'ember-sortablejs/utils/array-utils';
87
import { inject as service } from '@ember/service';
@@ -12,9 +11,11 @@ export default class SortableJsComponent extends Component {
1211

1312
@tracked list = [];
1413

14+
cachedList = null;
15+
hasUpdatedList = false; // Used to prevent unwanted renders. Probably there's a better way to do this.
1516
#sortableContainer = null;
16-
#sortableInstance = null;
17-
#internalEvents = [
17+
sortableInstance = null;
18+
internalEvents = [
1819
'onStart',
1920
'onAdd',
2021
'onUpdate',
@@ -66,7 +67,7 @@ export default class SortableJsComponent extends Component {
6667
this.#sortableContainer = element;
6768

6869
next(this, () => {
69-
this.#sortableInstance = Sortable.create(element, options);
70+
this.sortableInstance = Sortable.create(element, options);
7071
this.setupEventHandlers();
7172
this.setupInternalEventHandlers();
7273
this.setList();
@@ -78,8 +79,19 @@ export default class SortableJsComponent extends Component {
7879
this.list = [...(this.args.items || [])];
7980
}
8081

82+
@action
83+
cancelDnD() {
84+
if (this.cachedList) {
85+
this.list = [...this.cachedList];
86+
this.cachedList = null;
87+
this.dragStore.dragAddInstance?.cancelDnD();
88+
}
89+
this.dragStore.reset();
90+
}
91+
8192
willDestroy() {
82-
this.#sortableInstance.destroy();
93+
this.sortableInstance.destroy();
94+
this.dragStore.reset();
8395
}
8496

8597
onUpdate(evt) {
@@ -88,7 +100,8 @@ export default class SortableJsComponent extends Component {
88100
oldDraggableIndex,
89101
} = evt;
90102

91-
this.list = move(this.list, oldDraggableIndex, newDraggableIndex);
103+
this.sync(evt.item, move(this.list, oldDraggableIndex, newDraggableIndex));
104+
this.hasUpdatedList = true;
92105
this.args?.onUpdate?.(evt);
93106
}
94107

@@ -98,57 +111,66 @@ export default class SortableJsComponent extends Component {
98111
} = evt;
99112

100113
if (evt.pullMode !== 'clone') {
101-
this.list = removeFrom(this.list, oldDraggableIndex);
114+
this.sync(evt.item, removeFrom(this.list, oldDraggableIndex));
115+
this.hasUpdatedList = true;
102116
}
103117

104118
this.args?.onRemove?.(evt);
105119
}
106120

107121
onAdd(evt) {
108-
evt.item.remove(); // without this DOM is wrong
122+
this.cachedList = [...this.list];
123+
this.dragStore.dragAddInstance = this;
109124
const {
110125
oldDraggableIndex,
111126
newDraggableIndex,
112127
} = evt;
113-
const oldItem = this.dragStore.dragging.list[oldDraggableIndex];
114-
this.list = insertAt(this.list, newDraggableIndex, oldItem)
128+
const oldItem = this.dragStore.dragStartInstance.list[oldDraggableIndex];
129+
130+
this.sync(evt.item, insertAt(this.list, newDraggableIndex, oldItem));
115131
this.args?.onAdd?.(evt);
116132
}
117133

118134
onStart(evt) {
119-
this.dragStore.dragging = this;
135+
this.cachedList = [...this.list];
136+
this.dragStore.dragStartInstance = this;
120137
this.args?.onStart?.(evt);
121138
}
122139

123-
onEnd(evt) {
124-
this.args?.onEnd?.(evt);
125-
this.dragStore.dragging = null;
140+
onEnd(evt, ) {
141+
if (!this.hasUpdatedList) {
142+
this.sync(evt.item, this.list);
143+
}
144+
145+
this.args?.onEnd?.(evt, this.cancelDnD);
146+
this.hasUpdatedList = false;
147+
}
148+
149+
sync(element, changedArray) {
150+
element.remove();
151+
this.list = [...changedArray];
126152
}
127153

128154
setupEventHandlers() {
129155
this.#events.forEach(eventName => {
130156
const action = this.args[eventName];
131157
if (typeof action === 'function') {
132-
this.#sortableInstance.option(eventName, bind(this, 'performExternalAction', eventName));
158+
this.sortableInstance.option(eventName, bind(this, 'performExternalAction', eventName));
133159
}
134160
});
135161
}
136162

137163
setupInternalEventHandlers() {
138-
this.#internalEvents.forEach(eventName => {
139-
this.#sortableInstance.option(eventName, bind(this, this[eventName]));
164+
this.internalEvents.forEach(eventName => {
165+
this.sortableInstance.option(eventName, bind(this, this[eventName]));
140166
});
141167
}
142168

143169
performExternalAction(actionName, ...args) {
144-
let action = this.args[actionName];
145-
146-
if (typeof action === 'function') {
147-
action(...args, this.sortable);
148-
}
170+
this.args[actionName]?.(...args)
149171
}
150172

151173
setOption(option, value) {
152-
this.#sortableInstance.option(option, value);
174+
this.sortableInstance.option(option, value);
153175
}
154176
}

addon/services/drag-store.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import Service from '@ember/service';
22

33
export default class DragStoreService extends Service {
4-
dragging = null;
4+
dragStartInstance = null;
5+
dragAddInstance = null;
6+
7+
reset() {
8+
this.dragStartInstance = null;
9+
this.dragAddInstance = null;
10+
}
11+
512
}
+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import Controller from '@ember/controller';
2+
import { tracked } from '@glimmer/tracking';
3+
import { action } from '@ember/object';
4+
5+
class Employee {
6+
@tracked id;
7+
@tracked name;
8+
@tracked type;
9+
10+
constructor(id, { name, type }) {
11+
this.id = id;
12+
this.name = name;
13+
this.type = type;
14+
}
15+
}
16+
17+
export default class CancelableController extends Controller {
18+
@tracked list = [];
19+
@tracked devSort = [1, 2, 3];
20+
@tracked pmSort = [1, 2, 3];
21+
@tracked cancelCB = null;
22+
23+
constructor() {
24+
super(...arguments);
25+
setTimeout(() => {
26+
const all = [
27+
{ name: 'Luis', type: 'dev' },
28+
{ name: 'Jaden', type: 'dev' },
29+
{ name: 'Gustavo', type: 'dev' },
30+
{ name: 'Lance', type: 'pm' },
31+
{ name: 'Britni', type: 'pm' },
32+
{ name: 'Kelly', type: 'pm' }
33+
];
34+
this.list = all.map((person, i) => new Employee(i += 1, person));
35+
}, 5000);
36+
}
37+
38+
get devList() {
39+
return this.list.filter(employee => employee.type === 'dev');
40+
}
41+
42+
get sortedDevList() {
43+
return [...this.devList].sort((a, b) => a.id - b.id);
44+
}
45+
46+
get pmList() {
47+
return this.list.filter(employee => employee.type === 'pm');
48+
}
49+
50+
get sortedPmList() {
51+
return [...this.pmList].sort((a, b) => a.id - b.id);
52+
}
53+
54+
onSort(list, evt) {
55+
console.log(`*** Action *** - onSort list ${list}`, evt);
56+
}
57+
58+
onAdd(list, evt) {
59+
console.log(`*** Action *** - onAdd list ${list}`, evt);
60+
}
61+
62+
onRemove(list, evt) {
63+
console.log(`*** Action *** - onRemove list ${list}`, evt);
64+
}
65+
66+
@action
67+
onEnd(list, evt, cancelCB) {
68+
console.log('args log', arguments[0]);
69+
console.log('args log', arguments[1]);
70+
console.log('args log', arguments[2]);
71+
this.cancelCB = cancelCB;
72+
console.log(`*** Action *** - onEnd list ${list}`, evt);
73+
}
74+
75+
@action
76+
cancelDrag() {
77+
this.cancelCB?.();
78+
this.cancelCB = null;
79+
}
80+
}

tests/dummy/app/controllers/shared.js

+8-8
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,19 @@ export default class SharedController extends Controller {
4949
return [...this.pmList].sort((a, b) => a.id - b.id);
5050
}
5151

52-
onSort(evt) {
53-
console.log('*** Action *** - onSort', evt);
52+
onSort(evt, list) {
53+
console.log(`*** Action *** - onSort list ${list}`, evt);
5454
}
5555

56-
onAdd(evt) {
57-
console.log('*** Action *** - onAdd', evt);
56+
onAdd(evt, list) {
57+
console.log(`*** Action *** - onAdd list ${list}`, evt);
5858
}
5959

60-
onRemove(evt) {
61-
console.log('*** Action *** - onRemove', evt);
60+
onRemove(evt, list) {
61+
console.log(`*** Action *** - onRemove list ${list}`, evt);
6262
}
6363

64-
onEnd(evt) {
65-
console.log('*** Action *** - onEnd', evt);
64+
onEnd(evt, list) {
65+
console.log(`*** Action *** - onEnd list ${list}`, evt);
6666
}
6767
}

tests/dummy/app/router.js

+1
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ Router.map(function() {
1414
this.route('handle');
1515
this.route('filter');
1616
this.route('thresholds');
17+
this.route('cancelable');
1718
});

tests/dummy/app/routes/cancelable.js

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import Route from '@ember/routing/route';
2+
3+
export default class CancelableRoute extends Route {
4+
}
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<div class="row">
2+
<div class="col">
3+
<h2>Shared list</h2>
4+
<hr>
5+
</div>
6+
</div>
7+
<div class="row">
8+
<div class="col">
9+
<SortableJs
10+
class="list-group"
11+
@items={{this.sortedDevList}}
12+
@options={{hash animation=150 ghostClass="ghost-class" group="shared"}}
13+
@onRemove={{fn this.onRemove 'A'}}
14+
@onEnd={{fn this.onEnd 'A'}}
15+
@onSort={{fn this.onSort 'A'}}
16+
as |list|
17+
>
18+
{{log 'running a' list}}
19+
{{#each list as |item| }}
20+
<div data-record-id={{item.value.id}} class="list-group-item">{{item.value.name}}</div>
21+
{{/each}}
22+
</SortableJs>
23+
</div>
24+
<div class="col">
25+
<SortableJs
26+
class="list-group"
27+
@items={{this.sortedPmList}}
28+
@options={{hash animation=150 ghostClass="ghost-class" group="shared"}}
29+
@onAdd={{fn this.onAdd 'B'}}
30+
@onEnd={{fn this.onEnd 'B'}}
31+
@onSort={{fn this.onSort 'B'}}
32+
as |list|
33+
>
34+
{{log 'running b' list}}
35+
{{#each list as |item| }}
36+
<div data-record-id={{item.value.id}} class="list-group-item bg-yellow">{{item.value.name}}</div>
37+
{{/each}}
38+
</SortableJs>
39+
</div>
40+
<div class="col">
41+
<script type="application/javascript" src="https://gist.github.com/lvegerano/b3118b59c5bb32fe928f957444ef6203.js"></script>
42+
</div>
43+
</div>
44+
<div class="row">
45+
{{#if this.cancelCB}}
46+
<button type="button" {{on "click" this.cancelDrag}}>Cancel</button>
47+
{{/if}}
48+
</div>

tests/dummy/app/templates/shared.hbs

+6-6
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
class="list-group"
1111
@items={{this.sortedDevList}}
1212
@options={{hash animation=150 ghostClass="ghost-class" group="shared"}}
13-
@onRemove={{this.onRemove}}
14-
@onEnd={{this.onEnd}}
15-
@onSort={{this.onSort}}
13+
@onRemove={{fn this.onRemove 'A'}}
14+
@onEnd={{fn this.onEnd 'A'}}
15+
@onSort={{fn this.onSort 'A'}}
1616
as |list|
1717
>
1818
{{log 'running a' list}}
@@ -26,9 +26,9 @@
2626
class="list-group"
2727
@items={{this.sortedPmList}}
2828
@options={{hash animation=150 ghostClass="ghost-class" group="shared"}}
29-
@onAdd={{fn this.onAdd}}
30-
@onEnd={{this.onEnd}}
31-
@onSort={{this.onSort}}
29+
@onAdd={{fn this.onAdd 'B'}}
30+
@onEnd={{fn this.onEnd 'B'}}
31+
@onSort={{fn this.onSort 'B'}}
3232
as |list|
3333
>
3434
{{log 'running b' list}}

0 commit comments

Comments
 (0)