Lists.js 22 KB

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