/*
* Copyright 2019 Anyware Services
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This tree is an abstract tree panel for displaying a system files tree.
* Creates your own tree class by inheriting this one and define at the following methods:
* #createTreeStore,
* #editNode (only if cfg-allowNodeEditing is not set to false)
* #getMessageTargetConfiguration
* #getMessageTargetIdForResource,
* #getMessageTargetIdForCollection,
* #testTarget.
* @private
*/
Ext.define('Ametys.file.AbstractFileExplorerTree', {
extend: "Ext.tree.Panel",
/**
* @cfg {Boolean/String} [allowNodeEditing=true] Set to false to disable node editing. Then it is not necessary to implement #editNode.
*/
/**
* @cfg {Boolean/String} [allowDragAndDrop=true] Set to false to disable drag&drop.
*/
/**
* @cfg {Boolean/String} [enableOnFileSelected=false] Set to true to enable onFileSelected listener
*/
/**
* @cfg {Boolean/String} [allowFiltering=false] Set to true to display the filter toolbar
*/
allowFiltering: false,
statics: {
/**
* @property {String} TYPE_RESOURCE Type for resource file.
*/
TYPE_RESOURCE: 'resource',
/**
* @property {String} TYPE_COLLECTION Type for resource collection
*/
TYPE_COLLECTION: 'collection',
/**
* Get the extension of a file
* @param {String} name The file name
* @return {String} The extension
*/
getFileExtension: function(name)
{
var extension = "unknown";
var index = name.lastIndexOf('.');
if (index > 0)
{
extension = name.substring(index + 1).toLowerCase();
}
return extension;
},
/**
* Get the icon in small format (16x16 pixels) of a file
* @param {String} name The file name
* @return {String} The small icon
*/
getFileIconGlyph: function(name)
{
var extension = Ametys.file.AbstractFileExplorerTree.getFileExtension(name);
switch (extension)
{
case "css":
return "ametysicon-file-extension-css";
case "csv":
return "ametysicon-file-extension-csv";
case "doc":
return "ametysicon-file-extension-doc";
case "docx":
return "ametysicon-file-extension-docx";
case "html":
case "htm":
case "xhtml":
return "ametysicon-file-extension-html";
case "png":
case "jpg":
case "jpeg":
case "bmp":
case "gif":
return "ametysicon-image2";
case "pdf":
return "ametysicon-file-extension-pdf";
case "ppt":
return "ametysicon-file-extension-ppt";
case "pptx":
return "ametysicon-file-extension-pptx";
case "txt":
return "ametysicon-file-extension-txt";
case "avi":
case "mov":
case "swf":
case "wmv":
case "flv":
case "mpeg":
case "mpg":
case "mp4":
case "ogv":
case "ogg":
case "mkv":
case "webm":
return "ametysicon-movie16";
case "xml":
return "ametysicon-file-extension-xml";
case "xls":
return "ametysicon-file-extension-xls";
case "xlsx":
return "ametysicon-file-extension-xlsx";
case "zip":
return "ametysicon-file-extension-zip";
case "rar":
return "ametysicon-file-extension-rar";
case "odp":
return "ametysicon-file-extension-odp";
case "odt":
return "ametysicon-file-extension-odt";
case "jar":
return "ametysicon-file-extension-jar";
case "pps":
return "ametysicon-file-extension-pps";
case "psd":
return "ametysicon-file-extension-psd";
case "tar":
case "tgz":
return "ametysicon-file-extension-tar";
case "mp3":
case "wav":
case "oga":
case "mod":
case "mid":
return "ametysicon-file-extension-generic-music";
default:
return "ametysicon-file-extension-generic-unknown";
break;
}
}
},
initComponent: function()
{
this.store = this.createTreeStore();
this.store.on('beforeload', this._onBeforeLoad, this);
this.callParent();
},
constructor: function(config)
{
Ext.applyIf(config, {
scrollable:true,
animate:true,
cls: 'explorer-tree',
enableColumnResize: false,
hideHeaders: true,
columns: [{
xtype: 'treecolumn',
dataIndex: 'text',
flex: 1,
editor: {
xtype: 'textfield',
allowBlank: false,
selectOnFocus: true
}
}]
});
if (config.allowNodeEditing !== "false" && config.allowNodeEditing !== false)
{
Ext.applyIf(config, {
plugins: {
ptype: 'cellediting',
clicksToEdit: 2,
listeners: {
'beforeedit' : Ext.bind(this._onBeforeEdit, this),
'edit' : Ext.bind(this._onEdit, this)
}
}
});
}
if (config.allowDragAndDrop !== "false" && config.allowDragAndDrop !== false)
{
Ext.applyIf(config, {
viewConfig: {
plugins: {
ptype: 'ametystreeviewdragdrop',
containerScroll: true,
appendOnly: false,
sortOnDrop: false,
expandDelay: 500,
allowContainerDrops: false,
setAmetysDragInfos: Ext.bind(this.getDragInfo, this),
setAmetysDropZoneInfos: Ext.bind(this.getDropInfo, this)
}
}
});
}
if (this.allowFiltering === "true" || this.allowFiltering === true)
{
this._configureFiltering(config);
}
this.callParent(arguments);
this._resourceTooltipTpl = Ext.create('Ext.Template', [
'<span style="white-space: nowrap"><u>{{i18n PLUGINS_CORE_UI_FILES_EXPLORER_TOOLTIP_DATE}}</u> : </span> <span style="white-space: nowrap">{lastModified}</span><br/>',
'<u>{{i18n PLUGINS_CORE_UI_FILES_EXPLORER_TOOLTIP_SIZE}}</u> : <span style="white-space: nowrap">{size}</span><br/>'
]);
this.on('itemmouseenter', this._createQtip, this);
// Listening to some bus messages.
Ametys.message.MessageBus.on(Ametys.message.Message.CREATED, this._onFileCreated, this);
Ametys.message.MessageBus.on(Ametys.message.Message.MODIFIED, this._onFileModified, this);
Ametys.message.MessageBus.on(Ametys.message.Message.DELETED, this._onFileDeleted, this);
Ametys.message.MessageBus.on(Ametys.message.Message.MOVED, this._onFileMoved, this);
if (config.enableOnFileSelected === "true" || config.enableOnFileSelected === true)
{
// FIXME the SELECTION_CHANGED listener can create infinite loop when there is at least two editors opened then closing a editor that have no focus
// Infinite loop can also occur at startup when there more than one editor is opened.
Ametys.message.MessageBus.on(Ametys.message.Message.SELECTION_CHANGED, this._onFileSelected, this);
}
},
onDestroy: function()
{
Ametys.message.MessageBus.unAll(this);
this.callParent(arguments);
},
/**
* Creates a store for the system files structure
* @return {Ext.data.TreeStore} The tree store managing the system files structure
*/
createTreeStore: function()
{
throw new Error("The method #createTreeStore is not implemented in " + this.self.getName());
},
/**
* This function reload the given node
* @param {String} path The path of the node to reload
* @param {Function} callback The callback function to call after reload. Can be null. Has the following parameters:
* @param {Ext.data.Model} callback.node The refreshed node
*/
refreshNode: function(path, callback)
{
var node;
if (path == null)
{
var selection = this.getSelectionModel().getSelection();
node = selection.length > 0 ? selection[0] : null;
}
else
{
node = path != '' ? this.getSelectionModel().getStore().findRecord('path', path) : this.getRootNode();
}
if (node != null && (node.get('type') == Ametys.file.AbstractFileExplorerTree.TYPE_COLLECTION || node.get('type') == 'root'))
{
node.set('expanded', true);
if (Ext.isFunction(callback))
{
this.store.load({node: node, callback: function(records, op, success) { callback (node); }});
}
else
{
this.store.load({node: node});
}
}
},
/**
* Select a node in the tree
* @param {Ext.data.Model/String} node The node itself or its id
*/
selectNode: function(node)
{
if (typeof node == 'string')
{
node = this.getStore().getNodeById(node);
}
this.getSelectionModel().select([node]);
},
/**
* Expand the given node
* @param {String} node The node to expand.
* @param {Function} callback The callback function to call after reload. Can be null. Has the following parameters:
* @param {Ext.data.Model} callback.node The expended node
* @private
*/
_expandNode: function(node, callback)
{
if (node == null)
{
var selection = this.getSelectionModel().getSelection();
node = selection.length > 0 ? selection[0] : null;
}
if (node != null)
{
callback = callback ? Ext.bind(callback, null, [node]) : null;
this.expandNode(node, false, callback);
}
},
/**
* @private
* Appends the path parameter to store's Ajax requests.
*/
_onBeforeLoad: function(store, operation, eOpts)
{
var path = operation.node.get("path");
if (path)
{
//Passing 'path' as extra parameter during the 'node expand' Ajax call
operation.setParams( Ext.apply(operation.getParams(), {
path: path
}));
}
},
/**
* This listener is called before cell editing is triggered.
* Check the user can edit the node. Returns false if the editing should be canceled.
* @param {Ext.grid.plugin.CellEditing} editor The cell editor
* @param {Object} context An editing context event with the following properties:
* @private
*/
_onBeforeEdit: function(editor, context)
{
var node = this.getSelectionModel().getSelection()[0];
this._currentEditNode = node;
return !node.isRoot();
},
/**
* This listener is called after a cell is edited.
* @param {Ext.grid.plugin.CellEditing} editor The cell editor
* @param {Object} context An editing context event with the following properties:
* @param {Ext.grid.Panel} context.grid The owning grid Panel.
* @param {Ext.data.Model} context.record The record being edited.
* @param {String} context.field The name of the field being edited.
* @param {Mixed} context.value The field's current value.
* @param {HTMLElement} context.row The grid row element.
* @param {Ext.grid.column.Column} context.column The Column being edited.
* @param {Number} context.rowIdx The index of the row being edited.
* @param {Number} context.colIdx The index of the column being edited.
* @private
*/
_onEdit: function(editor, context)
{
if (!this._currentEditNode || Ext.String.trim(context.value) == Ext.String.trim(context.originalValue))
{
return;
}
this._valueBeforeEdit = context.originalValue;
this.editNode(this._currentEditNode, this._valueBeforeEdit, context.value);
},
/**
* @protected
* @template
* Do rename a resource (folder or file)
* @param {Ext.data.Model} node The resource
* @param {String} oldname The name before edit.
* @param {String} newname the new name of resources
*/
editNode: function(node, oldname, newname)
{
throw new Error("The method #editNode is not implemented in " + this.self.getName());
},
/**
* Update the name/text of a node
* @param {Ext.data.Model} node The node to edit. If null nothing will be done.
* @param {String} name The new name to set
*/
updateNodeName: function(node, name)
{
if (node != null)
{
node.beginEdit();
node.set('text', name);
node.set('name', name);
node.endEdit();
node.commit();
}
},
/**
* Get the message target configuration for given node
* @param {Ext.data.Model} node The node.
* @param {Boolean} busMessage True if it is for a bus message, false if it is for the drag and drop.
*/
getMessageTargetConfiguration: function(node, busMessage)
{
throw new Error("The method #getMessageTargetConfiguration is not implemented in " + this.self.getName());
},
/**
* Destroy and create the node tooltip when the mouse enters the node
* @param {Ext.view.View} view The tree view
* @param {Ext.data.Model} node The tree node
* @param {HTMLElement} el The node's element
*/
_createQtip: function(view, node, el)
{
if (node.get('type') != Ametys.file.AbstractFileExplorerTree.TYPE_RESOURCE)
{
return;
}
Ext.QuickTips.unregister(el);
Ext.QuickTips.register(Ext.apply({target: el, id: el.id + '-tooltip'}, this._getTooltip(node)));
},
/**
* Get the tooltip configuration
* @param {Ext.data.Model} node The tree node
* @returns {Object} The tooltip configuration. See Ametys.ui.fluent.Tooltip.
* @private
*/
_getTooltip: function(node)
{
var lastModified = Ext.util.Format.date(Ext.Date.parse(node.get('lastModified'), Ext.Date.patterns.ISO8601DateTime), "{{i18n PLUGINS_CORE_UI_RESOURCE_TOOLTIP_DATE_FORMAT}}");
var text = this._resourceTooltipTpl.applyTemplate ({
author: node.get('author'),
lastModified: lastModified,
size: Ext.util.Format.fileSize(node.get('size'))
});
var extension = Ametys.file.AbstractFileExplorerTree.getFileExtension(node.get('name'));
var isImg = extension == 'jpg' || extension == 'jpeg' || extension == 'gif' || extension == 'png';
return {
title: node.get('name'),
glyphIcon: isImg ? null : Ametys.file.AbstractFileExplorerTree.getFileIconGlyph(node.get('name')),
image: isImg ? this._getThumbnail(node) : null,
imageWidth: isImg ? 100 : 48,
imageHeight: isImg ? 100 : 48,
text: text,
inribbon: false
};
},
/**
* @protected
* Gets the tooltip thumbnail of an image
* @param {Ametys.file.AbstractFileExplorerTree.FileNode} node The node
* @return {String} The url of the thumbnail
*/
_getThumbnail: function(node)
{
return Ametys.getPluginDirectPrefix('core-ui') + "/thumbnail/image?path=" + node.get('path') + "&maxWidth=100&maxHeight=100";
},
/**
* This event is thrown by the getDragData to add the 'source' of the drag.
* @param {Object} item The default drag data that will be transmitted. You have to add a 'source' item in it:
* @param {Ametys.relation.RelationPoint} item.source The source (in the relation way) of the drag operation.
* @private
*/
getDragInfo: function(item)
{
var targets = [];
for (var i = 0; i < item.records.length; i++)
{
var record = item.records[i];
targets.push(this.getMessageTargetConfiguration(record, false));
}
if (targets.length > 0)
{
item.source = {
relationTypes: [Ametys.relation.Relation.MOVE],
targets: targets
};
}
},
/**
* This event is thrown before the beforeDrop event and create the target of the drop operation relation.
* @param {Ext.data.Model[]} targetRecords The target records of the drop operation.
* @param {Object} item The default drag data that will be transmitted. You have to add a 'target' item in it:
* @param {Object} item.target The target (in the relation way) of the drop operation. A Ametys.relation.RelationPoint config.
* @private
*/
getDropInfo: function(targetRecords, item)
{
var targets = [];
for (var i = 0; i < targetRecords.length; i++)
{
var record = targetRecords[i];
targets.push(this.getMessageTargetConfiguration(record, false));
}
if (targets.length > 0)
{
item.target = {
relationTypes: [Ametys.relation.Relation.MOVE],
targets: targets
};
}
},
/**
* Listener on {@link Ametys.message.Message#CREATED} message.
* If the tool is concerned by the created object, the parent node will be refreshed.
* @param {Ametys.message.Message} message The created message.
* @private
*/
_onFileCreated: function(message)
{
var folderTarget = message.getTarget(this.getMessageTargetIdForCollection());
if (folderTarget != null)
{
// Refresh parent node and rename new created node
var path = folderTarget.getParameters().path;
var parentPath = path.substring(0, path.lastIndexOf('/'));
if (this.allowNodeEditing)
{
this.refreshNode(parentPath, Ext.bind(this._renameNode, this, [folderTarget.getParameters().path], true));
}
else
{
this.refreshNode(parentPath, Ext.bind(this._selectNodeByPath, this, [folderTarget.getParameters().path]));
}
return;
}
var fileTarget = message.getTarget(this.getMessageTargetIdForResource());
if (fileTarget != null)
{
// Refresh parent node and select new node
var path = fileTarget.getParameters().path;
var parentPath = path.substring(0, path.lastIndexOf('/'));
this.refreshNode(parentPath, Ext.bind(this._selectNodeByPath, this, [path]));
}
},
/**
* Call this function to rename a node
* @param {Ext.data.Model} parentNode The parent node
* @param {String} path The path of the node to rename
*/
_renameNode: function(parentNode, path)
{
if (parentNode != null && path != null)
{
var node = this.getSelectionModel().getStore().findRecord('path', path);
this.selectNode(node);
Ext.defer(this._deferredEdit, 300, this, [node]);
}
},
/**
* Deferred function to edit a node
* @param {Ext.data.Model} node The node to start edit
*/
_deferredEdit: function(node)
{
this.editingPlugin.startEdit(node, 0);
},
/**
* Listener on {@link Ametys.message.Message#DELETED} message.
* If the tool is concerned by the deleted object, the object's node will be deleted.
* @param {Ametys.message.Message} message The deleted message.
* @private
*/
_onFileDeleted: function(message)
{
var targets = message.getTargets(this.testTarget);
if (targets.length > 0)
{
for (var i=0; i < targets.length; i++)
{
this._deleteNode(targets[i].getParameters().path);
}
}
},
/**
* Deletes a node from the tree and selects the parent node.
* @param {String} path The path of the node to delete.
* @private
*/
_deleteNode: function(path)
{
var node = this.getSelectionModel().getStore().findRecord('path', path);
if (node != null)
{
var parentNode = node.parentNode;
this.getSelectionModel().select([parentNode]);
node.remove();
}
},
/**
* Listener on {@link Ametys.message.Message#MODIFIED} message.
* If the tool is concerned by the modified object, the parent node will be refreshed.
* @param {Ametys.message.Message} message The modified message.
* @private
*/
_onFileModified: function(message)
{
var me = this;
var targets = message.getTargets(this.testTarget);
if (targets.length > 0)
{
for (var i=0; i < targets.length; i++)
{
var target = targets[i];
var path = target.getParameters().path;
var node = this.getSelectionModel().getStore().findRecord('path', path, 0, false, true); // case sensitive search
if (node == null)
{
// Node have been renamed or moved
var parentPath = path.substring(0, path.lastIndexOf('/'));
var parentNode = this.getSelectionModel().getStore().findRecord('path', path);
this.refreshNode(parentPath, Ext.bind(this._selectNodeByPath, this, [path]));
}
else
{
this.refreshNode(path, Ext.bind(this._selectNodeByPath, this, [path]));
}
}
}
},
/**
* Listener on {@link Ametys.message.Message#MOVED} message.
* If the tool is concerned by the modified object, the node will be refreshed.
* @param {Ametys.message.Message} message The modified message.
* @private
*/
_onFileMoved: function(message)
{
var targets = message.getTargets(this._checkTarget);
if (targets.length > 0)
{
for (var i = 0; i < targets.length; i++)
{
var target = targets[i];
var path = target.getParameters().path;
if (path)
{
path = path.substr(0, path.lastIndexOf("/"));
}
this.refreshNode(path);
var oldPath = message.getParameters().oldPath;
if (oldPath)
{
var oldNode = this.getSelectionModel().getStore().findRecord('path', oldPath);
if (oldNode != null)
{
oldNode.remove();
}
}
}
}
},
/**
* @protected
* Get the id of message target for a file resource
* @return {String|Function} the message target type or a function testing the target
*/
getMessageTargetIdForResource: function ()
{
throw new Error("The method #getMessageTargetIdForResource is not implemented in " + this.self.getName());
},
/**
* @protected
* Get the id of message target for a folder resource
* @return {String|Function} the message target type or a function testing the target
*/
getMessageTargetIdForCollection: function ()
{
throw new Error("The method #getMessageTargetIdForCollection is not implemented in " + this.self.getName());
},
/**
* Select a node by its path
* @param {String} path the path
*/
_selectNodeByPath: function(path)
{
var node = this.getSelectionModel().getStore().findRecord('path', path);
if (node != null)
{
this.selectNode(node);
}
},
/**
* Listener on {@link Ametys.message.Message#SELECTION_CHANGED} message.
* If the tool is concerned by the created object, the parent node will be refreshed.
* @param {Ametys.message.Message} message The created message.
* @private
*/
_onFileSelected: function(message)
{
var me = this;
var target = message.getTarget(this.testTarget);
if (target)
{
var node = this.getSelectionModel().getStore().findRecord('path', target.getParameters().path);
if (node && this.getSelectionModel().isSelected(node))
{
return;
}
this.expandPath ("/" + this.getRootNode().get('name') + "/" + target.getParameters().path, 'name', '/', function (success, lastNode) {
if (success && lastNode)
{
me.selectNode(lastNode);
}
});
}
},
/**
* @protected
* @template
* Test if the target matches
* @param {Ametys.message.MessageTarget} target The target to test.
*/
testTarget: function (target)
{
throw new Error("The method #testTarget is not implemented in " + this.self.getName());
},
_configureFiltering: function(config)
{
var dockedItems = config.dockedItems || [];
dockedItems.push(this._getFilterToolbarCfg());
dockedItems.push(this._getNoResultPanelCfg());
config.dockedItems = dockedItems;
this._counter = {};
this._filterValue = null;
},
_getFilterToolbarCfg: function()
{
return {
dock: 'top',
xtype: 'toolbar',
itemId: 'toolbar',
layout: {
type: 'hbox',
align: 'stretch'
},
items: [{
// Search input
xtype: 'textfield',
cls: 'ametys',
flex: 1,
maxWidth: 300,
itemId: 'search-filter-input',
emptyText: "{{i18n PLUGINS_CORE_UI_FILES_EXPLORER_TREE_FILTER}}",
minLength: 3,
minLengthText: "{{i18n PLUGINS_CORE_UI_FILES_EXPLORER_TREE_FILTER_INVALID}}",
listeners: {change: Ext.Function.createBuffered(this._searchFilter, 500, this)}
}, {
// Clear search filter
xtype: 'button',
tooltip: "{{i18n PLUGINS_CORE_UI_FILES_EXPLORER_TREE_CLEAR_FILTER}}",
handler: this._clearSearchFilter,
scope: this,
iconCls: 'a-btn-glyph ametysicon-eraser11 size-16',
cls: 'a-btn-light'
}]
};
},
_getNoResultPanelCfg: function()
{
return {
dock: 'top',
xtype: 'button',
hidden: true,
itemId: 'noresult',
ui: 'tool-hintmessage',
text: "{{i18n PLUGINS_CORE_UI_FILES_EXPLORER_TREE_FILTER_NO_MATCH}}" + "{{i18n PLUGINS_CORE_UI_FILES_EXPLORER_TREE_FILTER_NO_MATCH_ACTION}}",
scope: this,
handler: this._clearSearchFilter
};
},
/**
* This listener is call on 'change' event on filter input field
* Filters the tree by text input.
* @private
*/
_searchFilter: function(field)
{
var value = Ext.String.trim(field.getValue());
if (this._filterValue == value)
{
// Do nothing
return;
}
this._filterValue = value;
if (value.length >= field.minLength)
{
this._filterNodes(value, [this.getRootNode()]);
}
else
{
this._hideNoResultPanel();
this.clearFilter();
}
},
/**
* Get the resources the name matches the given value.
* Only descendant of nodes will be considered in the search.
*
* @param {String} value The value to match
* @param {Ext.data.Model[]} nodes The nodes where starting search
* @param {Boolean} [childNodesOnly] set to 'true' if you want to keep the nodes even if no child match.
* @param {Ext.data.TreeModel} [rootNode] The node to start filtering
* @private
*/
_filterNodes(value, nodes, childNodesOnly, rootNode)
{
// count the number of filter computed
// to be able to tell when all result are available
this._filterCounter = 0;
this._filterTotal = nodes.length;
this._hasFilterResult = false;
for (var i=0; i < nodes.length; i++)
{
var node = nodes[i];
this._getFilteredPath(value, node, childNodesOnly, rootNode);
}
},
/**
* Get the path of the child of node that match 'value'.
* The path are expected to be relative to rootNode.
* After computing the path, the method is expected to call
* #_getFilteredPathCb.
* @protected
* @template
*/
_getFilteredPath: function(value, node, childNodesOnly, rootNode)
{
throw new Error("The method #_getFilteredPath is not implemented in " + this.self.getName());
},
_getFilteredPathCb: function(paths, args)
{
this._filterCounter++;
var hasResult = false;
var node = args.node;
if (!paths)
{
return;
}
if (paths.length == 0)
{
if (args.childNodesOnly)
{
// There is no child nodes matching but some other nodes are matching.
hasResult = true;
for (var i=0; i < node.childNodes.length; i++)
{
this.filterBy (function () {return false}, node.childNodes[i]);
}
}
else
{
this.filterBy (function () {return false}, node);
}
}
else
{
hasResult = true;
this._expandAndFilter (paths, args.rootNode || node, node);
}
this._hasFilterResult = hasResult || this._hasFilterResult;
// If all the filter are computed, update the no search result panel
if (this._filterCounter == this._filterTotal)
{
if (!this._hasFilterResult)
{
this._showNoResultPanel();
}
else
{
this._hideNoResultPanel(false)
}
this._filterCounter = 0;
this._filterTotal = 0;
this._hasFilterResult = false;
}
},
/**
* Expand the tree to the given paths. Then filter nodes matching the given paths by calling the #_filterPaths method
* @param {String[]} paths The path to expand
* @param {Ext.data.Model} rootNode The concerned root node
* @param {Ext.data.Model} node The node from which apply filter
* @private
*/
_expandAndFilter: function(paths, rootNode, node)
{
node = node || rootNode;
this._counter[rootNode.getId()] = paths.length;
for (var i=0; i < paths.length; i++)
{
this.expandPath(rootNode.getPath('name') + paths[i], {
field:'name',
callback: Ext.bind (this._filterPaths, this, [paths, rootNode, node])
});
}
},
/**
* Filter nodes by path once the last expand has been processed
* @param {String[]} paths The path to filter by
* @param {Ext.data.Model} rootNode The concerned root node
* @param {Ext.data.Model} node The node from which apply filter
* @private
*/
_filterPaths: function (paths, rootNode, node)
{
// only execute the filterBy after the last expandPath()
if (--this._counter[rootNode.getId()] == 0)
{
var filterFn = Ext.bind (this._filterByPath, this, [paths, rootNode], true);
// Ensure that expand is complete by deferring the filterBy function ...
Ext.defer(this.filterBy, 50, this, [filterFn, node]);
}
},
/**
* Returns true if the node path is a part of given paths
* @param {Ext.data.Model} node The node to test
* @param {String[]} paths The paths
* @param {Ext.data.Model} rootNode The root node to build the complete paths
* @private
*/
_filterByPath: function (node, paths, rootNode)
{
var currentPath = node.getPath('name');
for (var i=0; i < paths.length; i++)
{
var path = rootNode.getPath('name') + paths[i] + '/';
if (path.indexOf(currentPath + '/') == 0)
{
return true;
}
}
},
/**
* Filters by a function. The specified function will be called for each Record in this Store.
* If the function returns true the Record is included, otherwise it is filtered out.
* @param {Function} filterFn A function to be called.
*/
filterBy: function (filterFn)
{
this.clearFilter();
this.getStore().filterBy(filterFn);
},
/**
* Hide the panel showing there is no result.
* @private
*/
_hideNoResultPanel: function ()
{
this.headerCt.show() // Show column title
this.body.show() // Show body
var noResultPanel = this.getDockedComponent('noresult');
if (noResultPanel)
{
noResultPanel.hide();
}
},
/**
* Show the panel showing there is no result.
* @private
*/
_showNoResultPanel: function ()
{
this.headerCt.hide() // Hide column title
this.body.hide() // Hide body
var noResultPanel = this.getDockedComponent('noresult');
if (noResultPanel)
{
noResultPanel.show();
}
},
/**
* Clear all filters
*/
clearFilter: function ()
{
this._filterValue = null;
this.getStore().clearFilter();
},
/**
* Clear the filter search
* @param {Ext.Button} btn The button
* @private
*/
_clearSearchFilter: function(btn)
{
this._hideNoResultPanel();
this.clearFilter();
this.getDockedComponent('toolbar').down('#search-filter-input').reset();
},
});