Lists.js
22.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
/**
* @class SimpleTasks.controller.Lists
* @extends Ext.app.Controller
*/
Ext.define('SimpleTasks.controller.Lists', {
extend: 'Ext.app.Controller',
models: ['List'],
stores: ['Lists', 'Tasks'],
views: [
'lists.Tree',
'lists.ContextMenu',
'Toolbar'
],
refs: [
{
ref: 'listTree',
selector: 'listTree'
},
{
ref: 'taskGrid',
selector: 'taskGrid'
},
{
ref: 'taskForm',
selector: 'taskForm'
},
{
ref: 'contextMenu',
selector: 'listsContextMenu',
xtype: 'listsContextMenu',
autoCreate: true
}
],
init: function() {
var me = this,
listsStore = me.getListsStore(),
tasksStore = me.getTasksStore();
me.control({
'[iconCls=tasks-new-list]': {
click: me.handleNewListClick
},
'[iconCls=tasks-new-folder]': {
click: me.handleNewFolderClick
},
'[iconCls=tasks-delete-list]': {
click: me.handleDeleteClick
},
'[iconCls=tasks-delete-folder]': {
click: me.handleDeleteClick
},
'listTree': {
afterrender: me.handleAfterListTreeRender,
edit: me.updateList,
canceledit: me.handleCancelEdit,
deleteclick: me.handleDeleteIconClick,
selectionchange: me.filterTaskGrid,
taskdrop: me.updateTaskList,
listdrop: me.reorderList,
itemmouseenter: me.showActions,
itemmouseleave: me.hideActions,
itemcontextmenu: me.showContextMenu
}
});
if(listsStore.isLoading()) {
listsStore.on('load', me.handleListsLoad, me);
} else {
me.handleListsLoad(listsStore);
}
listsStore.on('write', me.syncListsStores, me);
},
/**
* Handles a click on the "New List" button or context menu item.
* @param {Ext.Component} component
* @param {Ext.EventObject} e
*/
handleNewListClick: function(component, e) {
this.addList(true);
},
/**
* Handles a click on the "New Folder" button or context menu item.
* @param {Ext.Component} component
* @param {Ext.EventObject} e
*/
handleNewFolderClick: function(component, e) {
this.addList();
},
/**
* Adds an empty list to the lists store and starts editing the new list
* @param {Boolean} leaf True if the new node should be a leaf node.
*/
addList: function(leaf) {
var listTree = this.getListTree(),
cellEditingPlugin = listTree.cellEditingPlugin,
selectionModel = listTree.getSelectionModel(),
selectedList = selectionModel.getSelection()[0],
parentList = selectedList.isLeaf() ? selectedList.parentNode : selectedList,
newList = Ext.create('SimpleTasks.model.List', {
name: 'New ' + (leaf ? 'List' : 'Folder'),
leaf: leaf,
loaded: true // set loaded to true, so the tree won't try to dynamically load children for this node when expanded
}),
expandAndEdit = function() {
if(parentList.isExpanded()) {
selectionModel.select(newList);
cellEditingPlugin.startEdit(newList, 0);
} else {
listTree.on('afteritemexpand', function startEdit(list) {
if(list === parentList) {
selectionModel.select(newList);
cellEditingPlugin.startEdit(newList, 0);
// remove the afterexpand event listener
listTree.un('afteritemexpand', startEdit);
}
});
parentList.expand();
}
};
parentList.appendChild(newList);
if(listTree.getView().isVisible(true)) {
expandAndEdit();
} else {
listTree.on('expand', function onExpand() {
expandAndEdit();
listTree.un('expand', onExpand);
});
listTree.expand();
}
},
/**
* Handles the list list's "edit" event.
* Updates the list on the server whenever a list record is updated using the tree editor.
* @param {Ext.grid.plugin.CellEditing} editor
* @param {Object} e an edit event object
*/
updateList: function(editor, e) {
var me = this,
list = e.record;
list.save({
success: function(list, operation) {
// filter the task list by the currently selected list. This is necessary for newly added lists
// since this is the first point at which we have a primary key "id" from the server.
// If we don't filter here then any new tasks that are added will not appear until the filter is triggered by a selection change.
me.filterTaskGrid(me.getListTree().getSelectionModel(), [list]);
},
failure: function(list, operation) {
var error = operation.getError(),
msg = Ext.isObject(error) ? error.status + ' ' + error.statusText : error;
Ext.MessageBox.show({
title: 'Update List Failed',
msg: msg,
icon: Ext.Msg.ERROR,
buttons: Ext.Msg.OK
});
}
});
},
/**
* Handles the list tree's cancel edit event
* removes a newly added node if editing is canceled before the node has been saved to the server
* @param {Ext.grid.plugin.CellEditing} editor
* @param {Object} e an edit event object
*/
handleCancelEdit: function(editor, e) {
var list = e.record,
parent = list.parentNode;
parent.removeChild(list);
this.getListTree().getSelectionModel().select([parent]);
},
/**
* Handles a click on a delete icon in the list tree.
* @param {Ext.tree.View} view
* @param {Number} rowIndex
* @param {Number} colIndex
* @param {Ext.grid.column.Action} column
* @param {EventObject} e
*/
handleDeleteIconClick: function(view, rowIndex, colIndex, column, e) {
this.deleteList(view.getRecord(view.findTargetByEvent(e)));
},
/**
* Handles a click on the "Delete List" or "Delete Folder" button or menu item
* @param {Ext.Component} component
* @param {Ext.EventObject} e
*/
handleDeleteClick: function(component, e) {
this.deleteList(this.getListTree().getSelectionModel().getSelection()[0]);
},
/**
* Deletes a list from the server and updates the view.
* @param {SimpleTasks.model.List} list
*/
deleteList: function(list) {
var me = this,
listTree = me.getListTree(),
listName = list.get('name'),
selModel = listTree.getSelectionModel(),
tasksStore = me.getTasksStore(),
listsStore = me.getListsStore(),
isLocal = this.getListsStore().getProxy().type === 'localstorage',
filters, tasks;
Ext.Msg.show({
title: 'Delete List?',
msg: 'Are you sure you want to permanently delete the "' + listName + '" list and all its tasks?',
buttons: Ext.Msg.YESNO,
fn: function(response) {
if(response === 'yes') {
// save the existing filters
filters = tasksStore.filters.getRange(0, tasksStore.filters.getCount() - 1);
// clear the filters in the tasks store, we need to do this because tasksStore.queryBy only queries based on the current filter,
// but we need to query all lists in the store
tasksStore.clearFilter();
// recursively remove any tasks from the store that are associated with the list being deleted or any of its children.
(function deleteTasks(list) {
tasks = tasksStore.queryBy(function(task, id) {
return task.get('list_id') === list.get('id');
});
tasksStore.remove(tasks.getRange(0, tasks.getCount() - 1), !isLocal);
list.eachChild(function(child) {
deleteTasks(child);
});
})(list);
// reapply the filters
tasksStore.filter(filters);
// destroy the tree node on the server
list.parentNode.removeChild(list);
listsStore.sync({
failure: function(batch, options) {
var error = batch.exceptions[0].getError(),
msg = Ext.isObject(error) ? error.status + ' ' + error.statusText : error;
Ext.MessageBox.show({
title: 'Delete List Failed',
msg: msg,
icon: Ext.Msg.ERROR,
buttons: Ext.Msg.OK
});
}
});
if(isLocal) {
// only need to sync the tasks store when using local storage.
// when using an ajax proxy we will allow the server to handle deleting any tasks associated with the deleted list(s)
tasksStore.sync();
}
if(!listsStore.getNodeById(selModel.getSelection()[0].get('id'))) { //if the selection no longer exists in the store (it was part of the deleted node(s))
// change selection to the "All Tasks" list
selModel.select(0);
}
// refresh the list view so the task counts will be accurate
listTree.refreshView();
}
}
});
},
/**
* Handles the list tree's "selectionchange" event.
* Filters the task store based on the selected list.
* @param {Ext.selection.RowModel} selModel
* @param {SimpleTasks.model.List[]} lists
*/
filterTaskGrid: function(selModel, lists) {
var list = lists[0],
tasksStore = this.getTasksStore(),
listIds = [],
deleteListBtn = Ext.getCmp('delete-list-btn'),
deleteFolderBtn = Ext.getCmp('delete-folder-btn'),
filters = tasksStore.filters.getRange(0, tasksStore.filters.getCount() - 1),
filterCount = filters.length,
i = 0;
// first clear any existing filter
tasksStore.clearFilter();
// build an array of all the list_id's in the hierarchy of the selected list
list.cascadeBy(function(list) {
listIds.push(list.get('id'));
});
// remove any existing "list_id" filter from the filters array
for(; i < filterCount; i++) {
if(filters[i].property === 'list_id') {
filters.splice(i, 1);
filterCount --;
}
}
// add the new list_ids to the filters array
filters.push({ property: "list_id", value: new RegExp('^' + listIds.join('$|^') + '$') });
// apply the filters
tasksStore.filter(filters);
// set the center panel's title to the name of the currently selected list
this.getTaskGrid().setTitle(list.get('name'));
// enable or disable the "delete list" and "delete folder" buttons depending on what type of node is selected
if(list.get('id') === -1) {
deleteListBtn.disable();
deleteFolderBtn.disable();
} else if(list.isLeaf()) {
deleteListBtn.enable();
deleteFolderBtn.disable();
} else {
deleteListBtn.disable();
deleteFolderBtn.enable();
}
// make the currently selected list the default value for the list field on the new task form
this.getTaskForm().query('[name=list_id]')[0].setValue(list.get('id'));
},
/**
* Handles the list view's "taskdrop" event. Runs when a task is dragged and dropped on a list.
* Updates the task to belong to the list it was dropped on.
* @param {SimpleTasks.model.Task} task The Task record that was dropped
* @param {SimpleTasks.model.List} list The List record that the mouse was over when the drop happened
*/
updateTaskList: function(task, list) {
var me = this,
listId = list.get('id');
// set the tasks list_id field to the id of the list it was dropped on
task.set('list_id', listId);
// save the task to the server
task.save({
success: function(task, operation) {
// refresh the filters on the task list
me.getTaskGrid().refreshFilters();
// refresh the lists view so the task counts will be updated.
me.getListTree().refreshView();
},
failure: function(task, operation) {
var error = operation.getError(),
msg = Ext.isObject(error) ? error.status + ' ' + error.statusText : error;
Ext.MessageBox.show({
title: 'Move Task Failed',
msg: msg,
icon: Ext.Msg.ERROR,
buttons: Ext.Msg.OK
});
}
});
},
/**
* Handles the list view's "listdrop" event. Runs after a list is reordered by dragging and dropping.
* Commits the lists new position in the tree to the server.
* @param {SimpleTasks.model.List} list The List that was dropped
* @param {SimpleTasks.model.List} overList The List that the List was dropped on
* @param {String} position `"before"` or `"after"` depending on whether the mouse is above or below the midline of the node.
*/
reorderList: function(list, overList, position) {
var listsStore = this.getListsStore();
if(listsStore.getProxy().type === 'localstorage') {
listsStore.sync();
} else {
Ext.Ajax.request({
url: 'php/list/move.php',
jsonData: {
id: list.get('id'),
relatedId: overList.get('id'),
position: position
},
success: function(response, options) {
var responseData = Ext.decode(response.responseText);
if(!responseData.success) {
Ext.MessageBox.show({
title: 'Move Task Failed',
msg: responseData.message,
icon: Ext.Msg.ERROR,
buttons: Ext.Msg.OK
});
}
},
failure: function(response, options) {
Ext.MessageBox.show({
title: 'Move Task Failed',
msg: response.status + ' ' + response.statusText,
icon: Ext.Msg.ERROR,
buttons: Ext.Msg.OK
});
}
});
}
// refresh the lists view so the task counts will be updated.
this.getListTree().refreshView();
},
/**
* Handles the initial tasks store "load" event,
* refreshes the List tree view then removes itself as a handler.
* @param {SimpleTasks.store.Tasks} tasksStore
* @param {SimpleTasks.model.Task[]} tasks
* @param {Boolean} success
* @param {Ext.data.Operation} operation
*/
handleTasksLoad: function(tasksStore, tasks, success, operation) {
var me = this,
listTree = me.getListTree(),
selectionModel = listTree.getSelectionModel();
// refresh the lists view so the task counts will be updated.
listTree.refreshView();
// filter the task grid by the selected list
me.filterTaskGrid(selectionModel, selectionModel.getSelection());
// remove the event listener after the first run
tasksStore.un('load', this.handleTasksLoad, this);
},
/**
* Handles the initial lists store "load" event,
* selects the list tree's root node if the list tree exists, loads the tasks store, then removes itself as a handler.
* @param {SimpleTasks.store.Lists} listsStore
* @param {SimpleTasks.model.List[]} lists
* @param {Boolean} success
* @param {Ext.data.Operation} operation
*/
handleListsLoad: function(listsStore, lists, success, operation) {
var me = this,
listTree = me.getListTree(),
tasksStore = me.getTasksStore();
if(listTree) {
// if the list tree exists when the lists store is first loaded, select the root node.
// when using a server proxy, the list tree will always exist at this point since asyncronous loading of data allows time for the list tree to be created and rendered.
// when using a local storage proxy, the list tree will not yet exist at this point, so we'll have to select the root node on render instead (see handleAfterListTreeRender)
listTree.getSelectionModel().select(0);
}
// wait until lists are done loading to load tasks since the task grid's "list" column renderer depends on lists store being loaded
me.getTasksStore().load();
// if the tasks store is asynchronous (server proxy) attach load handler for refreshing the list counts after loading is complete
// if local storage is being used, isLoading will be false here since load() will run syncronously, so there is no need
// to refresh the lists view because load will have happened before the list tree is even rendered
if(tasksStore.isLoading()) {
tasksStore.on('load', me.handleTasksLoad, me);
}
// remove the event listener after the first run
listsStore.un('load', me.handleListsLoad, me);
},
/**
* Handles the list tree's "afterrender" event
* Selects the lists tree's root node, if the list tree exists
* @param {SimpleTasks.view.lists.Tree} listTree
*/
handleAfterListTreeRender: function(listTree) {
listTree.getSelectionModel().select(0);
},
/**
* Handles the lists store's write event.
* Syncronizes the other read only list stores with the newly saved data
* @param {SimpleTasks.store.Lists} listsStore
* @param {Ext.data.Operation} operation
*/
syncListsStores: function(listsStore, operation) {
var me = this,
stores = [
Ext.getStore('Lists-TaskGrid'),
Ext.getStore('Lists-TaskEditWindow'),
Ext.getStore('Lists-TaskForm')
],
listToSync;
Ext.each(operation.getRecords(), function(list) {
Ext.each(stores, function(store) {
if(store) {
listToSync = store.getNodeById(list.getId());
switch(operation.action) {
case 'create':
(store.getNodeById(list.parentNode.getId()) || store.getRootNode()).appendChild(list.copy());
break;
case 'update':
if(listToSync) {
listToSync.set(list.data);
listToSync.commit();
}
break;
case 'destroy':
if(listToSync) {
listToSync.remove(false);
}
}
}
});
});
},
/**
* Handles a mouseenter event on a list tree node.
* Shows the node's action icons.
* @param {Ext.tree.View} view
* @param {SimpleTasks.model.List} list
* @param {HTMLElement} node
* @param {Number} rowIndex
* @param {Ext.EventObject} e
*/
showActions: function(view, list, node, rowIndex, e) {
var icons = Ext.DomQuery.select('.x-action-col-icon', node);
if(view.getRecord(node).get('id') > 0) {
Ext.each(icons, function(icon){
Ext.get(icon).removeCls('x-hidden');
});
}
},
/**
* Handles a mouseleave event on a list tree node.
* Hides the node's action icons.
* @param {Ext.tree.View} view
* @param {SimpleTasks.model.List} list
* @param {HTMLElement} node
* @param {Number} rowIndex
* @param {Ext.EventObject} e
*/
hideActions: function(view, list, node, rowIndex, e) {
var icons = Ext.DomQuery.select('.x-action-col-icon', node);
Ext.each(icons, function(icon){
Ext.get(icon).addCls('x-hidden');
});
},
/**
* Handles the list tree's itemcontextmenu event
* Shows the list context menu.
* @param {Ext.grid.View} view
* @param {SimpleTasks.model.List} list
* @param {HTMLElement} node
* @param {Number} rowIndex
* @param {Ext.EventObject} e
*/
showContextMenu: function(view, list, node, rowIndex, e) {
var contextMenu = this.getContextMenu(),
newListItem = Ext.getCmp('new-list-item'),
newFolderItem = Ext.getCmp('new-folder-item'),
deleteFolderItem = Ext.getCmp('delete-folder-item'),
deleteListItem = Ext.getCmp('delete-list-item');
if(list.isLeaf()) {
newListItem.hide();
newFolderItem.hide();
deleteFolderItem.hide();
deleteListItem.show();
} else {
newListItem.show();
newFolderItem.show();
if(list.isRoot()) {
deleteFolderItem.hide();
} else {
deleteFolderItem.show();
}
deleteListItem.hide();
}
contextMenu.setList(list);
contextMenu.showAt(e.getX(), e.getY());
e.preventDefault();
}
});