/*
* Copyright 2016 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 content type structure
* @private
*/
Ext.define('Ametys.plugins.cms.contenttype.ContentTypeTree', {
extend: 'Ext.tree.Panel',
statics:
{
/**
* @property {Ext.XTemplate} _cTypeTooltipTpl The template used for content types' tooltip
*/
CONTENT_TYPE_TOOLTIP_TPL: Ext.create ('Ext.XTemplate', [
'{description}<br/>',
'<div class="x-clear">',
'</div>',
'<tpl if="isPrivate == true">',
"<span class='tooltip-ctype-type private ametysicon-lock81'>{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_PRIVATE}}</span><br/>",
'</tpl>',
'<tpl if="isReferenceTable == true">',
"<span class='tooltip-ctype-type reference-table ametysicon-list6 editor-tables1'>{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_REFERENCE_TABLE}}</span><br/>",
'</tpl>',
'<tpl if="isAbstract == true">',
"<span class='tooltip-ctype-type abstract ametysicon-text'>{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_ABSTRACT}}</span><br/>",
'</tpl>',
'<tpl if="isMixin == true">',
"<span class='tooltip-ctype-type mixin ametysicon-info28'>{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_MIXIN}}</span><br/>",
'</tpl>',
'<tpl if="isSimple == true">',
"<span class='tooltip-ctype-type simple ametysicon-document112'>{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_SIMPLE}}</span><br/>",
'</tpl>',
'<tpl if="isMultilingual == true">',
"<span class='tooltip-ctype-type multilingual ametysicon-translation'>{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_MULTILINGUAL}}</span><br/>",
'</tpl>',
"<span class='tooltip-ctype-type {glyphOrigin}'>{origin}</span>",
'</div>'
])
},
scrollable: true,
animate: true,
cls : 'contenttype-hierarchie-tree',
/**
* @cfg {String} [plugin="cms"] The plugin for the store.
*/
plugin: "cms",
/** @cfg {String} rootLabel The root node label. Only if #cfg-rootVisible */
/**
* @cfg {Boolean/String} [excludePrivate=false] True to exclude private content types.
*/
/**
* @cfg {Boolean/String} [excludeReferenceTable=true] True to exclude reference tables.
*/
/**
* @cfg {Boolean/String} [includeReferenceTableOnly=false] True to only include reference table content types.
*/
/**
* @cfg {Boolean/String} [excludeAbstract=false] True to exclude abstract content types.
*/
/**
* @cfg {Boolean/String} [excludeMixin=false] True to exclude mixin content types. By default excludeMixin is false.
*/
/**
* @cfg {Boolean/String} [includeMixinOnly=false] True to only include mixin content types. By default includeMixinOnly content type is false.
*/
/**
* @cfg {Boolean/String} [separateGlobal=false] True to separate global and local type under two different roots.
*/
/** @cfg {String[]} strictContentTypes The strict content types ids */
/** @cfg {String[]} contentTypes The parent content types ids */
initComponent: function ()
{
Ext.apply(this, {
folderSort: false,
root: {
text: this.rootLabel,
iconGlyph: "ametysicon-organization1",
expanded: true,
loaded: true
},
store: this.createTreeStore(),
dockedItems: [
this._getFilterToolbarConfig(),
{
dock: 'top',
xtype: 'button',
ui: 'tool-hintmessage',
hidden: true,
itemId: 'no-result',
text: "{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_FILTER_NO_MATCH}}" + "{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_FILTER_NO_MATCH_ACTION}}",
scope: this,
handler: this._clearSearchFilter
}
]
});
this.on('itemmouseenter', this._createQtip, this);
this.on('beforeselect', this._disabledNode, this);
this.on('beforeitemmouseenter', this._disabledNode, this);
this.on('load', this._onLoad, this);
this.on('checkchange', this._onCheckChange, this);
this.callParent();
},
constructor: function(config)
{
config.rootVisible = config.rootVisible || false;
config.rootLabel = config.rootLabel || "{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_ROOT}}";
this.rootVisible = config.rootVisible;
this.excludeReferenceTable = config.excludeReferenceTable === undefined ? false : config.excludeReferenceTable;
this.includeReferenceTableOnly = config.includeReferenceTableOnly === undefined ? false : config.includeReferenceTableOnly;
this.excludeAbstract = config.excludeAbstract === undefined ? false : config.excludeAbstract;
this.excludePrivate = config.excludePrivate === undefined ? false : config.excludePrivate;
this.excludeMixin = config.excludeMixin === undefined ? false : config.excludeMixin;
this.includeMixinOnly = config.includeMixinOnly === undefined ? false : config.includeMixinOnly;
this.excludeMode = config.excludeMode || 'disabled';
this.hierarchicalView = config.hierarchicalView === undefined ? true : config.hierarchicalView;
this.separateGlobal = config.separateGlobal === undefined ? false : config.separateGlobal;
this.strictContentTypes = config.strictContentTypes;
this.contentTypes = config.contentTypes;
this.checkMode = config.checkMode === undefined ? false : config.checkMode;
this.values = config.values;
this.callParent(arguments);
},
/**
* @protected
* Create the tree store
* @param {Object} config The tree panel configuration
* @return {Ext.data.TreeStore} The created tree store
*/
createTreeStore: function (config)
{
var sorters = [];
if (this.separateGlobal)
{
sorters.push({
sorterFn: function(n1, n2) {
// sort of hack to be sure that root nodes are always
// sorted the same way, whatever the language
if (n1.getDepth() == 0 && n2.getDepth() == 0)
{
var global1 = n1.get('global');
var global2 = n2.get('global');
return global1 == global2 ? 0 : global1 > global2 ? 1 : -1;
}
return 0;
}
})
}
sorters.push({
property: 'text',
direction: 'ASC'
});
var store = Ext.create('Ext.data.TreeStore', {
model: 'Ametys.plugins.cms.contenttype.ContentTypeTree.ContentTypeEntry',
autoLoad: false,
proxy: {
type: 'ametys',
plugin: this.plugin,
role: 'org.ametys.cms.contenttype.ContentTypesTreeComponent',
methodName: 'getContentTypes',
reader: {
type: 'json'
}
},
listeners: {
'beforeload': this._onBeforeLoad,
scope: this
},
sorters: sorters
});
return store;
},
/**
* Function called before loading the store
* @param {Ext.data.Store} store The store
* @param {Ext.data.operation.Operation} operation The object that will be passed to the Proxy to load the store
* @private
*/
_onBeforeLoad: function(store, operation)
{
operation.setParams( Ext.apply(operation.getParams() || {}, {
includeMixinOnly: this.includeMixinOnly,
hierarchicalView: this.hierarchicalView,
excludeReferenceTable: this.excludeReferenceTable,
includeReferenceTableOnly: this.includeReferenceTableOnly,
excludeMixin: this.excludeMixin,
excludeAbstract: this.excludeAbstract,
excludePrivate: this.excludePrivate,
excludeMode: this.excludeMode,
separateGlobal: this.separateGlobal,
strictContentTypes: this.strictContentTypes,
contentTypes: this.contentTypes
}));
},
/**
* @private
* Function after tree loading
* @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 The operation that triggered this load.
* @param {Ext.data.NodeInterface} node The loaded node
*/
_onLoad: function(store, records, successful, operation, node)
{
if (this.checkMode)
{
this._setCheckboxes(node);
}
},
/**
* Iterate through the children nodes, and call #_setCheckbox
* @param {Ext.data.Model} node The node to iterate
* @private
*/
_setCheckboxes: function(node)
{
var childNodes = node.childNodes;
for (var i=0; i < childNodes.length; i++)
{
this._setCheckbox(childNodes[i]);
this._setCheckboxes(childNodes[i]);
}
},
/**
* Put a checkbox on nodes that can be checked, with the corresponding state. Does not verify any rights.
* @param {Ext.data.Model} node The node
* @private
*/
_setCheckbox: function(node)
{
if (!node.get('disabled'))
{
node.set('checked', Ext.Array.contains(this.values, node.get('contentTypeId')));
}
else
{
node.set('checked', false);
}
},
/**
* Verify if a node can be checked. If it can, his value is updated
* @param {Ext.data.Model} node The node which state was updated
* @param {Boolean} checked The new node state
* @private
*/
_onCheckChange: function(node, checked)
{
if (node.get('disabled'))
{
// cancel check
node.set('checked', false);
}
else if (checked === true && Ext.Array.contains(this.values, node.get('contentTypeId')) === false)
{
this.values.push(node.get('contentTypeId'));
}
else if (checked === false && Ext.Array.contains(this.values, node.get('contentTypeId')) === true)
{
Ext.Array.remove(this.values, node.get('contentTypeId'));
}
},
/**
* Load the root node
*/
onRender: function(ct, position)
{
this.callParent(arguments);
this.getRootNode().expand();
},
/**
* @private
* 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)
{
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)
{
if (node.get('label'))
{
var text = Ametys.plugins.cms.contenttype.ContentTypeTree.CONTENT_TYPE_TOOLTIP_TPL.applyTemplate ({
description: node.get('description'),
'isPrivate': node.get('private'),
'isReferenceTable': node.get('reference-table'),
'isAbstract': node.get('abstract'),
'isMixin': node.get('mixin'),
'isSimple': node.get('simple'),
'isMultilingual': node.get('multilingual'),
'origin': node.get('origin'),
'glyphOrigin': node.get('glyphOrigin')
});
return {
title: node.get('label'),
image: Ext.isEmpty(node.get('iconGlyph')) ? Ametys.CONTEXT_PATH + (node.get('iconLarge') != '' ? node.get('iconLarge') : node.get('iconMedium')) : null,
glyphIcon: Ext.isEmpty(node.get('iconGlyph')) ? null : node.get('iconGlyph') + (!Ext.isEmpty(node.get('iconDecorator')) ? ' ' + node.get('iconDecorator') : ''),
imageWidth: 48,
imageHeight: 48,
text: text,
inribbon: false
};
}
else
{
return {
title: this.rootLabel,
glyphIcon: "ametysicon-organization1",
imageWidth: 48,
imageHeight: 48,
inribbon: false
};
}
},
/**
* @private
* Get the filter toolbar config
* @return {Object} The filter toolbar config
*/
_getFilterToolbarConfig: function()
{
return {
dock: 'top',
xtype: 'toolbar',
layout: {
type: 'hbox',
align: 'stretch'
},
border: false,
defaultType: 'button',
items: [{
// Filter input
xtype: 'textfield',
cls: 'ametys',
flex: 1,
maxWidth: 400,
itemId: 'plugins-filter-input',
emptyText: "{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_FILTER}}",
minLength: 3,
minLengthText: "{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_FILTER_INVALID}}",
msgTarget: 'qtip',
listeners: {change: Ext.Function.createBuffered(this._filter, 500, this)}
},
{
// Clear filter
tooltip: "{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_CLEAR_FILTER}}",
handler: Ext.bind (this._clearSearchFilter, this),
iconCls: "ametysicon-eraser11"
},
{
tooltip: "{{i18n PLUGINS_CMS_CONTENTTYPE_TREE_FILTER_BY_CHECK}}",
enableToggle: true,
toggleHandler: Ext.bind(this._displayCheckedNodesOnly, this),
hidden: !this.checkMode,
iconCls: 'a-btn-glyph ametysicon-check51 size-16',
cls: 'a-btn-light'
}
]
};
},
/**
* @private
* Filters the tree nodes by entered text.
* @param {Ext.form.field.Text} field This text field
*/
_filter: function(field)
{
this._filterField = field;
this.getStore().clearFilter();
var val = Ext.String.escapeRegex(field.getRawValue());
if (val.length > 2)
{
this._regexFilter = new RegExp(val, 'i');
if (this._checkFilter)
{
this.getStore().filter({
filterFn: Ext.bind(this._filterByTextAndChildrenCheckedNodes, this)
});
}
else
{
this.getStore().filter({
filterFn: Ext.bind(this._filterByTextAndChildren, this)
});
}
}
else
{
this._regexFilter = null;
if (this._checkFilter)
{
this.getStore().filter({
filterFn: Ext.bind(this._filterByTextAndChildrenCheckedNodes, this)
});
}
}
this.getDockedItems()[1].setVisible(!this.getStore().getCount()); // Display docked items if no node matched the search term
},
/**
* @private
* Filter function that check if a node in the tree should be visible or not.
* @param {Ext.data.Model} record The record to check.
* @return {boolean} True if the record should be visible.
*/
_filterByTextAndChildren: function (record)
{
var minDepth = this.rootVisible ? 1 : 0;
minDepth += this.separateGlobal ? 1 : 0;
var isVisible = (this._regexFilter == null || this._regexFilter.test(record.data.text))
&& record.getDepth() > minDepth;
if (!isVisible)
{
// if the record does not match, we check if any child is visible. If at least one is, this record should not be hidden.
// This is efficient because the data is in the store, and is not loaded in the view.
for (var i = 0; !isVisible && i < record.childNodes.length; i++) {
isVisible = this._filterByTextAndChildren(record.childNodes[i]);
}
}
if (isVisible)
{
this.expandNode(record);
}
return isVisible;
},
/**
* Triggered by the button "filter by checked only".
* @param {Ext.button.Button} button The button which triggered this method. Parameter not used.
* @param {Boolean} state If true, filter by "checked only"
* @private
*/
_displayCheckedNodesOnly: function(button, state)
{
this._checkFilter = state;
var filterValue = "";
if (this._filterField !== undefined)
{
filterValue = new String(this._filterField.getValue()).trim();
}
if (filterValue && filterValue.length < 3)
{
filterValue = "";
}
if (filterValue === "" && state === false)
{
this._clearSearchFilter();
}
else if (state === true)
{
this.getStore().filter({
filterFn: Ext.bind(this._filterByTextAndChildrenCheckedNodes, this)
});
}
else if (filterValue != "")
{
this._filter(this._filterField);
}
},
/**
* @private
* Filter function that check if a checked node in the tree should be visible or not.
* @param {Ext.data.Model} record The record to check.
* @return {boolean} True if the record should be visible.
*/
_filterByTextAndChildrenCheckedNodes: function (record)
{
var minDepth = this.rootVisible ? 1 : 0;
minDepth += this.separateGlobal ? 1 : 0;
var isVisible = (this._regexFilter == null || this._regexFilter.test(record.data.text)) && record.data.checked
&& record.getDepth() > minDepth;
if (!isVisible)
{
// if the record does not match, we check if any child is visible. If at least one is, this record should not be hidden.
// This is efficient because the data is in the store, and is not loaded in the view.
for (var i = 0; !isVisible && i < record.childNodes.length; i++) {
isVisible = this._filterByTextAndChildrenCheckedNodes(record.childNodes[i]);
}
}
if (isVisible)
{
this.expandNode(record);
}
return isVisible;
},
/**
* @private
* Handler for the clear search
*/
_clearSearchFilter: function ()
{
if (this._filterField)
{
this._filterField.reset();
}
this._regexFilter = null;
this.getStore().clearFilter();
this.getDockedItems()[1].setVisible(false);
var selection = this.getSelectionModel().getSelection()[0];
if (selection)
{
this.ensureVisible(selection.getPath());
}
},
/**
* @private
* Disabled node
* @param {Ext.selection.RowModel} rowModel The selection model
* @param {Ext.data.Model} record The selected record
* @return {Boolean}
*/
_disabledNode: function(rowModel, record)
{
if (record.get('disabled'))
{
return false;
}
return true;
}
});
Ext.define('Ametys.plugins.cms.contenttype.ContentTypeTree.ContentTypeEntry', {
extend: 'Ext.data.Model',
fields: [
{name: 'private', type:'boolean'},
{name: 'abstract', type:'boolean'},
{name: 'reference-table', type:'boolean'},
{name: 'mixin', type:'boolean'},
{name: 'simple', type:'boolean'},
{name: 'global', type:'boolean'},
{name: 'multilingual', type:'boolean'},
{name: 'disabled', type:'boolean', defaultValue: false},
{
name: 'text',
sortType: function(value) {
return Ext.data.SortTypes.asNonAccentedUCString(value || "").toLowerCase();
}
},
'label',
'description',
'contentTypeId',
'iconSmall',
'iconMedium',
'iconLarge',
'iconGlyph',
'iconDecorator',
'origin',
'glyphOrigin',
{
name: 'icon',
depends: ['iconGlyph', 'iconSmall'],
calculate: function (data)
{
if (Ext.isEmpty(data.iconGlyph))
{
return Ametys.CONTEXT_PATH + data.iconSmall;
}
return null;
}
},
{
name: 'iconCls',
depends: ['iconGlyph', 'iconDecorator'],
calculate: function (data)
{
if (!Ext.isEmpty(data.iconGlyph))
{
return 'a-tree-glyph ' + data.iconGlyph + (!Ext.isEmpty(data.iconDecorator) ? ' ' + data.iconDecorator : '');
}
else
{
return null;
}
}
},
{
name: 'cls',
convert: function (value, node)
{
return (node.get('abstract') ? ' abstract' : '') + (node.get('reference-table') ? ' reference-table' : '') + (node.get('private') ? ' private' : '') + (node.get('disabled') ? ' disabled' : '');
}
}
]
});