Ext.namespace('Zarafa.hierarchy.data'); /** * @class Zarafa.hierarchy.data.HierarchyTreeLoader * @extends Ext.tree.TreeLoader * * A Special treeloader to be used by the {@link Zarafa.hierarchy.ui.HierarchyTreePanel HierarchyTree}. * This wil dynamically load the child nodes for a given node by obtaining the subfolders of * the folder related to the given node. */ Zarafa.hierarchy.data.HierarchyTreeLoader = Ext.extend(Ext.tree.TreeLoader, { /** * @cfg {Zarafa.hierarchy.ui.Tree} tree The tree on which this treeloader * is applied. */ tree : undefined, /** * @cfg {Zarafa.core.data.HierarchyStore} store store which will be used to get data for hierarchy. */ store : undefined, /** * @cfg {Object} config option for {@link Zarafa.hierarchy.ui.FolderNode foldernode} */ nodeConfig : undefined, /** * @cfg {Boolean} deferredLoading True to defer updating the Hierarchy when the panel * is currently not visible. The parent {@link Ext.Container} which contains the * {@link Ext.layout.CardLayout}, is stored in the {@link #deferredLoadingActiveParent}. */ deferredLoading : false, /** * When {@link #deferredLoading} is true, this property contains the {@link Ext.Container} * to which this loader will be listening to determine which Container is active. * This will be initialized during {@link #onTreeAfterRender}. * @property * @type Ext.Container * @private */ deferredLoadingActiveParent : undefined, /** * When {@link #deferredLoading} is true, this property indicates if a call to * {@link #doHierarchyLoad} has been made and has been scheduled. This implies * that no events from the {@link #store} need to be handled, as a full refresh * is pending. * @property * @type Boolean * @private */ isDeferred : false, /** * @constructor * @param {Object} config Configuration object */ constructor : function(config) { config = config || {}; if (!config.directFn) { // Small hack, ExtJs calls directFn in the DOMWindow scope, rather // then the scope of this class. Since we wish to extend the TreeLoader // rather then passing the directFn as a configuration option, we are now // just going to create a delegate and force the function into the correct // scope. config.directFn = this.directFn.createDelegate(this); } Zarafa.hierarchy.data.HierarchyTreeLoader.superclass.constructor.call(this, config); // If the tree is already rendered, call onTreeAfterRender directly, // otherwise add the event handler. if (this.tree.rendered) { this.onTreeAfterRender(); } else { this.tree.on('afterrender', this.onTreeAfterRender, this, { single : true }); } }, /** * Event handler which is fired when the {@link #tree} is being rendered. This will * call {@link #bindStore} to hook all events from the {@link #store}. * @private */ onTreeAfterRender : function() { this.bindStore(this.store, true); // If deferred loading is enabled, then we are going to need the parent // container which will have to be activated before we will load it. // If we can't find the parent, then don't defer loading. if (this.deferredLoading === true) { // Search for the desired container which can be activated. if (this.isParentCardLayout(this.tree)) { this.deferredLoadingActiveParent = this.tree; } else { this.deferredLoadingActiveParent = this.tree.findParentBy(this.isParentCardLayout, this); if (!this.deferredLoadingActiveParent) { this.deferredLoading = false; } } } }, /** * Returned true if the {@link Ext.Component#ownerCt owner} of the given {@link Ext.Container} * contains the {@link Ext.layout.CardLayout}. This function is given in * {@link #onTreeAfterRender} to determine the {@link #deferredLoadingActiveParent}. * @param {Ext.Container} ct The container to check * @return {Boolean} True if the parent of the given container has the CardLayout * @private */ isParentCardLayout : function(ct) { return ct.ownerCt && ct.ownerCt.layout && ct.ownerCt.layout.type === 'card'; }, /** * Bind a store to this loader. This will intialize all required event handlers. * @param {Zarafa.core.data.HierarchyStore} store The store to bind * @param {Boolean} init True when this is called during initialization. * @private */ bindStore : function(store, init) { if (init !== true && this.store === store) { return; } if (this.store) { this.store.un('load', this.onHierarchyLoad, this); this.store.un('remove', this.onHierarchyStoreRemove, this); this.store.un('addFolder', this.onHierarchyAddFolder, this); this.store.un('updateFolder', this.onHierarchyUpdateFolder, this); this.store.un('removeFolder', this.onHierarchyRemoveFolder, this); } this.store = store; if (this.store) { this.store.on({ 'load' : this.onHierarchyLoad, 'remove' : this.onHierarchyStoreRemove, 'addFolder' : this.onHierarchyAddFolder, 'updateFolder' : this.onHierarchyUpdateFolder, 'removeFolder' : this.onHierarchyRemoveFolder, 'scope' : this }); } }, /** * Event handler for the {@link Zarafa.hierarcy.data.HierarchyStore#load load} event. * This will add the loaded records into the tree as Store nodes. * @param {Ext.data.Store} store The store which fired the event * @param {Ext.data.Record[]} records The records which were loaded into the store * @param {Object} options The additional options that were passed for loading the data. * @private */ onHierarchyLoad : function(store, records, options) { // If we don't want to append a new store into the tree, // then remove all currently existing nodes and rebuild // the entire tree from scratch. if (options.add !== true) { this.tree.getRootNode().removeAll(true); } var parentCt = this.deferredLoadingActiveParent; if (this.deferredLoading !== true || parentCt === parentCt.ownerCt.layout.activeItem) { this.doHierarchyLoad(); } else { // We are going to defer to doHierarchyLoad() action, // set isDeferred to true, so we don't need to perform // update event handlers. this.isDeferred = true; this.deferredLoadingActiveParent.on('activate', this.doHierarchyLoad, this, { single : true }); } }, /** * Called by {@link #onHierarchyLoad} to start (re)loading the hierarchy. * @private */ doHierarchyLoad : function() { var rootNode = this.tree.getRootNode(); if (this.fireEvent('beforeload', this, rootNode, this.directFn) !== false) { this.directFn(rootNode.id, this.doHierarchyLoadCallback.createDelegate(this)); } // The deferred action has been completed, // we can now listen to update events again. this.isDeferred = false; }, /** * Callback function for {@link #directFn} as used by {@link #doHierarchyLoad}. * @param {Object} data The data as returned by the server * @param {Object} response The response as returned by the server * @private */ doHierarchyLoadCallback : function(data, response) { var rootNode = this.tree.getRootNode(); for (var i = 0, len = data.length; i < len; i++) { var item = data[i]; var folder = item.folder; // Check if the node already exists or not. var treeNode = rootNode.findChildByEntryId(folder.get('entryid')); if (!treeNode) { var node = this.createNode(item); rootNode.appendChild(node); } else if (treeNode.attributes.folder !== folder) { treeNode.attributes.folder = folder; treeNode.reload(); } else if (folder.isFavoritesRootFolder()) { // Check if favorite node and favorite folder doesn't have same number of children // then reload the favorite node. var favoritesStore = folder.getMAPIStore().getFavoritesStore(); if (treeNode.childNodes.length !== favoritesStore.getCount()) { treeNode.reload(); } } } // when we close the shared store suggested contact folder // of shared store was removed but node was not removed from // contact context tree panel because we don't refresh/reload the tree panel // nodes when we switch the context so here we just reload the root node. if(rootNode.childNodes.length !== data.length) { rootNode.reload(); } this.fireEvent('load', this, rootNode, response); }, /** * Handles the 'remove' even from the {@link Zarafa.hierarchy.data.HierarchyStore hierarchyStore}. * This will lookup any folder inside the tree which belongs to the given {@link Zarafa.hierarchy.data.MAPIStoreRecord}, * and removes it from the tree. * @param {Zarafa.hierarchy.data.HierarchyStore} store The store which fired the event * @param {Zarafa.core.data.IPFRecord} record The MAPIStoreRecord which was removed from the hierarchy * @param {Number} index The index from where the store was removed * @private */ onHierarchyStoreRemove : function(store, record, index) { // A call to doHierarchyLoad is pending, // no need to execute this event handler. if (this.isDeferred === true) { return; } var rootNode = this.tree.getRootNode(); var treeNode = rootNode.findChildStoreByEntryId(record.get('store_entryid')); if (treeNode) { treeNode.remove(true); if (this.deferredLoading === true) { this.deferredLoadingActiveParent.on('activate', this.doHierarchyLoad, this, { single : true }); } } }, /** * Handles the add event on folder from the global hierarchy object. This will add the folder to the tree. * * @param {Zarafa.hierarchy.data.HierarchyStore} store The store which fired the event * @param {Zarafa.hierarchy.data.MAPIStoreRecord} mapiStore mapi store in which new folders are added. * @param {Zarafa.hierarchy.data.MAPIFolderRecord/Zarafa.hierarchy.data.MAPIFolderRecord[]} folder folders which are added in hierarchy. * @private */ onHierarchyAddFolder : function(store, mapiStore, record) { // A call to doHierarchyLoad is pending, // no need to execute this event handler. if (this.isDeferred === true) { return; } if (Array.isArray(record)) { for (var i = 0, len = record.length; i < len; i++) { this.onHierarchyAddFolder(store, mapiStore, record[i]); } return; } if (record.phantom !== true) { if (this.tree.nodeFilter(record)) { var treeNode; // If record/folder is favorites mark and active context is other then Mail or Home and "show all folders" // check box is unchecked then don't add new tree node in hierarchy. if (record.isFavoritesFolder() && (record.isContainerClass('IPF.Note') || !this.tree.hasFilter())) { treeNode = this.tree.getNodeById("favorites-"+record.get('entryid')); } else { treeNode = this.tree.getNodeById(record.get('entryid')); } if (!treeNode) { var parentNode = this.getFilteredParentNode(record); var nodeType = 'folder'; if (!parentNode) { parentNode = this.tree.getRootNode(); nodeType = 'rootfolder'; } var newNode = this.createNode(Ext.apply({ nodeType : nodeType, folder: record }, this.nodeConfig)); parentNode.appendChild(newNode); parentNode.expand(); } } } }, /** * Handles the update event on folder from the global hierarchy object. Updates folder in tree. * @param {Zarafa.hierarchy.data.HierarchyStore} store The store which fired the event * @param {Zarafa.hierarchy.data.MAPIStoreRecord} mapiStore mapi store in which folders are updated. * @param {Zarafa.hierarchy.data.MAPIFolderRecord} record folders which are updated in hierarchy. * @private */ onHierarchyUpdateFolder : function(store, mapiStore, record) { // A call to doHierarchyLoad is pending, // no need to execute this event handler. if (this.isDeferred === true) { return; } var treeNode = this.tree.getTreeNode(record); if (!treeNode) { // Don't add new node in hierarchy if its parent node is // not expanded yet. As Extjs follow the lazy rendering so when we expand the // parent node, tree automatically creates respective child nodes var parentNode = this.getFilteredParentNode(record); if (Ext.isDefined(parentNode) && (!parentNode || !parentNode.isExpanded())) { return; } // treeNode not found, so apparently the folder change might // have made this folder visible in the current hierarchy. // Let the 'addFolder' event handler handle this case. this.onHierarchyAddFolder(store, mapiStore, record); } else if (!this.tree.nodeFilter(record)) { // treeNode was found, but it doesn't pass our filter... // That means the update affected that the folder is no // longer visible in the tree... // Let the 'removeFolder' event handler handle this case. this.onHierarchyRemoveFolder(store, mapiStore, record); } else { var curParentNode = treeNode.parentNode; var newParentNode = this.getFilteredParentNode(record); if (!newParentNode) { newParentNode = this.tree.getRootNode(); } if (curParentNode !== newParentNode) { // It doesn't matter if there is a parentNode or not, // when we do appendChild, the parentNode will detect // if this is a move or not. newParentNode.appendChild(treeNode); } else { treeNode.updateUI(record); // Update favorites folder tree node in hierarchy if selected folder is not favorites folder but // it exists in favorites store(record was marked as favorites). if(!record.isFavoriteFolder() && record.existsInFavorites()) { var favRecord = record.getFavoritesFolder(); var favContentUnread = favRecord.get('content_unread'); var recordContentUnread = record.get('content_unread'); if( favContentUnread !== recordContentUnread) { favRecord.set('content_unread', recordContentUnread, false); } treeNode = this.tree.getTreeNode(favRecord); // treeNode is undefined in case where webapp was reloaded recently and // favorites tree was in collapsible mode. Extjs is follow the lazy rendering // so tree node was only created when tree is in expanded mode or user expand it. if (Ext.isDefined(treeNode)) { treeNode.updateUI(record); } } } } }, /** * Handles the delete event on folder from global hierarchy object. Deletes folder from tree. * @param {Zarafa.hierarchy.data.HierarchyStore} store The store which fired the event * @param {Zarafa.hierarchy.data.MAPIStoreRecord} mapiStore mapi store in which folders are removed. * @param {Zarafa.hierarchy.data.MAPIFolderRecord} folder folders which are removed from hierarchy. * @private */ onHierarchyRemoveFolder : function(store, mapiStore, record) { // A call to doHierarchyLoad is pending, // no need to execute this event handler. if (this.isDeferred === true) { return; } var treeNode = this.tree.getTreeNode(record); if (treeNode) { treeNode.remove(true); } if (record.existsInFavorites()) { treeNode = this.tree.getTreeNode(record.getFavoritesFolder()); if (treeNode) { treeNode.remove(true); } } }, /** * This is called when a node in the HierarchyTree is being expanded, this will read * the {@link Zarafa.hierarchy.data.MAPIFolderRecord} to find the child nodes which * are positioned below the expanded node. * * @param {String} node The ID of the node which is being expanded * @param {Function} fn The function which must be called with the JSON data of * the nodes below the provided node. * @private */ directFn : function(node, fn) { var treeNode = this.tree.getNodeById(node); var data = []; // The root can not be loaded through this function, // we only handle that through the 'load' event handler // onHierarchyLoad which will generate the special Store nodes. if (treeNode.isRoot) { var stores = this.store.getRange(); for (var i = 0, len = stores.length; i < len; i++) { var store = stores[i]; var folder = store.getSubtreeFolder(); if (folder) { // The IPM_SUBTREE of a shared stores doesn't need to be shown when // a filter has been applied and the IPM_SUBTREE itself isn't a shared folder. // This could be the case when we only loaded the Inbox or Calendar of the // other store, but we didn't load the entire store itself. In that situation // the IPM_SUBTREE is really a fake entry which exists for displaying in // a non-filtered tree. var visibleRoot = !this.tree.hasFilter() || !store.isSharedStore() || folder.isSharedFolder(); if (visibleRoot && this.tree.nodeFilter(folder)) { data.push(Ext.apply({ nodeType : 'rootfolder', folder: folder }, this.nodeConfig)); } else { data = data.concat(this.getFilteredChildNodes(folder, 'rootfolder')); } } var favoritesFolder = store.getFavoritesRootFolder(); if (store.isDefaultStore() && Ext.isDefined(favoritesFolder) && this.tree.nodeFilter(favoritesFolder)) { data.push(Ext.apply({ nodeType : 'rootfolder', folder: favoritesFolder }, this.nodeConfig)); } } fn(data, {status: true}); } else { // When we are not loading the root, we want to defer the loading // for 1 ms. This helps with large hierarchies where the JS would normally // try to load the entire tree in a single thread which might take so long // that the browser will kill it. /*jshint unused:false*/ var defer = function(node, fn) { data = this.getFilteredChildNodes(treeNode.getFolder(), 'folder'); fn(data, { status: true }); }.defer(1, this, [node, fn]); } }, /** * Obtain the list of nodes which are positioned below the given {@link Zarafa.hierarchy.data.MAPIFolderRecord folder}. * If a subfolder is {@link Zarafa.hierarchy.ui.Tree#nodeFilter filtered} the subfolder will not be added itself, * but subfolder will be used for a recursive call to {@link #getFilteredChildNodes} to see if the sub-subfolders * do match the filter. * @param {Zarafa.hierarchy.data.MAPIFolderRecord} folder The folder which is checked * @param {String} nodeType The nodeType which must be applied to each node * @return {Object[]} The array of nodes which must be created as subfolders * @private */ getFilteredChildNodes : function(folder, nodeType) { var subfolders = folder.getChildren(); var nodes = []; for (var i = 0, len = subfolders.length; i < len; i++) { var subfolder = subfolders[i]; if (this.tree.nodeFilter(subfolder)) { nodes.push(Ext.apply({ nodeType : nodeType, folder: subfolder }, this.nodeConfig)); } else if (subfolder.get('has_subfolder')) { nodes = nodes.concat(this.getFilteredChildNodes(subfolder, nodeType)); } } return nodes; }, /** * Obtain the parent node for the given {@link Zarafa.hierarchy.data.MAPIFolderRecord folder}, * normally only the node with an id which matches the 'parent_entryid' of the original * folder would be sufficient, but when the tree is filtered, this might not be the case. * Hence we start calling this function recursively until we find the first node in the chain * which should act as parent or grandparent of the given folder. * @param {Zarafa.hierarchy.data.MAPIFolderRecord} folder The folder which is being checked * @param {Zarafa.hierarchy.data.MAPIFolderRecord} base The root folder for the search, if * this folder is encountered as parent, this search ends. * @return {Ext.tree.TreeNode} The parent node for the folder * @private */ getFilteredParentNode : function(folder, base) { var parentfolder; if(folder.isFavoritesFolder()) { parentfolder = folder.getFavoritesRootFolder(); } else { parentfolder = folder.getParentFolder(); } var node = false; if (parentfolder) { if (parentfolder === base) { node = base; } else if (this.tree.nodeFilter(parentfolder)) { node = this.tree.getNodeById(parentfolder.get('entryid')); } else { node = this.getFilteredParentNode(parentfolder); } } return node; }, /** * Add extra attributes for a new {@link Zarafa.hierarchy.ui.FolderNode folderNode} which is about * to be created. This will check the {@link Zarafa.hierarchy.ui.FolderNode#folder folder} to * see what properties must be set. * @param {Object} attr The attributes which will be used to create the node * @return {Zarafa.hierarchy.ui.FolderNode} The created node */ createNode : function(attr) { var folder = attr.folder; if (folder) { if (attr.nodeType === 'rootfolder') { attr.extendedDisplayName = this.tree.hasFilter(); } // To uniquely identify the favorites tree nodes we append the "favorites-" key word with node id // when the node is created. attr.id = folder.isFavoritesFolder() ? "favorites-" + folder.get('entryid') : folder.get('entryid'); if (folder.isFavoritesRootFolder()) { attr.leaf = folder.get('assoc_content_count') === 0; } else { attr.leaf = !folder.get('has_subfolder'); } attr.uiProvider = Zarafa.hierarchy.ui.FolderNodeUI; attr.expanded = this.tree.isFolderOpened(folder); attr.allowDrag = !folder.isDefaultFolder() && !folder.isSearchFolder(); } return Zarafa.hierarchy.data.HierarchyTreeLoader.superclass.createNode.apply(this, arguments); }, /** * Destroys the TreeLoader */ destroy : function() { this.bindStore(null); Zarafa.hierarchy.data.HierarchyTreeLoader.superclass.destroy.apply(this, arguments); } });