/*
* Copyright 2015 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 class provides a TreePanel for tree-structured resources (files and folders)
* The tree includes a toolbar which allow to :
* - filter resources on name,
* - refresh a node
* - expand a node
* - collapse the whole tree.
* The default tree store uses the {@link Ametys.explorer.tree.ExplorerTree.NodeEntry} model. Override the #createTreeStore method to use another Model.
*
* Creates your own ExplorerTree class by inheriting this one and define at least the following methods: #getRootIds, #getDragInfo and #getDropInfo.
*
* Ext.define("My.ExplorerTree", {
* extend: "Ametys.explorer.tree.ExplorerTree",
*
* getRootIds: function() {
* return [...];
* },
*
* getDragInfo: function(item) {
* ...
* }
*
* getDropInfo: function(targetRecord, item) {
* ...
* }
* });
*/
Ext.define('Ametys.explorer.tree.ExplorerTree', {
extend: 'Ext.tree.Panel',
/**
* @property {String[]} _rootIds List of ids of the direct children of the root node.
* @private
*/
/**
* @cfg {Boolean} [inlineEditionEnable=false] True to enable inline edition of the tree node. Default to false.
*/
/**
* @property {Boolean} _inlineEditionEnable See #cfg-inlineEditionEnable
* @private
*/
/**
* @cfg {Boolean} [ignoreFiles=false] True to restrict the tree to folders (ignore files). Default to false.
*/
/**
* @property {Boolean} _ignoreFiles See #cfg-ignoreFiles
* @private
*/
/**
* @cfg {String[]} [allowedExtensions] The allowed file extensions. If not null, only files with an allowed extension will be visibles in the tree.
*/
/**
* @property {String[]} _allowedExtensions See #cfg-allowedExtensions
* @private
*/
/**
* @cfg {Boolean} [allowDragAndDrop=true] True to enable drag and drop in the tree
*/
/**
* @property {Boolean} _allowDragAndDrop See #cfg-allowDragAndDrop
* @private
*/
/**
* @private
* @property {Ext.Template} _resourceTooltipTpl The template used for resources' tooltip
*/
/**
* @private
* @property {Ext.data.TreeModel} _currentEditNode The current node being edited
*/
/**
* @private
* @property {String} _valueBeforeEdit The value of node text before editing it
*/
statics: {
/**
* The default filter.
*/
DEFAULT_FILTER: [],
/**
* The filter to use to view only images
*/
IMAGE_FILTER: ['jpg', 'jpeg', 'gif', 'png', 'svg'],
/**
* @property {RegExp} _regexpImage The RegExp for image filter.
* @static
* @private
*/
_regexpImage: /\.(jpg|jpeg|gif|png|svg)$/i,
/**
* The filter to use to view only flash
*/
FLASH_FILTER: ['swf'],
/**
* @property {RegExp} _regexpFlash The RegExp for flash filter.
* @static
* @private
*/
_regexpFlash: /\.swf$/i,
/**
* The filter to use to view only video or audio files
*/
MULTIMEDIA_FILTER: ['mp4', 'ogv', 'ogg', 'webm', 'swf', 'flv', 'mp3'],
/**
* @property {RegExp} _regexpMultimedia The RegExp for multimedia filter.
* @static
* @private
*/
_regexpMultimedia: /\.(swf|flv|mp3)$/i,
/**
* The filter to use to view only video files
*/
VIDEO_FILTER: ['mp4', 'ogv', 'ogg', 'webm'],
/**
* @property {RegExp} _regexpVideo The RegExp for video filter.
* @static
* @private
*/
_regexpVideo: /\.(mp4|ogv|ogg|webm)$/i,
/**
* The filter to use to view audio files
*/
SOUND_FILTER: ['mp3', 'oga', 'wav'],
/**
* @property {RegExp} _regexpSound The RegExp for sound filter.
* @static
* @private
*/
_regexpSound: /\.mp3$/i,
/**
* The filter to use to view only mp3
*/
PDF_FILTER: ['pdf'],
/**
* @property {RegExp} __regexpPdf The RegExp for pdf filter.
* @static
* @private
*/
_regexpPdf: /\.pdf$/i,
/**
* Get the icon in small format (16x16 pixels) of a file
* @param {String} name The file name
* @return {String} The small icon
*/
getFileSmallIcon: function (name)
{
var extension = Ametys.file.AbstractFileExplorerTree.getFileExtension(name);
return Ametys.getPluginDirectPrefix('explorer') + "/icon/" + extension + ".png";
},
/**
* Get the icon in large format of a file. If the file is an image, a thumbnail will be calculated.
* @param {String} id The id of the resource
* @param {String} name The file name
* @return {String} The large icon
*/
getFileLargeIcon: function (id, name)
{
var extension = Ametys.file.AbstractFileExplorerTree.getFileExtension(name);
if (extension == 'jpg' || extension == 'jpeg' || extension == 'gif' || extension == 'png')
{
return Ametys.getPluginDirectPrefix('explorer') + "/resource?id=" + id + "&maxWidth=100&maxHeight=100";
}
else
{
return Ametys.getPluginDirectPrefix('explorer') + "/thumbnail/" + extension + ".png";
}
},
/**
* Get the URL to view a resource
* @param {String} id The resource id
*/
getViewHref: function(id)
{
return Ametys.getPluginDirectPrefix('explorer') + '/resource?id=' + encodeURIComponent(id);
},
/**
* Get the URL to download the resource
* @param {String} id The resource id
*/
getDownloadHref: function(id)
{
return Ametys.getPluginDirectPrefix('explorer') + '/resource?id=' + encodeURIComponent(id) + '&download=true';
}
},
inheritableStatics: {
/**
* @property {String} RESOURCES_TYPE the type for resources
* @readonly
*/
RESOURCE_TYPE: 'resource',
/**
* @property {String} COLLECTION_TYPE the type for collections
* @readonly
*/
COLLECTION_TYPE: 'collection'
},
cls: 'explorer-tree',
animate: true,
scrollable: true,
rootVisible: false,
initComponent: function ()
{
var plugins = {};
if (this._allowDragAndDrop)
{
plugins = {
ptype: 'ametystreeviewdragdrop',
containerScroll: true,
appendOnly: true,
sortOnDrop: true,
expandDelay: 500,
setAmetysDragInfos: Ext.bind(this.getDragInfo, this),
setAmetysDropZoneInfos: Ext.bind(this.getDropInfo, this)
};
}
Ext.apply(this, {
folderSort: true,
root: {
id: 'dummy',
type: 'collection',
editable: false,
allowDrag: false,
allowDrop: false,
text : 'dummy',
name: 'dummy',
path: 'dummy',
expanded: false
},
store: this.createTreeStore(),
viewConfig: {
plugins: plugins,
loadMask:false
}
});
this.callParent(arguments);
},
constructor: function (config)
{
// Docked items
config.dockedItems = this.getDockItems(config);
// No result panel
var noResultPanelCfg = this._getNoResultPanelCfg();
if (noResultPanelCfg)
{
noResultPanelCfg.dock = noResultPanelCfg.dock || 'top';
config.dockedItems.push(noResultPanelCfg);
}
this._inlineEditionEnable = config['inlineEditionEnable'];
if (this._inlineEditionEnable === true)
{
config.plugins = {
ptype: 'cellediting',
clicksToEdit: 1,
editAfterSelect: true,
silentlyIgnoreInvalid: false,
listeners: {
'beforeedit' : Ext.bind(this._onBeforeEdit, this),
'edit' : Ext.bind(this._onEdit, this)
}
};
config.columns = [{
xtype: 'treecolumn',
dataIndex: 'text',
flex: 1,
editor: {
xtype: 'textfield',
allowBlank: false,
selectOnFocus: true
}
}];
}
config.enableColumnResize = false;
config.hideHeaders = true;
// ignore files must be set before the call to the parent ctor
this._ignoreFiles = (Ext.isBoolean(config.ignoreFiles) && config.ignoreFiles) || false;
this._allowDragAndDrop = (Ext.isBoolean(config.allowDragAndDrop) && Ext.isDefined(config.allowDragAndDrop)) ? config.allowDragAndDrop : true;//default true
this.callParent(arguments);
/**
* @event filterupdated
* Fires when a filter was updated
* @param {Ametys.explorer.tree.ExplorerTree} tree The explorer tree
*/
/**
* @event rootnodeschanged
* Fires when the root nodes were loaded
* @param {Ametys.explorer.tree.ExplorerTree} tree The explorer tree
*/
this._resourceTooltipTpl = Ext.create ('Ext.Template', [
'<u>{{i18n plugin.explorer:PLUGINS_EXPLORER_RESOURCE_TOOLTIP_AUTHOR}}</u> : {author}<br/>',
'<span style="white-space: nowrap"><u>{{i18n plugin.explorer:PLUGINS_EXPLORER_RESOURCE_TOOLTIP_DATE}}</u> : </span> <span style="white-space: nowrap">{lastModified}</span><br/>',
'<u>{{i18n plugin.explorer:PLUGINS_EXPLORER_RESOURCE_TOOLTIP_SIZE}}</u> : <span style="white-space: nowrap">{size}</span><br/>'
]);
this._allowedExtensions = config.allowedExtensions || config.filter || null;
this._filterValue = null;
this._rootIds = [];
this.on('itemmouseenter', this._createQtip, this);
this.on ('load', this._onLoad, this);
},
/**
* Get the ids of root nodes.
* @return {String[]} The ids of root nodes
*/
getRootIds: function ()
{
return this._rootIds;
},
/**
* Create the tree store
* @param {Object} config The tree panel configuration
* @return {Ext.data.TreeStore} The created tree store
*/
createTreeStore: function (config)
{
var url = this._ignoreFiles === true ? 'explorer-nodes' : 'child-nodes';
return Ext.create('Ext.data.TreeStore', Ext.apply({
model: 'Ametys.explorer.tree.ExplorerTree.NodeEntry',
proxy: {
type: 'ametys',
plugin: 'explorer',
url: url,
reader: {
type: 'xml',
rootProperty: 'Nodes',
record: '> Node'
}
},
autoLoad : false,
listeners: {
'beforeload': {fn: this._handleBeforeLoad, scope: this}
}
}, this._getStoreSortInfo()));
},
/**
* @protected
* Must return an object containing sort configuration property for the internal {@link Ext.data.TreeStore}
*/
_getStoreSortInfo: function()
{
return {
folderSort: true,
sorters: [{
sorterFn: function(n1, n2) {
// sort of hack to be sure that root nodes are always
// sorted the same way, whatever the language
// folders first
var isN1Collection = n1.data.type == 'collection';
var isN2Collection = n2.data.type == 'collection';
if (!((isN1Collection && isN2Collection) || (!isN1Collection && !isN2Collection)))
{
return isN1Collection && !isN2Collection ? -1 : 1;
}
var v1 = n1.getDepth() == 1 ? n1.get('name') : n1.get('text');
var v2 = n2.getDepth() == 1 ? n2.get('name') : n2.get('text');
// see {@link Ext.util.Sorter#defaultSorterFn}
return Ext.String.enhancedCompare(v1, v2);
}
}]
};
},
/**
* Handle the before load event of the store in order to cancel the load request on the root node.
* Be careful, this is mandatory is our case only because we have an hidden root node, which automatically perform a load when the node is created.
* @param {Ext.data.Store} store This store
* @param {Ext.data.operation.Operation} operation The Ext.data.operation.Operation object that will be passed to the Proxy to
* load the Store
*/
_handleBeforeLoad: function(store, operation)
{
var node = operation.node,
id = node != null ? node.getId() : null;
// Cancel load
if (!id || id === 'dummy')
{
return false;
}
operation.setParams( Ext.apply(operation.getParams() || {}, {
allowedExtensions: this._allowedExtensions
}));
},
/**
* Set the tree root nodes
* @param {Object[]/Ametys.explorer.tree.ExplorerTree.NodeEntry[]} rootNodes the root nodes array. Either explorer tree node entry objects or config objects.
* @param {Function} [callback] Function to execute once the root node expand completes
*/
setRootNodes: function(rootNodes, callback)
{
rootNodes = Ext.Array.from(rootNodes);
this.getRootNode().removeAll();
this._rootIds = [];
for (var i=0; i < rootNodes.length; i++)
{
this.getRootNode().appendChild(rootNodes[i]);
this._rootIds.push(rootNodes[i].id);
}
this.getRootNode().expand(false, callback);
this.fireEvent("rootnodeschanged", this);
},
/**
* Refresh the root nodes
* @param {Function} callback The function called when all root nodes have been refreshed
* @param {Object} scope the callback scope
*/
refreshRootNodes: function(callback, scope)
{
var callback = Ext.isFunction(callback) ? callback : Ext.emptyFn,
rl = this._rootIds.length,
me = this;
var i = 0;
function refreshNextNode ()
{
if (i == rl)
{
callback.call(scope);
}
else
{
me.refreshNode(me._rootIds[i++], refreshNextNode);
}
}
refreshNextNode();
},
/**
* Get the dock items
* @param {Object} config The initial tree panel configuration
* @return {Object[]} The dock items configuration
*/
getDockItems: function (config)
{
var dockedItems = config.dockedItems || [];
var toolbarCfg = this._getToolbarCfg();
if (toolbarCfg)
{
toolbarCfg.dock = toolbarCfg.dock || 'top';
dockedItems.push(toolbarCfg);
}
return dockedItems;
},
/**
* Get the right id to check when renaming a resource
* @return the right id for rename action
*/
getRightIdOnRename: function(node)
{
return 'Plugin_Explorer_Folder_Edit';
},
/**
* Get the right id to check when dropping a resource
* @return the right id for dropping action
*/
getRightIdOnDrop: function(node)
{
return 'Plugin_Explorer_Folder_Add';
},
/**
* Get the right id to check when dragging a resource
* @return the right id for dragging action
*/
getRightIdOnDrag: function (node)
{
return 'Plugin_Explorer_Folder_Delete';
},
/**
* Determines if user has the given right on current selection or given node
* @param {String} rightId The right's id to check
* @param {Ext.data.Model} node The node to check right on. If null the right will be search on current message target. Otherwise, the right will be check in asynchronous mode by sending a server request. Use the callback function in this last case.
* @param {Function} callback Callback function to call after checking user right in asynchronous mode. Can be null if node is null.
* @return true if the user has right in synchronous mode.
*/
hasRight: function(rightId, node, callback)
{
if (!rightId)
{
return true;
}
var rights = [];
var message = Ametys.message.MessageBus.getCurrentSelectionMessage(),
target = message.getTarget(function(target) {
return Ametys.message.MessageTarget.EXPLORER_COLLECTION == (target.id || target.getId()) || Ametys.message.MessageTarget.RESOURCE == (target.id || target.getId());
});
if (target)
{
rights = target.getParameters().rights || [];
}
if (Ext.Array.contains(rights, rightId))
{
return true;
}
return false;
},
/**
* 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') != this.self.RESOURCE_TYPE)
{
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 plugin.core-ui: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: Ext.String.escapeHtml(node.get('name')),
glyphIcon: isImg ? null : Ametys.file.AbstractFileExplorerTree.getFileIconGlyph(node.get('name')),
image: isImg ? Ametys.explorer.tree.ExplorerTree.getFileLargeIcon(node.getId(), node.get('name')) : null,
imageWidth: isImg ? 100 : 48,
imageHeight: isImg ? 100 : 48,
text: text,
inribbon: false
};
},
/**
* This function is called after loading node
* @param {Ext.data.TreeStore} store The tree store
* @param {Ext.data.TreeModel[]} records The records
* @param {Boolean} successful True if the operation was successful.
* @param {Ext.data.operation.Operation} operation The operation that triggered this load.
* @param {Ext.data.NodeInterface} node The node that was loaded.
*/
_onLoad: function (store, records, successful, operation, node)
{
// Ext.defer(this.clearFilter, 1, this, [], false);
},
/**
* @protected
* Retrieves the top toolbar config object.
*/
_getToolbarCfg: function()
{
return {
dock: 'top',
xtype: 'toolbar',
layout: {
type: 'hbox',
align: 'stretch'
},
defaultType: 'button',
items: [{
// Search input
xtype: 'textfield',
cls: 'ametys',
flex: 1,
maxWidth: 300,
itemId: 'search-filter-input',
emptyText: "{{i18n PLUGINS_EXPLORER_UITOOL_EXPLORER_TREE_FILTER}}",
enableKeyEvents: true,
minLength: 3,
minLengthText: "{{i18n PLUGINS_EXPLORER_UITOOL_EXPLORER_TREE_FILTER_INVALID}}",
msgTarget: 'qtip',
listeners: {change: Ext.Function.createBuffered(this._searchFilter, 500, this)}
}, {
// Clear filter
tooltip: "{{i18n PLUGINS_EXPLORER_UITOOL_EXPLORER_TREE_CLEAR_FILTER}}",
handler: Ext.bind (this._clearSearchFilter, this),
iconCls: 'a-btn-glyph ametysicon-eraser11 size-16',
cls: 'a-btn-light'
},
{
xtype: 'tbspacer',
flex: 0.0001
}, {
// Expand node
ui: 'light',
tooltip: "{{i18n PLUGINS_EXPLORER_UITOOL_EXPLORER_TREE_EXPAND_ALL}}",
handler: Ext.bind (this._expandNode, this, [], false),
icon: Ametys.getPluginResourcesPrefix('explorer') + '/img/tree/expand-all.gif',
cls: 'x-btn-text-icon'
}, {
// Collapse all
tooltip: "{{i18n PLUGINS_EXPLORER_UITOOL_EXPLORER_TREE_COLLAPSE_ALL}}",
handler: Ext.bind (this.collapseAll, this, [], false),
iconCls: 'a-btn-glyph ametysicon-minus-sign4 size-16',
cls: 'a-btn-light'
}, {
// Refresh node
ui: 'light',
itemId: 'refresh-btn',
tooltip: "{{i18n PLUGINS_EXPLORER_UITOOL_EXPLORER_TREE_REFRESH_PAGE}}",
handler: Ext.bind (this.refreshNode, this, [], false),
iconCls: 'a-btn-glyph ametysicon-arrow123 size-16',
cls: 'a-btn-light'
}]
};
},
/**
* @protected
* Retrieves the 'no result' panel config
*/
_getNoResultPanelCfg: function()
{
return {
dock: 'top',
xtype: 'button',
hidden: true,
itemId: 'noresult',
ui: 'tool-hintmessage',
text: "{{i18n PLUGINS_EXPLORER_UITOOL_EXPLORER_TREE_FILTER_NO_MATCH}}" + "{{i18n PLUGINS_EXPLORER_UITOOL_EXPLORER_TREE_FILTER_NO_MATCH_ACTION}}",
scope: this,
handler: this._clearSearchFilter
};
},
/**
* This listener is called on 'keyup' event on filter input field.
* Filters the tree by text input.
* @param {Ext.form.Field} field The field
* @private
*/
_searchFilter: function (field)
{
var value = Ext.String.trim(field.getValue());
if (this._filterValue == value)
{
// Do nothing
return;
}
this._filterValue = value;
if (value.length > 2)
{
var rootNodes = [];
for (var i=0; i < this._rootIds.length; i++)
{
rootNodes.push(this.getStore().getNodeById(this._rootIds[i]));
}
this._getFilteredResources (value, rootNodes);
// FIXME When filter is active, disable refresh button to avoid issues
// Indeed, for obscure reasons, and probably due to a extjs issue, when the tree contains many files, sometimes the refresh of a node
// make visible all files of the tree regardless of the active filter (from search input)
this.down('#refresh-btn').disable();
}
else
{
this._hideNoResultPanel();
this.clearFilter();
}
},
/**
* Get the resources the name matches the given value
* @param {String} value The value to match
* @param {Ext.data.Model[]} nodes The nodes where starting search
* @param {Boolean} [childNodesOnly] set to 'true' to filter the child nodes only.
* @param {Ext.data.TreeModel} [rootNode] The node to start filtering
* @protected
*/
_getFilteredResources: function (value, nodes, childNodesOnly, rootNode)
{
if (!this._filterRunning)
{
this._filterRunning = true;
this._filterCounter = nodes.length;
this._hasFilterResult = false;
this._filteredPaths = {};
for (var i=0; i < nodes.length; i++)
{
var node = nodes[i];
Ametys.data.ServerComm.callMethod({
role: "org.ametys.plugins.explorer.resources.actions.ExplorerResourcesDAO",
methodName: 'filterResourcesByRegExp',
parameters: [node.getId(), value, this._allowedExtensions],
errorMessage: "{{i18n PLUGINS_EXPLORER_UITOOL_EXPLORER_TREE_SEARCH_ERROR}}",
callback: {
handler: this._getFilteredResourcesCb,
scope: this,
arguments: {
node: node,
childNodesOnly: childNodesOnly,
rootNode: rootNode || node
}
}
});
}
}
else
{
Ext.defer(this._getFilteredResources, 100, this, [value, nodes, childNodesOnly, rootNode]);
}
},
/**
* @private
* Callback function after searching resources
* @param {Object} paths The paths of matching resources
* @param {Object[]} args The callback arguments
*/
_getFilteredResourcesCb: 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;
var rootNode = args.rootNode || node;
// store matching paths for this root node (they will be filtered after all root nodes are processed)
this._filteredPaths[rootNode.getId()] = paths;
}
this._hasFilterResult = hasResult || this._hasFilterResult;
if (this._filterCounter == 0)
{
if (!this._hasFilterResult)
{
this._showNoResultPanel();
if (this._filterField)
{
Ext.defer (this._filterField.markInvalid, 100, this._filterField, ["{{i18n PLUGINS_EXPLORER_UITOOL_EXPLORER_TREE_FILTER_NO_MATCH}}"]);
}
this._filterRunning = false;
}
else
{
// Expand all matching resources' paths, then filter after the last expand
this._expandAndFilter(this._filteredPaths);
this._hideNoResultPanel(false)
if (this._filterField)
{
this._filterField.clearInvalid();
}
}
this._filterCounter = 0;
this._hasFilterResult = false;
this._filteredPaths = {};
}
},
/**
* Expand the tree to the given paths. Then filter nodes matching the given paths by calling the #_filterPaths method
* @param {Object} paths The filtered paths to expand for each root node
* @private
*/
_expandAndFilter: function(filteredPaths)
{
this._expandCounter = 0;
for (var rootId in filteredPaths)
{
this._expandCounter += filteredPaths[rootId].length;
}
for (var rootId in filteredPaths)
{
var paths = filteredPaths[rootId];
var rootNode = this.getStore().getNodeById(rootId);
for (var i=0; i < paths.length; i++)
{
this.expandPath(rootNode.getPath('name') + paths[i], {
field:'name',
callback: Ext.bind (this._filterPaths, this, [filteredPaths])
});
}
}
},
/**
* Filter nodes by path once the last expand has been processed
* @param {Object} filteredPaths The paths to filter by for each root node
* @private
*/
_filterPaths: function (filteredPaths)
{
// only execute the filterBy after the last expandPath()
if (--this._expandCounter == 0)
{
var filterFn = Ext.bind (this._filterByPath, this, [filteredPaths], true);
// Ensure that expand is complete by deferring the filterBy function ...
Ext.defer(this.filterBy, 50, this, [filterFn]);
this._filterRunning = false;
}
},
/**
* Returns true if the node path is a part of given paths
* @param {Ext.data.Model} node The node to test
* @param {Object} filteredPaths The given paths by for each root node
* @private
*/
_filterByPath: function (node, filteredPaths)
{
var currentPath = node.getPath('name');
for (var rootId in filteredPaths)
{
var paths = filteredPaths[rootId];
var rootNode = this.getStore().getNodeById(rootId);
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);
},
/**
* Clear all filters
*/
clearFilter: function ()
{
this._filterValue = null;
this.down('#refresh-btn').enable();
this.getStore().clearFilter();
},
/**
* Update the filter for files.
* @param {Function} filter The filter function. Use a filter constant. Can be null.
* @param {String[]} allowedExtensions The allowed extensions. Can be null.
*/
updateFilter: function (filter, allowedExtensions)
{
this._allowedExtensions = allowedExtensions || filter || null;
this.refreshRootNodes(this._updateFilterCb, this);
},
/**
* Callback function called after #updateFilter is processed
* @private
*/
_updateFilterCb: function ()
{
this.fireEvent ('filterupdated', this);
},
/**
* Clear the filter search
* @param {Ext.Button} btn The button
* @protected
*/
_clearSearchFilter: function(btn)
{
this.clearFilter();
this.getDockedItems('container[dock="top"]')[0].down('#search-filter-input').reset();
this._hideNoResultPanel();
},
/**
* Hide the panel showing there is no result.
* @private
*/
_hideNoResultPanel: function ()
{
var noResultPanel = this.getDockedItems('#noresult')[0];
if (noResultPanel)
{
noResultPanel.hide();
}
},
/**
* Show the panel showing there is no result.
* @private
*/
_showNoResultPanel: function ()
{
var noResultPanel = this.getDockedItems('#noresult')[0];
if (noResultPanel)
{
noResultPanel.show();
}
},
/**
* 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 expanded 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);
}
},
/**
* 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 tree and select a node by its complete path
* @param {String} path The complete path from the root node
* @param {Boolean} autofocus Set to true to focus expanded node
*/
selectByPath: function (path, autofocus)
{
if (!Ext.String.startsWith(path, '/dummy'))
{
path = '/dummy' + path;
}
this.selectPath(path, 'name', '/', function(success, lastNode) {
if (success && autofocus)
{
this.getView().focusNode(lastNode);
}
}, this);
},
/**
* Get the path of the resource.
* @param {Ext.data.Model} node The node of the resource
* @return {String} The path
*/
getResourcePath: function (node)
{
var path = node.getPath('name')
if (path.indexOf('/dummy') == 0)
{
path = path.substr('/dummy'.length);
}
return path;
},
/**
* This function reload the given node
* @param {String} id The id 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 (id, callback)
{
callback = Ext.isFunction(callback) ? callback : Ext.EmptyFn;
var node;
if (id == null)
{
var selection = this.getSelectionModel().getSelection();
node = selection.length > 0 ? selection[0] : null;
}
else
{
node = this.getStore().getNodeById(id);
}
if (node != null && node.get('type') != Ametys.message.MessageTarget.RESOURCE)
{
// Set leaf to false, to allow children to be added during the load. Leaf will be set to true again if needed after the load.
node.set('leaf', false);
var me = this;
this.getStore().load({
node: node,
callback: function () {
if (me._filterValue && me._filterValue.length > 2)
{
// Filter child nodes only
me._getFilteredResources (me._filterValue, [node], true, me._getRootNode(node));
}
else
{
Ext.defer(me._expandNode, 200, me, [node, callback]);
}
},
scope: this
});
}
},
/**
* Get the root node of a given node
* @private
* @param {Ext.data.TreeModel} node The node to start search from
* @return {Ext.data.TreeModel} The root node
*/
_getRootNode: function(node)
{
for (var i=0; i < this._rootIds.length; i++)
{
var rootNode = this.getStore().getNodeById(this._rootIds[i]);
if (Ext.String.startsWith(node.getPath('name'), rootNode.getPath('name')))
{
return rootNode;
}
}
return null;
},
// ---------------------- Listeners ---------------------------------//
/**
* @private
* 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.
*/
getDragInfo: function(item)
{
var targets = [];
for (var i = 0; i < item.records.length; i++)
{
var cfg = this.getMessageTargetConfiguration(item.records[i]);
if (cfg != null)
{
targets.push(cfg);
}
}
if (targets.length > 0)
{
item.source = {
relationTypes: [Ametys.relation.Relation.MOVE, Ametys.relation.Relation.COPY, Ametys.relation.Relation.REFERENCE],
targets: targets
};
}
},
/**
* @private
* 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.
*/
getDropInfo: function(targetRecords, item)
{
var targets = [];
for (var i = 0; i < targetRecords.length; i++)
{
var cfg = this.getMessageTargetConfiguration(targetRecords[i]);
if (cfg != null)
{
targets.push(cfg);
}
}
if (targets.length > 0)
{
item.target = {
relationTypes: [Ametys.relation.Relation.MOVE],
targets: targets
};
}
},
/**
* @template
* Called by #getDragInfo, #getDragInfo or by tools to send the current selection
* @param {Ext.data.Model} record The tree record to convert to its Ametys.message.MessageTarget configuration
* @return {Object} The configuration to create a Ametys.message.MessageTarget. Can be null, if the record is null or not relevant to be a messagetarget.
*/
getMessageTargetConfiguration: function(record)
{
if (record == null)
{
// Empty selection
return null;
}
else if (record.get('type') == Ametys.explorer.tree.ExplorerTree.COLLECTION_TYPE)
{
return {
id: Ametys.message.MessageTarget.EXPLORER_COLLECTION,
parameters: { ids: [record.getId()]}
};
}
else if (record.get('type') == Ametys.explorer.tree.ExplorerTree.RESOURCE_TYPE)
{
return {
id: Ametys.message.MessageTarget.RESOURCE,
parameters: { ids: [record.getId()] }
};
}
else
{
return null;
}
},
/**
* 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];
if (node.get('isModifiable') !== true)
{
return false;
}
// Check user rights
if (!this.hasRight(this.getRightIdOnRename(node)))
{
return false;
}
var canRename = false;
if (node.get('type') == Ametys.explorer.tree.ExplorerTree.RESOURCE_TYPE)
{
canRename = Ametys.explorer.ExplorerNodeDAO.canRenameFile(node.get('path'));
}
else
{
canRename = Ametys.explorer.ExplorerNodeDAO.canRenameNode(node);
}
if (canRename)
{
this._currentEditNode = node;
}
return canRename;
},
/**
* Fires 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 {@link Ext.grid.column.Column} 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.
* @param {Mixed} context.originalValue The original value before 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;
if (this._currentEditNode.get('type') == Ametys.explorer.tree.ExplorerTree.RESOURCE_TYPE)
{
Ametys.explorer.resources.actions.File.rename(
this._currentEditNode.getId(),
context.originalValue,
context.value,
Ext.bind(this._renameCb, this)
);
}
else
{
var renameFnHolder = Ametys.explorer.ExplorerNodeDAO.getRenameActionHolder();
if (renameFnHolder && renameFnHolder.rename)
{
renameFnHolder.rename.apply(renameFnHolder, [
this._currentEditNode.getId(),
context.value,
this._renameCb,
this
]);
}
}
},
/**
* @private
* Callback function called after renaming
* @param {String} id The id of renamed file
* @param {String} name The name of the file
* @param {Boolean} success True if the rename action succeed
*/
_renameCb: function(id, name, success)
{
var node = this._currentEditNode;
if (!success)
{
node.beginEdit();
node.set('text', this._valueBeforeEdit);
node.set('name', this._valueBeforeEdit);
node.endEdit();
node.commit();
}
else
{
node.beginEdit();
node.set('text', name);
node.set('name', name);
node.endEdit();
node.commit();
// We should do this
// this.getStore().sort();
// This is a workaround but it has undesired side effects :
// When you rename a file and then delete it, without having changed the selection, the deletion does not actually happen
// see ISSUE CMS-6916
// var sorters = this.getStore().getSorters().getRange();
// this.getStore().getSorters().clear();
// this.getStore().sort(sorters);
// second workaround
node.parentNode.sort();
}
this._currentEditNode = null;
this._valueBeforeEdit = null;
}
});