ember-sortable 2.2.5 → 3.0.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.
- package/.github/ISSUE_TEMPLATE/bug_report.md +2 -2
- package/.github/workflows/github_ci.yml +15 -13
- package/.huskyrc +0 -1
- package/CHANGELOG.md +234 -104
- package/MIGRATION_GUIDE_MODIFIERS.md +20 -13
- package/MIGRATION_GUIDE_V2.md +20 -8
- package/README.md +83 -149
- package/V2_MIGRATION_RFC.md +188 -222
- package/addon/modifiers/sortable-group.js +59 -52
- package/addon/modifiers/sortable-handle.js +16 -18
- package/addon/modifiers/sortable-item.js +124 -84
- package/addon/services/ember-sortable.js +3 -6
- package/addon/system/scroll-parent.js +1 -1
- package/addon/utils/coordinate.js +1 -1
- package/addon/utils/css-calculation.js +2 -2
- package/addon/utils/defaults.js +4 -3
- package/addon/utils/keyboard.js +3 -12
- package/addon-test-support/helpers/drag.js +3 -11
- package/addon-test-support/helpers/index.js +1 -4
- package/addon-test-support/helpers/reorder.js +4 -6
- package/addon-test-support/utils/keyboard.js +3 -12
- package/addon-test-support/utils/offset.js +6 -6
- package/config/environment.js +2 -2
- package/index.js +0 -20
- package/package.json +32 -18
- package/addon/components/sortable-group.js +0 -726
- package/addon/components/sortable-handle.js +0 -11
- package/addon/components/sortable-item.js +0 -753
- package/addon/styles/components/sortable-group.css +0 -9
- package/addon/templates/components/sortable-group.hbs +0 -16
- package/addon/templates/components/sortable-handle.hbs +0 -1
- package/addon/templates/components/sortable-item.hbs +0 -2
- package/addon/utils/transitionend.js +0 -22
- package/app/components/sortable-group.js +0 -3
- package/app/components/sortable-handle.js +0 -4
- package/app/components/sortable-item.js +0 -3
- package/jsconfig.json +0 -1
- package/vendor/polyfills/closest.js +0 -21
|
@@ -1,726 +0,0 @@
|
|
|
1
|
-
import { A } from '@ember/array';
|
|
2
|
-
import Component from '@ember/component';
|
|
3
|
-
import { set, get, computed, defineProperty } from '@ember/object';
|
|
4
|
-
import { run } from '@ember/runloop';
|
|
5
|
-
import layout from '../templates/components/sortable-group';
|
|
6
|
-
import {
|
|
7
|
-
isEnterKey,
|
|
8
|
-
isSpaceKey,
|
|
9
|
-
isEscapeKey,
|
|
10
|
-
isDownArrowKey,
|
|
11
|
-
isUpArrowKey,
|
|
12
|
-
isLeftArrowKey,
|
|
13
|
-
isRightArrowKey,
|
|
14
|
-
} from '../utils/keyboard';
|
|
15
|
-
import { ANNOUNCEMENT_ACTION_TYPES } from '../utils/constant';
|
|
16
|
-
|
|
17
|
-
const a = A;
|
|
18
|
-
const NO_MODEL = {};
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* This component supports re-ordering items in a group via drag-drop and keyboard navigation.
|
|
22
|
-
* The component is built with accessibility in mind.
|
|
23
|
-
*
|
|
24
|
-
* @param {Ember.Object} groupModel The group of models to be rearranged.
|
|
25
|
-
* @param {Ember.Array} model The array of models.
|
|
26
|
-
* @param {String} a11yItemName A name for each model, used for creating more meaningful a11y announcements.
|
|
27
|
-
* @param {Object} a11yAnnouncementConfig A map of action to function to build meaningful a11y announcements.
|
|
28
|
-
* @param {String} itemVisualClass A class for styling visual indicators on the yielded `sortable-item`.
|
|
29
|
-
* @param {Object} handleVisualClass An object for styling visual indicators on the yielded `sortable-handle` on different `move`.
|
|
30
|
-
* @param {Function} [onChange] An optional callback for when position rearrangements are confirmed.
|
|
31
|
-
*
|
|
32
|
-
* @module drag-drop/draggable-group
|
|
33
|
-
* @example
|
|
34
|
-
* {{#sortable-group data-test-vertical-demo-group tagName="ol" a11yAnnouncementConfig=a11yAnnouncementConfig a11yItemName="spanish number" itemVisualClass=itemVisualClass handleVisualClass=handleVisualClass onChange=(action "update") model=model.items as |group|}}
|
|
35
|
-
* {{#each group.model as |item|}}
|
|
36
|
-
* {{#group.item data-test-vertical-demo-item tagName="li" model=item as |groupItem|}}
|
|
37
|
-
* {{item}}
|
|
38
|
-
* {{#groupItem.handle data-test-vertical-demo-handle class="handle"}}
|
|
39
|
-
* <span data-item={{item}}>
|
|
40
|
-
* <span>⇕</span>
|
|
41
|
-
* </span>
|
|
42
|
-
* {{/groupItem.handle}}
|
|
43
|
-
* {{/group.item}}
|
|
44
|
-
* {{/each}}
|
|
45
|
-
* {{/sortable-group}}
|
|
46
|
-
**/
|
|
47
|
-
export default Component.extend({
|
|
48
|
-
layout,
|
|
49
|
-
tagName: 'ol',
|
|
50
|
-
|
|
51
|
-
attributeBindings: ['data-test-selector', 'tabindex', 'role'],
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
Called when order of items has been changed
|
|
55
|
-
@property onChange
|
|
56
|
-
@type Action
|
|
57
|
-
@param object group model (omitted if not set)
|
|
58
|
-
@param array item models in their new order
|
|
59
|
-
@param object item model just dragged
|
|
60
|
-
@default null
|
|
61
|
-
*/
|
|
62
|
-
onChange: function() {},
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
@property direction
|
|
66
|
-
@type string
|
|
67
|
-
@default y
|
|
68
|
-
*/
|
|
69
|
-
direction: 'y',
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
@property model
|
|
73
|
-
@type Any
|
|
74
|
-
@default null
|
|
75
|
-
*/
|
|
76
|
-
model: NO_MODEL,
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* @property a group associated with the model
|
|
80
|
-
* @type Any
|
|
81
|
-
* @default null
|
|
82
|
-
*/
|
|
83
|
-
groupModel: NO_MODEL,
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* @property an object containing different classes for visual indicators
|
|
87
|
-
* @type
|
|
88
|
-
* @default null
|
|
89
|
-
* @example
|
|
90
|
-
* {
|
|
91
|
-
* UP: 'up'
|
|
92
|
-
* DOWN: 'down',
|
|
93
|
-
* LEFT: 'left',
|
|
94
|
-
* RIGHT: 'right',
|
|
95
|
-
* }
|
|
96
|
-
*/
|
|
97
|
-
handleVisualClass: NO_MODEL,
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* @property an object containing functions for producing screen reader announcements
|
|
101
|
-
* @type
|
|
102
|
-
* @default null
|
|
103
|
-
* @example
|
|
104
|
-
* {
|
|
105
|
-
* MOVE: function() {},
|
|
106
|
-
* ACTIVATE: function() {},
|
|
107
|
-
* CONFIRM: function() {},
|
|
108
|
-
* CANCEL: function() {},
|
|
109
|
-
* }
|
|
110
|
-
*/
|
|
111
|
-
a11yAnnouncementConfig: NO_MODEL,
|
|
112
|
-
|
|
113
|
-
/** Primary keyboard utils */
|
|
114
|
-
// Tracks the currently selected item
|
|
115
|
-
_selectedItem: null,
|
|
116
|
-
// Tracks the current move
|
|
117
|
-
move: null,
|
|
118
|
-
// Tracks the status of keyboard reorder mode
|
|
119
|
-
isKeyboardReorderModeEnabled: false,
|
|
120
|
-
// Tracks if we're still performing a programmatical focus.
|
|
121
|
-
isRetainingFocus: false,
|
|
122
|
-
/** End of keyboard utils */
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
@property items
|
|
126
|
-
@type Ember.NativeArray
|
|
127
|
-
*/
|
|
128
|
-
items: computed(() => a()),
|
|
129
|
-
|
|
130
|
-
init() {
|
|
131
|
-
this._super(...arguments);
|
|
132
|
-
|
|
133
|
-
this._setGetterSetters();
|
|
134
|
-
this.set('moves', []);
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
didInsertElement() {
|
|
138
|
-
this._super(...arguments);
|
|
139
|
-
|
|
140
|
-
// Adding announcer inside an ordered list violates a11y guidelines, so we insert it after our list.
|
|
141
|
-
const announcer = this._createAnnouncer();
|
|
142
|
-
this.set('announcer', announcer);
|
|
143
|
-
this.element.insertAdjacentElement('afterend', announcer);
|
|
144
|
-
},
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Explanation
|
|
148
|
-
* 1. `KeyboardReorderMode` is disabled: users can activate it via ENTER or SPACE.
|
|
149
|
-
* 2. `KeyboardReorderMode` is enabled: users can reorder via UP or DOWN arrow keys. TODO: Expand to more keys, e.g LEFT, RIGHT
|
|
150
|
-
* 3. `KeyboardReorderMode` is enabled: users can finalize/save the reordering via ENTER or SPACE.
|
|
151
|
-
* 4. `KeyboardReorderMode` is enabled: users can discard the reordering via ESC.
|
|
152
|
-
*
|
|
153
|
-
* @param {Event} evt a DOM event
|
|
154
|
-
*/
|
|
155
|
-
keyDown(event) {
|
|
156
|
-
if (!this.get('isKeyDownEnabled')) {
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
// Note: If handle is specified, we need to target the keyDown on the handle
|
|
160
|
-
const isKeyboardReorderModeEnabled = this.get('isKeyboardReorderModeEnabled');
|
|
161
|
-
const _selectedItem = this.get('_selectedItem');
|
|
162
|
-
|
|
163
|
-
if (!isKeyboardReorderModeEnabled && (isEnterKey(event) || isSpaceKey(event))) {
|
|
164
|
-
this.prepareKeyboardReorderMode();
|
|
165
|
-
this._announceAction(ANNOUNCEMENT_ACTION_TYPES.ACTIVATE);
|
|
166
|
-
this._updateItemVisualIndicators(_selectedItem, true);
|
|
167
|
-
this._updateHandleVisualIndicators(_selectedItem, true);
|
|
168
|
-
|
|
169
|
-
this.set('isRetainingFocus', true);
|
|
170
|
-
|
|
171
|
-
run.scheduleOnce('render', () => {
|
|
172
|
-
this.element.focus();
|
|
173
|
-
this.set('isRetainingFocus', false);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
// Prevent the default scroll
|
|
177
|
-
event.preventDefault();
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (isKeyboardReorderModeEnabled) {
|
|
182
|
-
this._handleKeyboardReorder(event);
|
|
183
|
-
event.preventDefault();
|
|
184
|
-
}
|
|
185
|
-
},
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Make sure that we cancel any ongoing keyboard operation when the focus is lost from the handle.
|
|
189
|
-
* Because this can be fired pre-maturely, effectively cancelling before other keyboard operations,
|
|
190
|
-
* we need to wait until other operations are completed, so this will cancel properly.
|
|
191
|
-
*
|
|
192
|
-
* @param {Event} event a DOM event.
|
|
193
|
-
*/
|
|
194
|
-
focusOut() {
|
|
195
|
-
if (!this.get('isRetainingFocus') && !this._isElementWithinHandle(document.activeElement)) {
|
|
196
|
-
this.cancelKeyboardSelection();
|
|
197
|
-
}
|
|
198
|
-
},
|
|
199
|
-
|
|
200
|
-
prepareKeyboardReorderMode() {
|
|
201
|
-
this._enableKeyboardReorderMode();
|
|
202
|
-
this._setupA11yApplicationContainer();
|
|
203
|
-
},
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Moves the item to its new position and adds the move to our history.
|
|
207
|
-
*
|
|
208
|
-
* @param {Object} item the item to be moved.
|
|
209
|
-
* @param {Integer} delta how much to move index-wise.
|
|
210
|
-
*/
|
|
211
|
-
moveItem(item, delta) {
|
|
212
|
-
const { sortedItems, moves } = this.getProperties('sortedItems', 'moves');
|
|
213
|
-
const sortedIndex = sortedItems.indexOf(item);
|
|
214
|
-
const newSortedIndex = sortedIndex + delta;
|
|
215
|
-
|
|
216
|
-
// If out of bounds, we don't do anything.
|
|
217
|
-
if (newSortedIndex < 0 || newSortedIndex >= sortedItems.length) {
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
this._announceAction(ANNOUNCEMENT_ACTION_TYPES.MOVE, delta);
|
|
221
|
-
// Guarantees that the before the UI is fully rendered before we move again.
|
|
222
|
-
run.scheduleOnce('render', () => {
|
|
223
|
-
this._move(sortedIndex, newSortedIndex);
|
|
224
|
-
this._updateHandleVisualIndicators(item, true);
|
|
225
|
-
|
|
226
|
-
moves.push([sortedIndex, newSortedIndex]);
|
|
227
|
-
});
|
|
228
|
-
},
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Handles all the necessary operations needed for cancelling the ccurrent keyboard selection.
|
|
232
|
-
* 1. Disables keyboard reorder mode.
|
|
233
|
-
* 2. Undo all of the tracked moves.
|
|
234
|
-
* 3. Tears down the application container, so we are not focus locked within the application.
|
|
235
|
-
* 4. Resets the current selected item.
|
|
236
|
-
*/
|
|
237
|
-
cancelKeyboardSelection() {
|
|
238
|
-
const _selectedItem = this.get('_selectedItem');
|
|
239
|
-
this._disableKeyboardReorderMode();
|
|
240
|
-
// Revert the process by reversing the move.
|
|
241
|
-
const moves = this.get('moves');
|
|
242
|
-
while (moves.length > 0) {
|
|
243
|
-
const move = moves.pop();
|
|
244
|
-
this._move(move[1], move[0])
|
|
245
|
-
}
|
|
246
|
-
this._tearDownA11yApplicationContainer();
|
|
247
|
-
this._updateItemVisualIndicators(_selectedItem, false);
|
|
248
|
-
this._updateHandleVisualIndicators(_selectedItem, false);
|
|
249
|
-
this._resetItemSelection();
|
|
250
|
-
},
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Handles all th necessary operations needed for confirming the current keyboard selection.
|
|
254
|
-
* 1. Disables keyboard reorder mode.
|
|
255
|
-
* 2. Tears down the application container, so we are not focus locked within the container.
|
|
256
|
-
* 3. Make sure to update and sync all the internal items and UI.
|
|
257
|
-
* 4. Triggers the `onChange` action if provided.
|
|
258
|
-
* 5. Resets the currently selected item.
|
|
259
|
-
*/
|
|
260
|
-
confirmKeyboardSelection() {
|
|
261
|
-
const items = this.get('sortedItems');
|
|
262
|
-
const groupModel = this.get('groupModel');
|
|
263
|
-
const _selectedItem = this.get('_selectedItem');
|
|
264
|
-
const selectedModel = _selectedItem.get('model');
|
|
265
|
-
const itemModels = items.mapBy('model');
|
|
266
|
-
this.set('moves', []);
|
|
267
|
-
this._disableKeyboardReorderMode();
|
|
268
|
-
this._tearDownA11yApplicationContainer();
|
|
269
|
-
this._updateItems();
|
|
270
|
-
if (groupModel !== NO_MODEL) {
|
|
271
|
-
this.onChange(groupModel, itemModels, selectedModel)
|
|
272
|
-
} else {
|
|
273
|
-
this.onChange(itemModels, selectedModel);
|
|
274
|
-
}
|
|
275
|
-
this._updateItemVisualIndicators(_selectedItem, false);
|
|
276
|
-
this._updateHandleVisualIndicators(_selectedItem, false);
|
|
277
|
-
this._resetItemSelection();
|
|
278
|
-
},
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Enables keyboard navigation
|
|
282
|
-
*/
|
|
283
|
-
_activateKeyDown(){
|
|
284
|
-
this.set('isKeyDownEnabled', true);
|
|
285
|
-
},
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Disables keyboard navigation
|
|
289
|
-
* Currently used to handle keydown events bubbling up from
|
|
290
|
-
* elements that aren't meant to invoke keyboard navigation
|
|
291
|
-
* by ignoring them.
|
|
292
|
-
*/
|
|
293
|
-
_deactivateKeyDown() {
|
|
294
|
-
this.set('isKeyDownEnabled', false);
|
|
295
|
-
},
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
Register an item with this group.
|
|
299
|
-
@method registerItem
|
|
300
|
-
@param {SortableItem} [item]
|
|
301
|
-
*/
|
|
302
|
-
_registerItem(item) {
|
|
303
|
-
this.get('items').addObject(item);
|
|
304
|
-
},
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
De-register an item with this group.
|
|
308
|
-
@method deregisterItem
|
|
309
|
-
@param {SortableItem} [item]
|
|
310
|
-
*/
|
|
311
|
-
_deregisterItem(item) {
|
|
312
|
-
this.get('items').removeObject(item);
|
|
313
|
-
},
|
|
314
|
-
|
|
315
|
-
_setSelectedItem(item) {
|
|
316
|
-
this.set('_selectedItem', item);
|
|
317
|
-
},
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
Prepare for sorting.
|
|
321
|
-
Main purpose is to stash the current itemPosition so
|
|
322
|
-
we don’t incur expensive re-layouts.
|
|
323
|
-
@method _=repare
|
|
324
|
-
*/
|
|
325
|
-
_prepare() {
|
|
326
|
-
this._itemPosition = this.get('itemPosition');
|
|
327
|
-
},
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
Update item positions (relatively to the first element position).
|
|
331
|
-
@method update
|
|
332
|
-
*/
|
|
333
|
-
_update() {
|
|
334
|
-
let sortedItems = this.get('sortedItems');
|
|
335
|
-
// Position of the first element
|
|
336
|
-
let position = this._itemPosition;
|
|
337
|
-
|
|
338
|
-
// Just in case we haven’t called prepare first.
|
|
339
|
-
if (position === undefined) {
|
|
340
|
-
position = this.get('itemPosition');
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
sortedItems.forEach(item => {
|
|
344
|
-
let dimension;
|
|
345
|
-
let direction = this.get('direction');
|
|
346
|
-
|
|
347
|
-
if (!get(item, 'isDragging')) {
|
|
348
|
-
set(item, direction, position);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// add additional spacing around active element
|
|
352
|
-
if (get(item, 'isBusy')) {
|
|
353
|
-
position += get(item, 'spacing') * 2;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
if (direction === 'x') {
|
|
357
|
-
dimension = 'width';
|
|
358
|
-
}
|
|
359
|
-
if (direction === 'y') {
|
|
360
|
-
dimension = 'height';
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
position += get(item, dimension);
|
|
364
|
-
});
|
|
365
|
-
},
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
@method _commit
|
|
369
|
-
*/
|
|
370
|
-
_commit() {
|
|
371
|
-
const items = this.get('sortedItems');
|
|
372
|
-
const groupModel = this.get('groupModel');
|
|
373
|
-
const itemModels = items.mapBy('model');
|
|
374
|
-
const draggedItem = items.findBy('wasDropped', true);
|
|
375
|
-
let draggedModel;
|
|
376
|
-
|
|
377
|
-
if (draggedItem) {
|
|
378
|
-
set(draggedItem, 'wasDropped', false); // Reset
|
|
379
|
-
draggedModel = get(draggedItem, 'model');
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
this._updateItems();
|
|
383
|
-
if (groupModel !== NO_MODEL) {
|
|
384
|
-
this.onChange(groupModel, itemModels, draggedModel);
|
|
385
|
-
} else {
|
|
386
|
-
this.onChange(itemModels, draggedModel);
|
|
387
|
-
}
|
|
388
|
-
},
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Keeps the UI in sync with actual changes.
|
|
392
|
-
* Needed for drag and keyboard operations.
|
|
393
|
-
*/
|
|
394
|
-
_updateItems() {
|
|
395
|
-
const items = this.get('sortedItems');
|
|
396
|
-
|
|
397
|
-
delete this._itemPosition;
|
|
398
|
-
|
|
399
|
-
run.schedule('render', () => {
|
|
400
|
-
items.invoke('freeze');
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
run.schedule('afterRender', () => {
|
|
404
|
-
items.invoke('reset');
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
run.next(() => {
|
|
408
|
-
run.schedule('render', () => {
|
|
409
|
-
items.invoke('thaw');
|
|
410
|
-
});
|
|
411
|
-
});
|
|
412
|
-
},
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Moves an sortedItem from one index to another index, effectively performing an reorder.
|
|
416
|
-
*
|
|
417
|
-
* @param {Integer} fromIndex the original index
|
|
418
|
-
* @param {Integer} toIndex the new index
|
|
419
|
-
*/
|
|
420
|
-
_move(fromIndex, toIndex) {
|
|
421
|
-
const { direction, sortedItems } = this.getProperties('direction', 'sortedItems');
|
|
422
|
-
const item = sortedItems[fromIndex];
|
|
423
|
-
const nextItem = sortedItems[toIndex];
|
|
424
|
-
|
|
425
|
-
// switch direction values to notify sortedItems to update, so it sorts by direction.
|
|
426
|
-
let value;
|
|
427
|
-
const dimension = direction === "y" ? "height" : "width";
|
|
428
|
-
// DOWN or RIGHT
|
|
429
|
-
if (toIndex > fromIndex) {
|
|
430
|
-
value = item.get(direction);
|
|
431
|
-
item.set(direction, nextItem.get(direction) + (nextItem.get(dimension) - item.get(dimension)));
|
|
432
|
-
nextItem.set(direction, value);
|
|
433
|
-
// UP or LEFT
|
|
434
|
-
} else {
|
|
435
|
-
value = nextItem.get(direction);
|
|
436
|
-
nextItem.set(direction, item.get(direction) + (item.get(dimension) - nextItem.get(dimension)));
|
|
437
|
-
item.set(direction, value);
|
|
438
|
-
}
|
|
439
|
-
},
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Handles all of the keyboard operations, such as
|
|
443
|
-
* 1. Keyboard navigation for UP, DOWN, LEFT, RIGHT
|
|
444
|
-
* 2. Confirming reorder
|
|
445
|
-
* 3. Discard reorder
|
|
446
|
-
* 4. Also handles refocusing the element that triggered the interaction.
|
|
447
|
-
*
|
|
448
|
-
* @param {Event} event a DOM event.
|
|
449
|
-
*/
|
|
450
|
-
_handleKeyboardReorder(event) {
|
|
451
|
-
let { direction, _selectedItem } = this.getProperties('direction', '_selectedItem');
|
|
452
|
-
|
|
453
|
-
if (direction === "y" && isDownArrowKey(event)) {
|
|
454
|
-
this.moveItem(_selectedItem, 1);
|
|
455
|
-
} else if (direction === "y" && isUpArrowKey(event)) {
|
|
456
|
-
this.moveItem(_selectedItem, -1);
|
|
457
|
-
} else if (direction === "x" && isLeftArrowKey(event)) {
|
|
458
|
-
this.moveItem(_selectedItem, -1);
|
|
459
|
-
} else if (direction === "x" && isRightArrowKey(event)) {
|
|
460
|
-
this.moveItem(_selectedItem, 1);
|
|
461
|
-
} else if (isEnterKey(event) || isSpaceKey(event)) {
|
|
462
|
-
// confirm will reset the _selectedItem, so caching it here before we remove it.
|
|
463
|
-
const itemElement = this.get('_selectedItem.element');
|
|
464
|
-
this._announceAction(ANNOUNCEMENT_ACTION_TYPES.CONFIRM);
|
|
465
|
-
this.confirmKeyboardSelection();
|
|
466
|
-
|
|
467
|
-
this.set('isRetainingFocus', true);
|
|
468
|
-
run.scheduleOnce('render', () => this._focusItem(itemElement));
|
|
469
|
-
} else if (isEscapeKey(event)) {
|
|
470
|
-
// cancel will reset the _selectedItem, so caching it here before we remove it.
|
|
471
|
-
const _selectedItemElement = this.get('_selectedItem.element');
|
|
472
|
-
this._announceAction(ANNOUNCEMENT_ACTION_TYPES.CANCEL);
|
|
473
|
-
this.cancelKeyboardSelection();
|
|
474
|
-
|
|
475
|
-
this.set('isRetainingFocus', true);
|
|
476
|
-
run.scheduleOnce('render', () => {
|
|
477
|
-
const moves = this.get('moves');
|
|
478
|
-
if (moves && moves.length > 0) {
|
|
479
|
-
const sortedItems = this.get('sortedItems');
|
|
480
|
-
const itemElement = sortedItems[moves[0].fromIndex].element
|
|
481
|
-
this._focusItem(itemElement);
|
|
482
|
-
} else {
|
|
483
|
-
this._focusItem(_selectedItemElement);
|
|
484
|
-
}
|
|
485
|
-
this.set('isRetainingFocus', false);
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
},
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Sets focus on the current item or its handle.
|
|
492
|
-
*
|
|
493
|
-
* @param {Elment} itemElement an DOM element representing an sortable-item.
|
|
494
|
-
*/
|
|
495
|
-
_focusItem(itemElement) {
|
|
496
|
-
const handle = itemElement.querySelector('[data-sortable-handle]');
|
|
497
|
-
if (handle) {
|
|
498
|
-
handle.focus();
|
|
499
|
-
} else {
|
|
500
|
-
// The consumer did not use a handle, so we set focus back to the item.
|
|
501
|
-
itemElement.focus();
|
|
502
|
-
}
|
|
503
|
-
},
|
|
504
|
-
|
|
505
|
-
/**
|
|
506
|
-
* Enables keyboard reorder mode.
|
|
507
|
-
*/
|
|
508
|
-
_enableKeyboardReorderMode() {
|
|
509
|
-
this.set('isKeyboardReorderModeEnabled', true);
|
|
510
|
-
},
|
|
511
|
-
|
|
512
|
-
/**
|
|
513
|
-
* Disables keyboard reorder mode
|
|
514
|
-
*/
|
|
515
|
-
_disableKeyboardReorderMode() {
|
|
516
|
-
this.set('isKeyboardReorderModeEnabled', false);
|
|
517
|
-
},
|
|
518
|
-
|
|
519
|
-
/**
|
|
520
|
-
* Sets up the group as an application and make it programmatically focusable.
|
|
521
|
-
*/
|
|
522
|
-
_setupA11yApplicationContainer() {
|
|
523
|
-
this.setProperties({
|
|
524
|
-
role: 'application',
|
|
525
|
-
tabindex: -1,
|
|
526
|
-
});
|
|
527
|
-
},
|
|
528
|
-
|
|
529
|
-
/**
|
|
530
|
-
* Creates a `visually-hidden` `aria-live` announcement region.
|
|
531
|
-
*/
|
|
532
|
-
_createAnnouncer() {
|
|
533
|
-
const announcer = document.createElement('span');
|
|
534
|
-
announcer.setAttribute('aria-live', 'polite');
|
|
535
|
-
announcer.classList.add('visually-hidden');
|
|
536
|
-
return announcer;
|
|
537
|
-
},
|
|
538
|
-
|
|
539
|
-
/**
|
|
540
|
-
* Announces the message constructed from `a11yAnnouncementConfig`.
|
|
541
|
-
*
|
|
542
|
-
* @param {String} type the action type.
|
|
543
|
-
* @param {Number} delta how much distance (item-wise) is being moved.
|
|
544
|
-
*/
|
|
545
|
-
_announceAction(type, delta = null) {
|
|
546
|
-
const a11yAnnouncementConfig = this.get('a11yAnnouncementConfig');
|
|
547
|
-
const a11yItemName = this.get('a11yItemName');
|
|
548
|
-
|
|
549
|
-
if (a11yAnnouncementConfig === NO_MODEL || !a11yItemName || !(type in a11yAnnouncementConfig)) {
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
const sortedItems = this.get('sortedItems');
|
|
554
|
-
const _selectedItem = this.get('_selectedItem');
|
|
555
|
-
const index = sortedItems.indexOf(_selectedItem);
|
|
556
|
-
const announcer = this.get('announcer');
|
|
557
|
-
|
|
558
|
-
const config = {
|
|
559
|
-
a11yItemName,
|
|
560
|
-
index: index,
|
|
561
|
-
maxLength : sortedItems.length,
|
|
562
|
-
direction: this.get('direction'),
|
|
563
|
-
delta,
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const message = a11yAnnouncementConfig[type](config);
|
|
567
|
-
announcer.textContent = message;
|
|
568
|
-
|
|
569
|
-
// Reset the message after the message is announced.
|
|
570
|
-
run.later(() => {
|
|
571
|
-
announcer.textContent = '';
|
|
572
|
-
}, 1000);
|
|
573
|
-
},
|
|
574
|
-
|
|
575
|
-
/**
|
|
576
|
-
* Tears down the `role=application` container.
|
|
577
|
-
*/
|
|
578
|
-
_tearDownA11yApplicationContainer() {
|
|
579
|
-
this.setProperties({
|
|
580
|
-
role: undefined,
|
|
581
|
-
tabindex: undefined,
|
|
582
|
-
});
|
|
583
|
-
},
|
|
584
|
-
|
|
585
|
-
/**
|
|
586
|
-
* Reset the selected item.
|
|
587
|
-
*/
|
|
588
|
-
_resetItemSelection() {
|
|
589
|
-
this.set('_selectedItem', null);
|
|
590
|
-
},
|
|
591
|
-
|
|
592
|
-
/**
|
|
593
|
-
* Checks if the given element is a descedant of a handle.
|
|
594
|
-
*
|
|
595
|
-
* @param {Element} element a DOM element.
|
|
596
|
-
*/
|
|
597
|
-
_isElementWithinHandle(element) {
|
|
598
|
-
return element.closest(
|
|
599
|
-
`#${this.element.id} [data-sortable-handle]`
|
|
600
|
-
);
|
|
601
|
-
},
|
|
602
|
-
|
|
603
|
-
/**
|
|
604
|
-
* Updates the selected item's visual indicators.
|
|
605
|
-
*
|
|
606
|
-
* @param {Object} item the selected item.
|
|
607
|
-
* @param {Boolean} isActive to activate or deactivate the class.
|
|
608
|
-
*/
|
|
609
|
-
_updateItemVisualIndicators(item, isActive) {
|
|
610
|
-
const itemVisualClass = this.get('itemVisualClass');
|
|
611
|
-
|
|
612
|
-
if (!itemVisualClass || !item) {
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
if (isActive) {
|
|
617
|
-
item.element.classList.add(itemVisualClass);
|
|
618
|
-
} else {
|
|
619
|
-
item.element.classList.remove(itemVisualClass);
|
|
620
|
-
}
|
|
621
|
-
},
|
|
622
|
-
|
|
623
|
-
/**
|
|
624
|
-
* Updates the selected item's handle's visual indicators
|
|
625
|
-
*
|
|
626
|
-
* @param {Object} item the selected item.
|
|
627
|
-
* @param {Boolean} isUpdate to update or not update.
|
|
628
|
-
*/
|
|
629
|
-
_updateHandleVisualIndicators(item, isUpdate) {
|
|
630
|
-
const handleVisualClass = this.get('handleVisualClass');
|
|
631
|
-
|
|
632
|
-
if (handleVisualClass === NO_MODEL || !item) {
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
const sortedItems = this.get('sortedItems');
|
|
637
|
-
const direction = this.get('direction');
|
|
638
|
-
const index = sortedItems.indexOf(item);
|
|
639
|
-
const handle = item.element.querySelector('[data-sortable-handle');
|
|
640
|
-
const visualHandle = handle ? handle : item.element;
|
|
641
|
-
const visualKeys = direction === 'y' ? ['UP', 'DOWN'] : ['LEFT', 'RIGHT'];
|
|
642
|
-
|
|
643
|
-
visualKeys.forEach(visualKey => {
|
|
644
|
-
visualHandle.classList.remove(handleVisualClass[visualKey]);
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
if (!isUpdate) {
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
if (index > 0) {
|
|
652
|
-
visualHandle.classList.add(handleVisualClass[visualKeys[0]]);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
if (index < sortedItems.length - 1) {
|
|
656
|
-
visualHandle.classList.add(handleVisualClass[visualKeys[1]]);
|
|
657
|
-
}
|
|
658
|
-
},
|
|
659
|
-
|
|
660
|
-
/**
|
|
661
|
-
* Defining getters and setters to support native getter and setters until we decide to drop support for ember versions below 3.10
|
|
662
|
-
*/
|
|
663
|
-
_setGetterSetters() {
|
|
664
|
-
/**
|
|
665
|
-
Position for the first item.
|
|
666
|
-
If spacing is present, first item's position will have to change as well.
|
|
667
|
-
@property itemPosition
|
|
668
|
-
@type Number
|
|
669
|
-
*/
|
|
670
|
-
defineProperty(this, 'itemPosition', {
|
|
671
|
-
get() {
|
|
672
|
-
const direction = this.get('direction');
|
|
673
|
-
|
|
674
|
-
return this.get(`sortedItems.firstObject.${direction}`) - this.get('sortedItems.firstObject.spacing');
|
|
675
|
-
}
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
/**
|
|
679
|
-
An array of DOM elements.
|
|
680
|
-
@property sortedItems
|
|
681
|
-
@type Array
|
|
682
|
-
*/
|
|
683
|
-
defineProperty(this, 'sortedItems', {
|
|
684
|
-
get() {
|
|
685
|
-
const items = a(this.get('items'));
|
|
686
|
-
const direction = this.get('direction');
|
|
687
|
-
|
|
688
|
-
return a(items.sortBy(direction));
|
|
689
|
-
}
|
|
690
|
-
})
|
|
691
|
-
},
|
|
692
|
-
|
|
693
|
-
actions: {
|
|
694
|
-
activateKeyDown(){
|
|
695
|
-
this._activateKeyDown();
|
|
696
|
-
},
|
|
697
|
-
|
|
698
|
-
deactivateKeyDown() {
|
|
699
|
-
this._deactivateKeyDown();
|
|
700
|
-
},
|
|
701
|
-
|
|
702
|
-
registerItem(item) {
|
|
703
|
-
this._registerItem(item);
|
|
704
|
-
},
|
|
705
|
-
|
|
706
|
-
deregisterItem(item) {
|
|
707
|
-
this._deregisterItem(item);
|
|
708
|
-
},
|
|
709
|
-
|
|
710
|
-
setSelectedItem(item) {
|
|
711
|
-
this._setSelectedItem(item);
|
|
712
|
-
},
|
|
713
|
-
|
|
714
|
-
prepare() {
|
|
715
|
-
this._prepare();
|
|
716
|
-
},
|
|
717
|
-
|
|
718
|
-
update() {
|
|
719
|
-
this._update();
|
|
720
|
-
},
|
|
721
|
-
|
|
722
|
-
commit() {
|
|
723
|
-
this._commit();
|
|
724
|
-
},
|
|
725
|
-
}
|
|
726
|
-
});
|