/** * @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(); } });