/*
 *  Copyright 2018 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.
 */
(function ()
{
    Ext.override(Ext, {
        /**
         * @member Ext
         * @method moveTo 
         * @since Ametys-Runtime-4.0
         * @ametys
         * The same as #copyTo but properties are also removed from source object.
         * @param {Object} dest The destination object.
         * @param {Object} source The source object.
         * @param {String/String[]} names Either an Array of property names, or a comma-delimited list
         * of property names to copy.
         * @param {Boolean} [usePrototypeKeys=false] Pass `true` to copy keys off of the
         * prototype as well as the instance.
         * @return {Object} The `dest` object. 
         */
        moveTo: function(dest, source, names, usePrototypeKeys) {
            var result = this.copyTo(dest, source, names, usePrototypeKeys);
            
            Ext.Array.forEach(Ext.Array.from(names), function(item, index, allItems) {
                delete source[item];
            }, this);
            
            return result;
        }
    });
})();
    
(function ()
{
    Ext.override(Ext.String, {
        /**
         * @member Ext.String
         * @method deemphasize 
         * @since Ametys-Runtime-3.9
         * @ametys
         * Convert a string value into a non accentued string
         * @param {Object} s The value being converted
         * @return {String} The deemphasize value
         */
        deemphasize: function (s)
        {
            if (!s) return s;
            
            s = s.replace(new RegExp(/[ÀÁÂÃÄÅ]/g),"A");
            s = s.replace(new RegExp(/[àáâãäå]/g),"a");
            s = s.replace(new RegExp(/Æ/g),"AE");
            s = s.replace(new RegExp(/æ/g),"ae");
            s = s.replace(new RegExp(/Ç/g),"C");
            s = s.replace(new RegExp(/ç/g),"c");
            s = s.replace(new RegExp(/[ÈÉÊË]/g),"E");
            s = s.replace(new RegExp(/[èéêë]/g),"e");
            s = s.replace(new RegExp(/[ÌÍÎÏ]/g),"I");
            s = s.replace(new RegExp(/[ìíîï]/g),"i");
            s = s.replace(new RegExp(/Ñ/g),"N");
            s = s.replace(new RegExp(/ñ/g),"n");
            s = s.replace(new RegExp(/[ÒÓÔÕÖ]/g),"O");
            s = s.replace(new RegExp(/[òóôõö]/g),"o");
            s = s.replace(new RegExp(/Œ/g),"OE");
            s = s.replace(new RegExp(/œ/g),"oe");
            s = s.replace(new RegExp(/[ÙÚÛÜ]/g),"U");
            s = s.replace(new RegExp(/[ùúûü]/g),"u");
            s = s.replace(new RegExp(/[ÝŸ]/g),"y");
            s = s.replace(new RegExp(/[ýÿ]/g),"y");

            return s;
        },
        
        /**
         * @member Ext.String
         * @method enhancedCompare 
         * @since Ametys-Runtime-4.9
         * @ametys
         * Compares two strings. Comparison is made ignoring case and accents and honouring natural numbers ordering.
         * @param {String} s1 The first string to be compared
         * @param {String} s2 The second string to be compared
         * @return {Number} a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second.
         */
        enhancedCompare: function(s1, s2)
        {
            const str1 = Ext.String.deemphasize(s1.toLowerCase());
            const str2 = Ext.String.deemphasize(s2.toLowerCase());
            
            function isDigit(c) 
            {
                return c >= '0' && c <= '9';
            }
            
            let i = 0, j = 0;
            while (i < str1.length && j < str2.length) 
            {
                let char1 = str1.charAt(i);
                let char2 = str2.charAt(j);

                // If both characters are digits, extract the full number
                if (isDigit(char1) && isDigit(char2)) 
                {
                    let start1 = i, start2 = j;

                    while (i < str1.length && isDigit(str1.charAt(i))) 
                    {
                        i++;
                    }
                    
                    while (j < str2.length && isDigit(str2.charAt(j))) 
                    {
                        j++;
                    }

                    let num1 = parseInt(str1.substring(start1, i), 10);
                    let num2 = parseInt(str2.substring(start2, j), 10);

                    if (num1 !== num2) 
                    {
                        return num1 - num2;
                    }
                } 
                else
                {
                    // Compare characters lexicographically
                    if (char1 !== char2) 
                    {
                        return char1.localeCompare(char2);
                    }
                    
                    i++;
                    j++;
                }
            }

            // If one string has remaining characters, it is considered "greater"
            if (i < str1.length) 
            {
                return 1;
            } 
            else if (j < str2.length)
            {
                return -1;
            }

            // Strings are equal
            return 0;
        }
    });
})();
(function ()
{
    // Override SortType to add support for accented characters
    Ext.override(Ext.data.SortTypes, {
        /**
         * @member Ext.data.SortTypes
         * @method asNonAccentedUCString 
         * @since Ametys-Runtime-3.9
         * @ametys
         * Case insensitive string (which takes accents into account)
         * @param {Object} s The value being converted
         * @return {String} The comparison value
         */
        asNonAccentedUCString: function (s)
        {
            if (!s)
            {
                return s;
            }
            
            s = Ext.String.deemphasize(String(s).toLowerCase());
            
            return Ext.data.SortTypes.asUCString(s);
        }
    });
    
    Ext.override(Ext.data.field.String, { sortType: 'asNonAccentedUCString' });
})();

(function() {
    // Override component to save flex value
    Ext.override(Ext.Component, {
        getState: function()
        {
            var state = this.callParent(arguments);
            
            state = this.addPropertyToState(state, 'flex');
            
            return state;
        }
    })
})();

/*
 * Supports for ametysDescription on fields and fields containers.
 */
/**
 * @member Ext.form.field.Base
 * @ametys
 * @since Ametys-Runtime-3.9
 * @cfg {String} ametysDescription A help image is added with the given description as a tooltip
 */
/**
 * @member Ext.form.field.Base
 * @ametys
 * @since Ametys-Runtime-3.9
 * @cfg {String} ametysDescriptionUrl An action url for the description tooltip
 */
/**
 * @member Ext.form.field.Base
 * @ametys
 * @since Ametys-Runtime-3.9
 * @cfg {Number/String} ametysDescriptionWidth The tooltip width. The default value is 210. You can set to "auto" to autosize.
 */
/**
 * @member Ext.form.FieldContainer
 * @ametys
 * @since Ametys-Runtime-3.9
 * @cfg {String} ametysDescription A help image is added with the given description as a tooltip
 */
/**
 * @member Ext.form.FieldContainer
 * @ametys
 * @since Ametys-Runtime-3.9
 * @cfg {String} ametysDescriptionUrl An action url for the description tooltip
 */
/**
 * @member Ext.form.FieldContainer
 * @ametys
 * @since Ametys-Runtime-3.9
 * @cfg {Number/String} ametysDescriptionWidth The tooltip width. The default value is 210. You can set to "auto" to autosize.
 */
(function ()
{
    /*
     * Support of warning message and ametys description on fields
     */
    
    
    var renderFn = {
        /**
         * @member Ext.form.Labelable
         * @ametys
         * @since Ametys-Runtime-3.9
         * 
         * Get the readable value of a Field. The default implementation returns the same as Ext.form.field.Field#getValue.
         * Override this method to return an understandable value for more complex field (such as combobox, file input, ...). 
         * @template
         */
        getReadableValue: function ()
        {
            return this.getValue();
        }
    };
    
    Ext.define("Ametys.form.field.Base", Ext.apply(Ext.clone(renderFn), { override: 'Ext.form.field.Base'}));
    Ext.define("Ametys.form.FieldContainer", Ext.apply(Ext.clone(renderFn), { override: 'Ext.form.FieldContainer'}));
    
    
    var ametysLabelable =  {
        beforeLabelTpl: Ext.create("Ext.XTemplate", '<tpl if="topLabel &amp;&amp; fieldLabel"><div class="x-form-item-label-wrapper x-form-item-label-top-wrapper"></tpl>'),
        afterLabelTpl: Ext.create("Ext.XTemplate", ['<tpl if="topLabel &amp;&amp; fieldLabel">',
                        '<tpl if="ametysDescription">',
                            '<div id="{id}-descWrapEl" data-ref="descWrapEl" class="ametys-description"><div></div></div>',
                        '</tpl>',
                        '<tpl if="renderWarning">',
                            '<div id="{id}-warningWrapEl" data-ref="warningWrapEl" class="ametys-warning" style="display: none"><div></div></div>',
                        '</tpl>',
                        '<tpl if="renderError">',
                            '<div id="{id}-errorWrapEl" data-ref="errorWrapEl" class="{errorWrapCls} {errorWrapCls}-{ui}',
                                ' {errorWrapExtraCls}" style="{errorWrapStyle}">',
                                '<div role="presentation" id="{id}-errorEl" data-ref="errorEl" ',
                                    'class="{errorMsgCls} {invalidMsgCls} {invalidMsgCls}-{ui}" ',
                                    'data-anchorTarget="{tipAnchorTarget}">',
                                '</div>',
                            '</div>',
                        '</tpl>',                        
            '</div></tpl>'
        ]),
        
        afterOutterBodyEl: [  
                        '<tpl if="!(topLabel &amp;&amp; fieldLabel)">',
                            '<tpl if="renderWarning">',
                                '<div id="{id}-warningWrapEl" data-ref="warningWrapEl" class="ametys-warning" style="display: none"><div></div></div>',
                            '</tpl>',
                            '<tpl if="ametysDescription">',
                                '<div id="{id}-descWrapEl" data-ref="descWrapEl" class="ametys-description""><div id="{id}-descWrapSubEl"></div></div>',
                            '</tpl>',
                        '</tpl>'
        ],    
        
        initConfig : function(config)
        {
            config = config || {};
            config.childEls = config.childEls || [];
            config.childEls.push("warningWrapEl");
            config.childEls.push("descWrapEl");
            
            this.callParent(arguments); 
            
            this.on("afterrender", this._renderTooltip, this);
            this.on("destroy", this._destroyTooltip, this);
        },
        
        /**
         * @private
         * @member Ext.form.Labelable
         * @ametys
         * @since Ametys-Runtime-3.9
         * On render listener
         */
        _onRenderLabelable: function()
        {
            var sideErrorCell = Ext.get(this.getId() + "-sideErrorCell");
            if (sideErrorCell)
            {
                sideErrorCell.addCls(this.errorMsgCls + "-wrapper")
            }
        },
        
        getInsertionRenderData: function(data, names)
        {
            // Replace double quotes characters (") by its HTML code
            data.ametysDescription = this.ametysDescription && this.ametysDescription.replace(/"/g, "&#034;");
            data.ametysDescriptionUrl = this.ametysDescriptionUrl && this.ametysDescriptionUrl.replace(/"/g, "&#034;");
            data.renderWarning = true;
            data.topLabel = (this.labelAlign === 'top');
            data.ui = this.ui;
            
            return this.callParent(arguments);
        },
        
        initLabelable: function ()
        {
            this.callParent(arguments);
            
            this.on("render", this._onRenderLabelable, this);
        },
        
        /**
         * @member Ext.form.Labelable
         * @ametys
         * @since Ametys-Runtime-3.9
         * 
         * @event warningchange
         * Fires when the active warning message is changed via {@link #setActiveWarning}.
         * @param {Ext.form.Labelable} this
         * @param {String} warning The active warning message
         */
        
        /**
         * @member Ext.form.Labelable
         * @ametys
         * @since Ametys-Runtime-3.9
         * 
         * @private
         * Render a tooltip on this element
         */
        _renderTooltip: function()
        {
            var me = this;
            if (me.ametysDescription && me.descWrapEl)
            {
                Ext.tip.QuickTipManager.register({text: me.ametysDescription, help: me.ametysDescriptionUrl, width: me.ametysDescriptionWidth, target: me.descWrapEl.child('div').id, inribbon: false, fluent: true});
            }
        },
        
        /**
         * @member Ext.form.Labelable
         * @ametys
         * @since Ametys-Runtime-3.9
         * 
         * @private
         * Destroy the registered tooltip
         */
        _destroyTooltip: function()
        {
            var me = this;
            if (me.ametysDescription && me.descWrapEl)
            {
                Ext.tip.QuickTipManager.unregister(me.descWrapEl.child('div').id);
            }
        },
        
        /**
         * @member Ext.form.Labelable
         * @ametys
         * @since Ametys-Runtime-3.9
         * 
         * @cfg {String} warningCls
         * The CSS class to use when marking the component has warning.
         */
        warningCls : Ext.baseCSSPrefix + 'form-warning',
        
        /**
         * @member Ext.form.Labelable
         * @ametys
         * @since Ametys-Runtime-3.9
         * 
         * @cfg {String/String[]/Ext.XTemplate} activeWarnsTpl
         * The template used to format the Array of warnings messages passed to {@link #setActiveWarnings} into a single HTML
         * string. It renders each message as an item in an unordered list.
         */
        activeWarnsTpl: [
              '<tpl if="warns && warns.length">',
                  '<ul class="{listCls}">',
                      '<tpl for="warns">',
                          '<li role="warn">',
                              '<tpl if="message">',
                                  '{message}',
                              '<tpl else>',
                                  '{.}',
                               '</tpl>',
                          '</li>',
                      '</tpl>',
                  '</ul>',
              '</tpl>'
        ],
        
        /**
         * @member Ext.form.Labelable
         * @ametys
         * @since Ametys-Runtime-3.9
         * 
         * Gets an Array of any active warning currently applied to the field. 
         * @return {String/Object} The active warning on the component; if there are no warning, null is returned.
         */
        getActiveWarning: function ()
        {
            return this.activeWarn;
        },
        
        /**
         * @member Ext.form.Labelable
         * @ametys
         * @since Ametys-Runtime-3.9
         * 
         * Gets an Array of any active warnings currently applied to the field. 
         * @return {String[]/Object[]} The active warnings on the component; if there are no warnings, an empty Array is
         * returned.
         */
        getActiveWarnings: function() {
            return this.activeWarns || [];
        },
        
        /**
         * @member Ext.form.Labelable
         * @ametys
         * @since Ametys-Runtime-3.9
         * 
         * Tells whether the field currently has an active warning message. 
         * @return {Boolean}
         */
        hasActiveWarning: function() {
            return !!this.getActiveWarning();
        },
        
        /**
         * @member Ext.form.Labelable
         * @ametys
         * @since Ametys-Runtime-4.8
         * 
         * Add warnings to the already active ones 
         */
        addActiveWarnings: function(warns)
        {
            let allWarns = this.activeWarns ? Ext.Array.clone(this.activeWarns) : [];
            Ext.Array.push(allWarns, warns);
            this.setActiveWarnings(allWarns);
        },
        
        /**
         * @member Ext.form.Labelable
         * @ametys
         * @since Ametys-Runtime-3.9
         * 
         * Sets the active warning message to the given string. 
         * This replaces the entire warning message contents with the given string. 
         * Also see {@link #setActiveWarnings} which accepts an Array of messages and formats them according to the
         * {@link #activeWarnsTpl}. 
         * @param {String/Object} msg The warning message
         */
        setActiveWarning: function (msg) 
        {
            this.setActiveWarnings(msg);
        },

        /**
         * @member Ext.form.Labelable
         * @ametys
         * @since Ametys-Runtime-3.9
         * 
         * Set the active warning message to an Array of warning messages. The messages are formatted into a single message
         * string using the {@link #activeWarnsTpl}. Also see {@link #setActiveWarning} which allows setting the entire warning
         * contents with a single string. 
         * @param {String[]/Object[]} warns The warning messages
         */
        setActiveWarnings: function (warns)
        {
            warns = Ext.Array.from(warns);
            this.activeWarns = warns;
            
            this.activeWarn = Ext.XTemplate.getTpl(this, 'activeWarnsTpl').apply({
                warns: warns,
                listCls: Ext.plainListCls 
            });
            
            this.renderActiveWarning();
        },
        
        /**
         * @member Ext.form.Labelable
         * @ametys
         * @since Ametys-Runtime-3.9
         * 
         * Clears the active warning message(s). Note that this only clears the warning message element's text and attributes,
         * you'll have to call doComponentLayout to actually update the field's layout to match. If the field extends {@link
         * Ext.form.field.Base} you should call {@link Ext.form.field.Base#clearInvalid clearInvalid} instead.
         */
        unsetActiveWarnings: function ()
        {
            delete this.activeWarn;
            delete this.activeWarns;
            this.renderActiveWarning();
        },
        
        /**
         * @member Ext.form.Labelable
         * @ametys
         * @since Ametys-Runtime-3.9
         * 
         * Updates the rendered DOM to match the current activeWarn. This only updates the content and
         * attributes, you'll have to call doComponentLayout to actually update the display.
         */
        renderActiveWarning: function() 
        {
            var me = this,
                activeWarning = me.getActiveWarning(),
                hasWarning = !!activeWarning;
    
            if (activeWarning !== me.lastActiveWarn) {
                me.lastActiveWarn = activeWarning;
                me.fireEvent('warningchange', me, activeWarning);
            }
    
            if (me.rendered && !me.destroyed && !me.preventMark) 
            {
                me.toggleWarningCls(hasWarning);
                
                if (this.warningWrapEl) 
                {
                	this.warningWrapEl.dom.setAttribute("data-warnqtip", activeWarning);
                	
                	var displayValue = hasWarning ? '' : 'none';
                	if (this.warningWrapEl.dom.style.display != displayValue)
                	{
                		this.warningWrapEl.dom.style.display = displayValue;
                		
                		me.updateLayout();
                	}
                    
                }
            }            
        },
        
        /**
         * @member Ext.form.Labelable
         * @ametys
         * @since Ametys-Runtime-4.0
         * @private
         * Add/remove invalid class(es)
         * @param {Boolean} hasWarning Has a warning
         */
        toggleWarningCls: function(hasWarning) 
        {
            this.el[hasWarning ? 'addCls' : 'removeCls'](this.warningCls);
        },        
        
        /**
         * @member Ext.form.Labelable
         * @ametys
         * @since Ametys-Runtime-3.9
         * 
         * Hide the active warning message(s). Note that this only hides the warning message(s). The active warning message(s) are not cleared. 
         * Then you could call #renderActiveWarning method to show the warning message(s).
         * If you want to delete the active warning message(s) you should call Ext.form.field.Field#clearInvalid instead.
         */
        hideActiveWarning: function ()
        {
            var me = this,
                activeWarning = me.getActiveWarning(),
                hasWarning = !!activeWarning;
            
            if (hasWarning && me.rendered && !me.isDestroyed && !me.preventMark) 
            {
                me.toggleWarningCls(false);

                if (this.warningWrapEl) 
                {
                    this.warningWrapEl.dom.style.display = 'none';
                }
            }
        }
    };

    Ext.Array.forEach(Ext.form.Labelable.prototype.labelableRenderTpl, function(value, index) {
        if (value == '<tpl if="renderError">')
        {
            Ext.form.Labelable.prototype.labelableRenderTpl[index] = '<tpl if="renderError && !(topLabel &amp;&amp; fieldLabel)">';
        }
    });
    Ext.Array.insert(Ext.form.Labelable.prototype.labelableRenderTpl, Ext.form.Labelable.prototype.labelableRenderTpl.length - 1, ametysLabelable.afterOutterBodyEl);
    
    Ext.override(Ext.form.Labelable, Ext.apply(Ext.clone(ametysLabelable), { 
        statics: {
            initTip: function() 
            {
                this.callParent(arguments);
                
                var tip = this.warnTip,
                    cfg, copy;
    
                if (tip) {
                    return;
                }
    
                cfg = {
                    id: 'ext-form-warn-tip',
                    //<debug>
                    // tell the spec runner to ignore this element when checking if the dom is clean
                    sticky: true,
                    //</debug>
                    ui: 'form-warning'
                };
    
                // On Touch devices, tapping the target shows the qtip
                if (Ext.supports.Touch) {
                    cfg.dismissDelay = 0;
                    cfg.anchor = 'top';
                    cfg.showDelay = 0;
                    cfg.listeners = {
                        beforeshow: function() {
                            this.minWidth = Ext.fly(this.anchorTarget).getWidth();
                        }
                    };
                }
                tip = this.warnTip = Ext.create('Ext.tip.QuickTip', cfg);
                copy = Ext.apply({}, tip.tagConfig);
                copy.attribute = 'warnqtip';
                tip.setTagConfig(copy);                
            },
            
            destroyTip: function() 
            {
                this.callParent(arguments);

                this.tip = Ext.destroy(this.tip);
            }
        }
    }));
    Ext.override(Ext.form.field.Base, Ext.clone(ametysLabelable));
    Ext.override(Ext.form.FieldContainer, Ext.clone(ametysLabelable));
    
    Ext.override(Ext.form.field.Text, {

        /** 
         * @member Ext.form.field.Text
         * @ametys
         * @since Ametys-Runtime-4.0
         * @private
         * @property {String} triggerWrapWarningCls The css classname to set on trigger wrapper if warning
         */
        triggerWrapWarningCls: Ext.baseCSSPrefix + 'form-trigger-wrap-warning',
        /** 
         * @member Ext.form.field.Text
         * @ametys
         * @since Ametys-Runtime-4.0
         * @private
         * @property {String} inputWrapWarningCls The css classname to set on input wrapper if warning
         */
        inputWrapWarningCls: Ext.baseCSSPrefix + 'form-text-wrap-warning',
        
        /**
         * @cfg {RegExp} warnRegex
         * A JavaScript RegExp object to be tested against the field value
         * If the test fails, the field will be marked warned using **{@link #warnRegexText}**
         */

        /**
         * @cfg {String} warnRegexText
         * The warning text to display if **{@link #warnRegex}** is used and the test fails on change
         */
        warnRegexText: '',
    
        toggleWarningCls: function(hasWarning) 
        {
            var method = hasWarning ? 'addCls' : 'removeCls';
    
            this.callParent();
    
            this.triggerWrap[method](this.triggerWrapWarningCls);
            this.inputWrap[method](this.inputWrapWarningCls);
        },
        
        onChange: function(newVal, oldVal) {
            var me = this;
                
            if (me.warnRegex && !me.warnRegex.test(newVal)) {
                var activeWarnings = me.getActiveWarnings();
                if (!Ext.Array.contains(activeWarnings, me.warnRegexText))
                {
                    Ext.Array.push(activeWarnings, me.warnRegexText);
                }
                me.markWarning(activeWarnings);
            }
            else if (me.hasActiveWarning())
            {
                var activeWarnings = me.getActiveWarnings();
                activeWarnings = Ext.Array.remove(activeWarnings, me.warnRegexText);
                if (activeWarnings.length > 0)
                {
                    me.setActiveWarnings(activeWarnings);
                }
                else
                {
                    me.clearWarning();
                }
            }

            me.callParent([newVal, oldVal]);
        }
    });
     
    Ext.define("Ametys.form.field.Field", {
        override: "Ext.form.field.Field",
                
        /**
         * @member Ext.form.field.Field
         * @ametys
         * @since Ametys-Runtime-3.9
         * 
         * Associate one or more warning messages with this field.
         * @param {String/String[]} warns The warning message(s) for the field.
         */
        markWarning: Ext.emptyFn,
        
        /**
         * @member Ext.form.field.Field
         * @ametys
         * @since Ametys-Runtime-3.9
         * 
         * Clear any warning styles/messages for this field.
         */
        clearWarning: Ext.emptyFn,
        
        /**
         * @member Ext.form.field.Field
         * @ametys
         * @since Ametys-Runtime-4.0
         * 
         * Setting this to true will allow the field from being submitted even when it is disabled.
         */
        submitDisabledValue: false,
        
        /**
         * @member Ext.form.field.Field
         * @ametys
         * @since Ametys-Runtime-3.9
         * 
         * Get the readable value of a Field. The default implementation returns the same as #getValue.
         * Override this method to return an understandable value for more complex field (such as combobox, file input, ...). 
         * @template
         */
        getReadableValue: function ()
        {
            return this.getValue();
        },
        
        /**
         * @member Ext.form.field.Field
         * @ametys
         * @template
         * @since Ametys-Runtime-4.0
         *  
         * Returns the current data value of the field as a JSON serializable value.
         * By default, returns the result of {@link #getValue}
         * 
         * @return {Object} value The field value as a JSON serializable value.
         */
    	getJsonValue: function()
    	{
    		return this.getValue();
    	}
    });
    
    var ametysFieldBase = {
    		
    	getJsonValue: function()
    	{
    		return this.getValue();
    	},
    		
        /**
         * @member Ext.form.field.Base
         * @ametys
         * @since Ametys-Runtime-3.9
         * 
         * Associate one or more warning messages with this field.
         * @param {String/String[]} warns The warning message(s) for the field.
         */
        markWarning: function (warns)
        {
            this.setActiveWarnings(Ext.Array.from(warns));
            
            if (this.hasActiveError())
            {
                // Hide active warning message(s)
                this.hideActiveWarning();
            }
            
            this.updateLayout();
        },
        
        /**
         * @member Ext.form.field.Base
         * @ametys
         * @since Ametys-Runtime-3.9
         * 
         * Clear any warning styles/messages for this field.
         */
        clearWarning: function() 
        {
            this.unsetActiveWarnings();
            this.updateLayout();
        },
        
        /**
         * @member Ext.form.field.Base
         * @ametys
         * @since Ametys-Runtime-3.9
         *  
         * Overrides the method from the Ext.form.Labelable mixin to also add the warningCls to the inputEl
         */
        renderActiveWarning: function() 
        {
            var me = this,
                hasWarning = me.hasActiveWarning(),
                warningCls = me.warningCls + '-field';
    
            if (me.inputEl) {
                // Add/remove invalid class
                me.inputEl[hasWarning ? 'addCls' : 'removeCls']([
                    warningCls, warningCls + '-' + me.ui
                ]);
            }
            me.mixins.labelable.renderActiveWarning.call(me);            
        },
        
        markInvalid: function ()
        {
            if (this.hasActiveWarning())
            {
                // Hide active warning message(s) if exist
                this.hideActiveWarning();
            }
            this.callParent(arguments);
        },
        
        clearInvalid: function ()
        {
            this.callParent(arguments);
            
            if (this.hasActiveWarning())
            {
                // Display active warning message(s) if exist
                this.renderActiveWarning();
            }
        }
    };
    
    Ext.define("Ametys.form.field.Base", Ext.apply(Ext.clone(ametysFieldBase), { 
        override: 'Ext.form.field.Base',
        
        ignoreChangeRe: /data\-errorqtip|data\-warnqtip|style\.|className/,
        
        getSubmitData: function() {
            var me = this,
                data = null,
                val;
            if ((!me.disabled || me.submitDisabledValue) && me.submitValue) {
                val = me.getSubmitValue();
                if (val !== null) {
                    data = {};
                    data[me.getName()] = val;
                }
            }
            return data;
        }
    }));
    
    Ext.define("Ametys.layout.component.field.FieldContainer", {
        override: 'Ext.layout.component.field.FieldContainer',
    
        publishInnerWidth: function (ownerContext, width) {
            var owner = this.owner;
            
            if (owner.labelAlign !== 'top' && owner.descWrapEl)
            {
                // When label is not at top, description will reduce the space for field
                width -= owner.descWrapEl.getWidth();
            }
            
            if (owner.labelAlign !== 'top' && owner.warningWrapEl && owner.hasActiveWarning()) 
            {
                width -= owner.warningWrapEl.getWidth();
            }
            
            if (owner.labelAlign == 'top' && owner.errorWrapEl && owner.hasActiveError()) 
            {
                width += owner.errorWrapEl.getWidth(); // Compensation
            }
            
            this.callParent([ownerContext, width]);
        }
     });
    
})();
                
/*
 * Support for optional label on text field to indicate field is multiple
 */
(function() 
{
    Ext.define("Ametys.form.field.Text", {
        override: "Ext.form.field.Text",
        
        getSubTplMarkup: function() {
            var result = this.callParent(arguments);
            
            /**
             * @member Ext.form.field.Text
             * @ametys
             * @since Ametys-Runtime-3.9
             * @cfg {Boolean} [ametysShowMultipleHint=false] true to show to multiple hint under the field. false by default
             */
            if (this.ametysShowMultipleHint == true)
            {
                result += '<div class="ametys-field-hint">(' + "{{i18n PLUGINS_CORE_UI_MULTIPLE_HINT}}" + ')</div>'
            }
            
            return result;
        }
    });
})();

/*
 * Support for optional label on text field to indicate field is multiple
 */
(function() 
{
    Ext.define("Ametys.form.field.Date", {
        override: "Ext.form.field.Date",
        
        getJsonValue: function ()
        {
        	return this.getSubmitValue();
        }
    });
})();

/*
 * Support for optional label on files to indicate max allowed size 
 */
(function() 
{
    Ext.define("Ametys.form.field.File", {
        override: "Ext.form.field.File",
        
        /**
         * @member Ext.form.field.File
         * @ametys
         * @since Ametys-Runtime-4.8
         * @cfg {Boolean} [multipleFile=false] To make selection multiple
         */
        
        afterRender: function()
        {
            this.callParent(arguments);
            
            /**
             * @member Ext.form.field.File
             * @ametys
             * @since Ametys-Runtime-3.9
             * @cfg {Boolean} ametysShowMaxUploadSizeHint false to hide to max size hint under the field. true by default
             */
            if (Ametys.MAX_UPLOAD_SIZE != undefined && Ametys.MAX_UPLOAD_SIZE != '' && this.ametysShowMaxUploadSizeHint !== false)
            {
                this.inputEl.parent().dom.setAttribute('data-maxsizemsg', "({{i18n PLUGINS_CORE_UI_UPLOAD_HINT}}" + Ext.util.Format.fileSize(Ametys.MAX_UPLOAD_SIZE) + ")");
            }
        },
        
        getValue: function() {
            return this.value;
        },
        
        /**
         * @member Ext.form.field.File
         * @ametys
         * @since Ametys-Runtime-4.8
         * @return File[] The files
         * Get the files of the field input
         */
        getFiles: function()
        {
            var filesArray = [];
            for (let file of this.fileInputEl.dom.files)
            {
                filesArray.push(file);
            }
            
            return filesArray;
        }
    });
    
    Ext.define("Ametys.form.field.FileButton", {
        override: "Ext.form.field.FileButton",
    
        createFileInput: function(isTemporary)
        {
            this.callParent(arguments);
            if (!!this.ownerCt.multipleFile)
            {
                this.fileInputEl.dom.setAttribute('multiple', true)
            }
        },
        
        fireChange: function(e) 
        {
            var filenames = [];
            
            if (!!this.ownerCt.multipleFile)
            {
                for (let file of this.fileInputEl.dom.files)
                {
                    filenames.push(file.name);
                }
            }
            else
            {
                filenames = this.fileInputEl.dom.files[0].name;
            }
            
            this.fireEvent('change', this, e, filenames);
        }
    });
})();

/*
 * Support for readable value
 */
(function() 
{
    Ext.define("Ametys.form.field.ComboBox", {
        override: "Ext.form.field.ComboBox",
        
        getReadableValue: function ()
        {
            return this.getDisplayValue();
        }
    });
})();

/*
 * Support for optional label on text field to indicate field is multiple
 */
(function() 
{
    Ext.define("Ametys.form.field.Number", {
        override: "Ext.form.field.Number",

        /**
         * @cfg {Boolean} [submitLocaleSeparator=false ]
         * @since Ametys-Runtime-3.9
         * @ametys
         *
         * @inheritdoc Ext.form.field.Number#cfg-submitLocaleSeparator
         */
        
        submitLocaleSeparator: false,
        
        /**
         * @cfg {String} [baseChars='0123456789.'] 
         * @since Ametys-Runtime-3.9
         * @ametys
         *
         * @inheritdoc Ext.form.field.Number#cfg-baseChars
         */
        baseChars: '0123456789.'
    });
})();

/*
 * Support for background animation 
 */
(function () 
{
    /**
     * @member Ext.dom.Element
     * @method animate 
     * @since Ametys-Runtime-3.9
     * @ametys
     * Ametys additionally handles `background-position` to animate a background-image and `background-position-step` to step this animation.
     * Both args are array of numbers with unit.
     * To right align, use '100%'.
     * 
     * The following example will animate the background image of the element to the coordinates 0,0. 
     * The animation will be "normal" on the x axis but will only use 256 multiples on the y axis.
     * 
     *     el.animate({ 
     *          to: { 
     *              'background-position': ['0px', '0px'], 
     *              'background-position-step': ['1px', '256px'] 
     *          }, 
     *          duration: 500, 
     *          easing: 'linear' 
     *     });
     */
    Ext.define('Ametys.fx.target.Element', {
        override: 'Ext.fx.target.Element',
        
        getElVal: function(element, attr, val) 
        {
            if (val == undefined && attr === 'background-position') 
            {
                var bgPos = element.getStyle("background-position");
                /^([^ ]*) ([^ ]*)$/.exec(bgPos);
                val = [ RegExp.$1, RegExp.$2 ];
                
                return val;
            }
            return this.callParent(arguments);
        },
        
        setElVal: function(element, attr, value)
        {
            if (attr === 'background-position') 
            {
                var anim = Ext.fx.Manager.getActiveAnimation(element.id);
                var to = anim.to['background-position'];
                var by = anim.to['background-position-step']
                if (by == null)
                {
                    by = [1, 1];
                }

                var roundedVal = [
                    Math.round((parseInt(value[0]) - parseInt(to[0])) / parseInt(by[0])) * parseInt(by[0]) + parseInt(to[0]),
                    Math.round((parseInt(value[1]) - parseInt(to[1])) / parseInt(by[1])) * parseInt(by[1]) + parseInt(to[1])
                ];
                var units = [
                    value[0].replace(/[-.0-9]*/, ''),
                    value[1].replace(/[-.0-9]*/, '')
                ];

                element.setStyle("background-position", roundedVal[0] + units[0] + ' ' + roundedVal[1] + units[1]);
            } 
            else 
            {
                this.callParent(arguments);
            }
        }
    });
})();

/*
 * Add a truncate method on TextMetrics 
 */
(function () 
{
    Ext.define('Ametys.util.TextMetrics', {
        override: 'Ext.util.TextMetrics',
        
        /**
         * @member Ext.util.TextMetrics
         * @ametys
         * @since Ametys-Runtime-3.9
         * Make an ellipsis on the provided text if necessary.
         * @param {String} text The text to test
         * @param {Number} maxWidth The max authorized with for this text
         * @param {String} [ellipsis='...'] The ellipsis text.
         * @returns {String} The text (potentially ellipsed) that fills in maxWidth. Returns an empty text, if the initial text is fully truncated.
         */
        ellipseText: function(text, maxWidth, ellipsis) 
        {
            if (text == null || text == '')
            {
                return '';
            }
            
            ellipsis = ellipsis || '...';
            
            function getLastNonEmptyTextNode(html)
            {
                var lastChild = html.lastChild;
                if (lastChild == null)
                {
                    return null;
                }

                while (lastChild.lastChild != null)
                {
                    lastChild = lastChild.lastChild;
                }
                
                if (lastChild.nodeType == 3) // node text
                {
                    var text = lastChild.nodeValue;
                    if (Ext.isEmpty(text) || text == ellipsis)
                    {
                        // Remove empty text
                        lastChild.parentNode.removeChild(lastChild);
                        return getLastNonEmptyTextNode(html);
                    }
                    else
                    {
                        if (text.indexOf(ellipsis, text.length - 3) == -1)
                        {
                            lastChild.nodeValue = text + ellipsis;
                        }
                        return lastChild;
                    }
                }
                else
                {
                    // Remove empty tag
                    lastChild.parentNode.removeChild(lastChild);
                    return getLastNonEmptyTextNode(html);
                }
            }
            
            var html = document.createElement('div');
            html.innerHTML = text;
            
            if (this.getWidth(html.innerHTML) > maxWidth && maxWidth > 0)
            {
                var textNode = getLastNonEmptyTextNode(html);
                
                while (this.getWidth (html.innerHTML) > maxWidth)
                {
                    textNode.nodeValue = textNode.nodeValue.substring (0, textNode.nodeValue.length - ellipsis.length - 1) + ellipsis;
                    textNode = getLastNonEmptyTextNode(html);
                }
                
                return html.innerHTML;
            }
            else
            {
                return text;
            }
        },
        
        /**
         * @member Ext.util.TextMetrics
         * @ametys
         * @since Ametys-Runtime-3.9
         * Delimits the text given a number of lines and a maximal width.
         * @param {String} text The text to delimit.
         * @param {Number} maxWidth The max authorized with for each line.
         * @param {Number} nbLines The max number of lines authorized.
         * @param {String} ellipsis The possible ellipsis at the end of the text. Default to '...'
         * @param {String/Boolean} hyphen The word break character used to for word that exceed the max width. False to disable hyphenation.
         * @returns {String} The text (potentially ellipsed) that fills in maxWidth. Returns an empty text, if the initial text is fully truncated.
         */
        delimitText: function(text, maxWidth, nbLines, ellipsis, hyphen)
        {
            if (!text)
            {
                return '';
            }
            
            if (!maxWidth)
            {
                return text;
            }
            
            nbLines = nbLines || 1;
            if (nbLines <= 1)
            {
                return this.ellipseText(text, maxWidth, ellipsis);
            }
            
            if (hyphen !== false)
            {
                hyphen = hyphen || '-';
            }
            
            // Cut text in lines.
            var lines = [];
            var nextIndex = -1;
            var nextWord = '';
            var currentLine = '';
                
            do
            {
                nextIndex = text.indexOf(' ');
                
                if (nextIndex != -1)
                {
                    nextWord = text.substr(0, nextIndex);
                    text = text.substring(nextIndex + 1);
                }
                else
                {
                    nextWord = text;
                    text = '';
                }
                
                // Add next word to current line, or create a new line depending on max width threshold.
                if (currentLine != '')
                {
                    // test width of currentline + nextword
                    if (this.getWidth(currentLine + ' ' + nextWord) <= maxWidth)
                    {
                        currentLine += ' ' + nextWord;
                        nextWord = '';
                    }
                    else
                    {
                        lines.push(currentLine);
                        currentLine = '';
                    }
                }
                
                // Next hyphenation management.
                while (nextWord != '')
                {
                    // Ensure next word is not exceeding max width
                    if (this.getWidth(nextWord) > maxWidth)
                    {
                        nextWordTruncated = this.ellipseText(nextWord, maxWidth, hyphen);
                        
                        // If possible, add a new line with the next word truncated
                        // Otherwise, just add the next word into the current line,
                        // it will be ellipsed as the last line.
                        if (lines.length < nbLines - 1)
                        {
                            lines.push(nextWordTruncated);
                            
                            // Calculate the rest of next word (if hyphenation is not disabled)
                            if (hyphen !== false && Ext.String.endsWith(nextWordTruncated, hyphen))
                            {
                                nextWord = nextWord.substring(nextWordTruncated.length - hyphen.length);
                            }
                            else
                            {
                                nextWord = '';
                            }
                            
                            currentLine = '';
                        }
                        else
                        {
                            currentLine = nextWord;
                            nextWord = '';
                        }
                    }
                    else
                    {
                        currentLine = nextWord;
                        nextWord = '';
                    }
                }
                
            } while (text != '' && lines.length < nbLines - 1);
                
            // Do not forget to re-include the current line into the text
            // because it must be taken into account when ellipsing the last line.
            if (currentLine != '')
            {
                text = text != '' ? currentLine + ' ' + text : currentLine;
            }
            
            // Add the last line which might be ellipsed.
            if (text != '' && lines.length < nbLines)
            {
                var lastLine = this.ellipseText(text, maxWidth);
                lines.push(lastLine);
            }
            
            return lines.join('<br/>');
        }
    });
})();

/*
 * Overriding some ux classes
 */
(function()
{
    /**
     * Override of the {@link Ext.ux.DataView.Draggable} to handle multiselection with Drag operation.
     * Also embed other minor tweaks, see {@link Ext.ux.DataView.Draggable} for comparison.
     */
    Ext.define('Ametys.ux.DataView.Draggable', {
        override: 'Ext.ux.DataView.Draggable',
    
        /**
         * @private
         * Allow to override itemSelector ghost config.
         * @param {Ext.view.View} dataview  The Ext.view.View instance that this DragZone is attached to
         * @param {Object} config The configuration
         */
        init: function(dataview, config) {
            this.callParent(arguments);
    
            Ext.apply(this.ghostConfig, {
                itemSelector: config.ghostConfig && config.ghostConfig.itemSelector || this.ghostConfig.itemSelector
            });
        },
        
        /**
         * Tweaked from {@link Ext.ux.DataView.Draggable} (method getDragData,
         * see source) to correctly handle multiselection.
         * See {@link Ext.view.DragZone#getDragData}
         * @param e
         * @private
         */
        getDragData: function(e) {
            var draggable = this.dvDraggable,
                dataview  = this.dataview,
                selModel  = dataview.getSelectionModel(),
                target    = e.getTarget(draggable.itemSelector),
                selected, records, dragData;
            
            // Do not allow drag with ctrl or shift modifier.
            if (!selModel.getCount() || e.ctrlKey || e.shiftKey) return false;
        
            // Modifying logic of this if-block, to allow multiselection.
            if (target)
            {
                // Target record must be draggable.
                var targetRecord = dataview.getRecord(target);
                if (targetRecord.get('allowDrag') === false)
                {
                    return false;
                }
                
                // If target is not already selected,
                // select it (and only it)
                if (!dataview.isSelected(target)) {
                    selModel.select(dataview.getRecord(target));
                }
                
                // Filters the records that are allowed to be dragged, and
                // cancel the event if no record can be dragged.
                records = selModel.getSelection();
                records = Ext.Array.filter(records, function(record)
                {
                    return record.get('allowDrag') !== false;
                });
                
                if (Ext.isEmpty(records))
                {
                    return false;
                }
                
                selected = Ext.Array.map(records, function(record) {
                    return dataview.getNode(record);
                });
                
                dragData = {
                    copy: true,
                    nodes: selected,
                    records: records,
                    item: true
                };
                
                if (selected.length == 1) {
                    dragData.single = true;
                    dragData.ddel = target;
                } else {
                    dragData.multi = true;
                    dragData.ddel = draggable.prepareGhost(records).dom;
                }
        
                return dragData;
            }
        
            return false;
        }
    });
})();

(function ()
        {
            // Override SortType to add support for accented characters
            Ext.define("Ametys.dom.Query", {
                override: "Ext.dom.Query",
                
                /**
                 * @member Ext.dom.Query
                 * @method selectDirectElements 
                 * @since Ametys-Runtime-3.9
                 * @ametys
                 * Select a direct child element by a given name
                 * @param {String} [element="*"] Name of the elements to limit.
                 * @param {HTMLElement} [node=document] The start of the query.
                 * @return {HTMLElement[]} An array of DOM elements
                 */
                selectDirectElements: function (element, node)
                {
                    var selector = element || '*';
                    
                    var childNodes = Ext.dom.Query.select('> ' + selector, node);
                    var elements = [];
                    for (var i = 0; i < childNodes.length; i++)
                    {
                        // Test if Node.ELEMENT_NODE
                        if (childNodes[i].nodeType == 1)
                        {
                            elements.push(childNodes[i]);
                        }
                    }
                    return elements;
                }
            });
})();

(function ()
        {
            Ext.define("Ametys.Base", {
                override: 'Ext.Base',
                
                inheritableStatics: {
                    /**
                     * @private
                     * @property {Ametys.log.Logger} _logger The logger instance
                     */
                    _logger: null,
                    
                    /**
                     * @member Ext.Base
                     * @method getLogger
                     * @static
                     * @ametys
                     * @since Ametys-Runtime-3.9 
                     * Get the logger of this class (using classname as category)
                     * 
                     *      try
                     *      {
                     *          if (this.getLogger().isDebugEnabled())
                     *          {
                     *              this.getLogger().debug("Starting process")
                     *          }
                     *      
                     *          ...
                     *
                     *          if (this.getLogger().isDebugEnabled())
                     *          {
                     *              this.getLogger().debug("Ending process")
                     *          }
                     *      }
                     *      catch (e)
                     *      {
                     *              this.getLogger().error({message: "Ending process", details: e});
                     *      }
                     * 
                     * @return {Ametys.log.Logger} The logger
                     */
                    getLogger: function()
                    {
                        if (this._logger == null)
                        {
                            this._logger = Ametys.log.LoggerFactory.getLoggerFor(this.getName()) 
                        }
                        return this._logger;
                    }
                },
                
                /**
                 * @member Ext.Base
                 * @method getLogger
                 * @ametys
                 * @since Ametys-Runtime-3.9 
                 * Get the logger of this class (using classname as category)
                 * 
                 *      try
                 *      {
                 *          if (this.getLogger().isDebugEnabled())
                 *          {
                 *              this.getLogger().debug("Starting process")
                 *          }
                 *      
                 *          ...
                 *
                 *          if (this.getLogger().isDebugEnabled())
                 *          {
                 *              this.getLogger().debug("Ending process")
                 *          }
                 *      }
                 *      catch (e)
                 *      {
                 *              this.getLogger().error({message: "Ending process", details: e});
                 *      }
                 * 
                 * @return {Ametys.log.Logger} The logger
                 */
                getLogger: function()
                {
                    if (this.self._logger == null)
                    {
                        this.self._logger = Ametys.log.LoggerFactory.getLoggerFor(this.self.getName()) 
                    }
                    return this.self._logger;
                },
                
                /**
                 * @member Ext.Base
                 * @method addCallables
                 * @ametys
                 * @since Ametys-Runtime-4.0 
                 * Add methods to this object that will call a server method using Ametys.data.ServerComm#callMethod.
                 * 
                 * The generated method should be documented using the following template
                 * 
                 * 
                 *          @ callable
                 *          @ member My.Object
                 *          @ method MethodName 
                 *          This calls the method 'MMM' of the server DAO 'XXX'.
                 *          @ param {Object[]} parameters The parameters to transmit to the server method
                 *          @ param {} parameters[0] myparam
                 *          ...
                 *          @ param {Function} callback The function to call when the java process is over. Can be null. Use options.scope for the scope. 
                 *          @ param {Object} callback.returnedValue The value return from the server. Null on error (please note that when an error occured, the callback may not be called depending on the value of errorMessage).
                 *          @ param {Object} callback.args Other arguments specified in option.arguments                 
                 *          @ param {Object[]} callback.parameters Parameters of the initial call transmited in parameters argument.
                 * 
                 *          @ param {Object/String/Boolean} [progressMessage] The message and title to show on the dialog containing a progress bar, or only the message or true to use default message and title
                 *          @ param {Object/String/Boolean} [progressMessage] The message and title to show on the dialog containing a progress bar, or only the message or true to use default message and title. This will be shown when downloading a file to show its progress with a progress bar.
                 *          @ param {Object} [progressMessage.msg] The message of the dialog containing a progress bar, if not present, the default message will be used
                 * 
                 *          @ param {Object} [progressCallback] The callback to call to show progress
                 *          @ param {Function} [progressCallback.handler] Called to show progress.
                 *          @ param {Object} [progressCallback.handler.uploadPercent] The percentage of progress of the upload. 
                 *          @ param {Object} [progressCallback.handler.serverPercent] The percentage of progress of the server process. null when not supported.                  
                 *          @ param {Object[]} [progressCallback.handler.arguments] Is the 'progressCallback.arguments'
                 * 
                 *          @ param {Object} [options] Advanced options for the call.
                 *          @ param {Boolean/String/Object} [options.errorMessage] Display an error message. See Ametys.data.ServerComm#callMethod errorMessage.
                 *          @ param {Boolean/String/Object} [options.waitMessage] Display a waiting message. See Ametys.data.ServerComm#callMethod waitMessage.
                 *          @ param {Number} [options.scope] This parameter is the scope used to call the callback. Moreover is the given class is a mixin of Ametys.data.ServerCaller, its methods Ametys.data.ServerCaller#beforeServerCall and Ametys.data.ServerCaller#afterServerCall will be used so see their documentation to look for additional options (such a refreshing on Ametys.ribbon.element.ui.ButtonController#beforeServerCall).
                 *          @ param {Number} [options.priority] The message priority. See Ametys.data.ServerComm#callMethod for more information on the priority. PRIORITY_SYNCHRONOUS cannot be used here.
                 *          @ param {String} [options.cancelCode] Cancel similar unachieved read operations. See Ametys.data.ServerComm#callMethod cancelCode.
                 *          @ param {Object} [options.arguments] Additional arguments set in the callback.arguments parameter.                  
                 *          @ param {Boolean} [options.ignoreCallbackOnError] If the server throws an exception, should the callback beeing called with a null parameter. See Ametys.data.ServerComm#callMethod ignoreOnError.
                 * 
                 * 
                 * @param {Object/Object[]} configs The default values for Ametys.data.ServerComm#callMethod config argument. Concerning the callback config, it will be added (not replaced).
                 * @param {Function} [configs.convertor] An optional function to convert the argument "returnValue" of the callback of the created method. 
                 * @param {Object} configs.convertor.returnedValue The value return from the server. Null on error (please note that when an error occured, the callback may not be called depending on the value of errorMessage).
                 * @param {Object} configs.convertor.arguments Other arguments specified in option.arguments                 
                 * @param {Object[]} configs.convertor.parameters Parameters of the initial call transmited in parameters argument.
                 * @param {Object} configs.convertor.return The converted value
                 * @param {String} [configs.localName=configs.methodName] This additionnal optionnal argument stands for the local method name.
                 * @param {Number} [configs.localParamsIndex] After the index in parameters array, parameters are considered as local only and will not be transmited to server. Use to transmit to all callbacks. Can be null if all parameters are server parameters.  Negative values are offsets from the end of the parameters array.
                 */
                addCallables: function(configs)
                {
                    configs = Ext.Array.from(configs);
                    
                    Ext.Array.each(configs, function(config) {
                    	config.callback = Ext.Array.from(config.callback);
                    	 
                        this[config.localName || config.methodName] = function(parameters, callback, options) {
                            parameters = parameters || [];
                            options = options || {};
                            
                            // If the scope is a ServerCaller component, let's use #beforeServerCall 
                            if (options.scope && options.scope.isServerCaller)
                            {
                                options.scope.beforeServerCall(options);
                            }
                            
                            // Let's merge the current call parameters, with the default values set at the addCallable call.
                            var methodConfig = {
                                parameters: Ext.Array.slice(parameters, 0, config.localParamsIndex),
                                waitMessage: options.waitMessage,
                                errorMessage: options.errorMessage,
                                cancelCode: options.cancelCode,
                                priority: options.priority   
                            }
                            var finalConfig = Ext.applyIf(methodConfig, Ext.clone(config)); // we have to clone config so config.callback is not altered by following behavior
                            
                            // During the addCallable one or more callbacks may have been set
                            if (callback != null)
                            {
                            	if (Ext.isFunction(config.convertor))
                            	{
                            		var originalCallback = callback;
                            		callback = function(returnValue, args, parameters) {
                            			var convertedValue = config.convertor.apply(this, [returnValue, args, parameters]);
                            			originalCallback.apply(this, [convertedValue, args, parameters]);
                            		}
                            	}
                            	
                            	// Let's add the current method callback
                                finalConfig.callback.push({
                                        handler: callback,
                                        scope: options.scope,
                                        arguments: options.arguments,
                                        ignoreOnError: options.ignoreCallbackOnError
                                });
                            }
                            
                            for (var i = 0; i < finalConfig.callback.length; i++)
                            {
                            	finalConfig.callback[i].handler = Ext.bind (finalConfig.callback[i].handler, finalConfig.callback[i].scope || this, [parameters], 2)
                            }
                            
                            // If the scope is a ServerCaller component, let's use #afterServerCall 
                            if (options.scope && options.scope.isServerCaller)
                            {
                                finalConfig.callback.push({
                                        handler: options.scope.afterServerCall,
                                        scope: options.scope,
                                        arguments: options,
                                        ignoreOnError: false
                                });
                            }
                            
                            // Do the server call now
                            Ametys.data.ServerComm.callMethod(finalConfig);
                        }
                    }, this);
                }                
            });
})();

(function() {
    Ext.override(Ext.grid.plugin.CellEditing, {
        
        /**
         * @member Ext.grid.plugin.CellEditing
         * @ametys
         * @since Ametys-Runtime-3.9
         * @cfg {Boolean} moveEditorOnEnter
         * <tt>false</tt> to turn off moving the editor to the next row (down) when the enter key is pressed or the previous row (up) when shift + enter keys are pressed.
         */

        /**
         * @member Ext.grid.plugin.CellEditing
         * @ametys
         * @since Ametys-Runtime-3.9
         * @cfg {Boolean} [editAfterSelect=false] When #cfg-triggerEvent is not specified or is 'cellclick' and #cfg-clicksToEdit is 1, this configuration allow to enter in edition only if the record was focused first. As a rename under files manager, you will have to first click on the row to select it and click again to edit it (but not doubleclick).
         */

        /**
         * @member Ext.grid.plugin.CellEditing
         * @ametys
         * @since Ametys-Runtime-3.9
         * @private
         * @property {String} armed Used when #cfg-editAfterSelect is true. The cell id that was "armed"... so that was selected in a preceding operation: this is to distinguish a click to select and a second click to edit.
         */
        
        initEditTriggers: function()
        {
            this.callParent(arguments);
            
             if (this.editAfterSelect && (this.triggerEvent == null || this.triggerEvent == 'cellclick') && this.clicksToEdit === 1)
             {
                 this.mon(this.view, 'celldblclick', this.onCellDblClick, this);
             }
        },
        
        onCellClick: function(view, cell, colIdx, record, row, rowIdx, e)
        {
            if (Ext.fly(e.getTarget()).is(".x-field, .x-field *, a, a *"))
            {
                // Let's ignore clicks in fields or links in cells
                return;
            }
            
            let cellIdx = rowIdx + "#" + colIdx;
            
            if (!this.editAfterSelect || (this.triggerEvent != null && this.triggerEvent != 'cellclick') || this.clicksToEdit !== 1)
            {
                this.callParent(arguments);
            }
            else if (this.armed == cellIdx && this.oncellclicktimeout == null)
            {
                this.oncellclicktimeout = Ext.defer(this.onCellClickAndNotDoubleClick, 300, this, arguments);
            }
            this.armed = cellIdx;
        },
        
        /**
         * @member Ext.grid.plugin.CellEditing
         * @ametys
         * @since Ametys-Runtime-3.9
         * @private
         * This method is call asynchronously by #onCellClick, when a single click was done and not a double click
         */
        onCellClickAndNotDoubleClick: function()
        {
            this.oncellclicktimeout = null;
            Ext.defer(this.superclass.onCellClick, 0, this, arguments);
        },
        
        /**
         * @member Ext.grid.plugin.CellEditing
         * @ametys
         * @since Ametys-Runtime-3.9
         * @private
         * Listener on cell double click, only when cell editing is set to a single click on the cell AND #cfg-editAfterSelect is true.
         * This listener is here to cancel a starting editing when finally this is not a simple click
         */
        onCellDblClick: function()
        {
            if (this.oncellclicktimeout != null)
            {
                window.clearTimeout(this.oncellclicktimeout);
                this.oncellclicktimeout = null;
            }
        }
    });

    Ext.override(Ext.grid.CellEditor, {
        
        onSpecialKey : function(field, event) 
        {
            this.callParent(arguments);
            
            if (this.editingPlugin.moveEditorOnEnter == true && event.getKey() == event.ENTER) 
            {
                // We just left the edit mode using the ENTER key: let's edit the following line as editingPlugin#moveEditorOnEnter is true
                var view = this.editingPlugin.view;
                
                if (!view.walkRecs)
                {
                    // LockingGrid does not support walkRecs, so let's add it (and its dependencies)
                    view.walkRecs = Ext.view.Table.prototype.walkRecs;
                    view.getNodeByRecord = Ext.view.Table.prototype.getNodeByRecord;
                    view.retrieveNode = Ext.view.Table.prototype.retrieveNode;
                    view.getRowId = function(record) {
                        return this.lockedView.id + '-record-' + record.internalId;
                    };
                }
                var newRecord = view.walkRecs(this.context.record, event.shiftKey ? -1 : 1);
                if (newRecord && newRecord != this.context.record)
                {
                    event.shiftKey = false; // Shift+ENTER should goes up, but the navigation model will consider this as a SHIFT+UP that means "keep selection" during the move
                    
                    var currentPos = view.getNavigationModel().getPosition()
                    if (currentPos)
                    {
                        var newPos = currentPos.setRow(newRecord);
                        view.getNavigationModel().setPosition(newPos, null, event);
                        
                        this.editingPlugin.startEdit(newRecord, newPos.column);
                    }
                    else
                    {
                        this.editingPlugin.cancelEdit();
                    }
                }
            }
        }
    });
    
    Ext.override(Ext.grid.plugin.CellEditing, {
        // Click event in a editor widget is sent after the cell click
        activateCell: function(position)
        {
            var me = this;
            if (me.getActiveColumn() == position.column
                && me.getActiveRecord() == position.record)
            {
                return true; // Cell is actionable but we want to ignore all editor stuff, since we just reclick in an editing cell
            }
            else
            {
                return this.callParent(arguments);
            }
        }
    }); 
    
    Ext.override(Ext.grid.column.Column, {
        
        /**
         * @member Ext.grid.column.Column
         * @ametys
         * @since Ametys-Runtime-4.9
         * @cfg {boolean} [doNotEscapeHtml=false] When #cfg-renderer and #cfg-xtype are not specified, the default renderer for column is {@link Ametys.grid.GridColumnHelper.renderWithHtmlEscaping} that escape HTML data. Set #cfg-doNotEscapeHtml to true to bypass this default renderer.
         */
        
        constructor: function(config)
        {
            if (!config.xtype
                    && !config.renderer
                    && config.doNotEscapeHtml !== true)
                {
                    config.renderer = Ametys.grid.GridColumnHelper.renderWithHtmlEscaping;
                }
                
            this.callParent(arguments);
        }
    });
    
    Ext.override(Ext.grid.header.Container, {
        /**
         * @event headerauxclick
         * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates
         * all column headers.
         * @param {Ext.grid.column.Column} column The Column header Component which provides
         * the column definition
         * @param {Ext.event.Event} e
         * @param {HTMLElement} t
         */
        
        /**
         * @cfg {Boolean} hideOnAuxClick
         * True to enable hidding with a "middle" click on hideable column
         */
        hideOnAuxClick: true,
        
        initEvents: function() {
            // Mostly inspired from the Ext.grid.header.Container#initEvents method
           let me = this;
            
           me.callParent();

           // If this is top level, listen for events to delegate to descendant headers.
           if (!me.isColumn && !me.isGroupHeader) {
               let listeners = {
                   auxclick: me._onHeaderCtAuxClick,
                   scope: me
               };
               
               me.mon(me.el, listeners);
           }
       },
       
       _onHeaderCtAuxClick: function(e, t) {
            // Mostly inspired from the Ext.grid.header.Container#onHeaderCtEvent method
            let me = this,
                headerEl = me.getHeaderElByEvent(e);

           if (headerEl && !me.blockEvents) {
               let header = Ext.getCmp(headerEl.id);

               if (header) {
                   let targetEl = header[header.clickTargetName];

                   // If there's no possibility that the mouseEvent was on child header items,
                   // or it was definitely in our titleEl, then process it
                   if ((!header.isGroupHeader && !header.isContainer) || e.within(targetEl)) {
                       me._onHeaderAuxClick(header, e, t);
                   }
               }
           }
       },
       
       _onHeaderAuxClick: function(header, e, t) {
           header.fireEvent('headerauxclick', this, header, e, t);
           this.fireEvent('headerauxclick', this, header, e, t);
           
           // enable hide on aux click if not explicitly disabled on the container or header
           if (this.hideOnAuxClick && header.hideOnAuxClick)
           {
               // Only act on "middle" click and hideable column
               if (e.button == 1 && header.isHideable()) {
                   header.setVisible(false);
               }
           }
       }
    });
    
	Ext.override(Ext.tree.Column, {
	        
			statics: 
			{
				/**
				 * @member Ext.tree.Column
				 * @ametys
				 * @since Ametys-Runtime-4.9
		         * @property {boolean} doNotEscapeNodeText=false When <code>false</code>, the default renderer for text node is {@link Ametys.grid.GridColumnHelper.renderWithHtmlEscaping} that escape HTML data.<br>
				 * Set to <code>true</code> before creating {@link Ext.tree.Panel} to skip escaping of text nodes.
		         */
				doNotEscapeNodeText: false
			},
			
	        constructor: function(config)
	        {
	            if (!config.renderer
	                    && Ext.tree.Column.doNotEscapeNodeText !== true)
	                {
	                    config.renderer = Ametys.grid.GridColumnHelper.renderWithHtmlEscaping;
	                }
	                
	            this.callParent(arguments);
	        }
	    });    
})();

(function()
{
    // Override #isEqual on a model field to be able to compare two objects based on their set of properties and values
    function isEqual(lhs, rhs, field, original)
    {
        if (!field)
        {
            if (Ext.isObject(lhs) && Ext.isObject(rhs))
            {
                return Ext.Object.equals(lhs, rhs);       
            } 
            else if (Ext.isArray(lhs) && Ext.isArray(rhs))
            {
                if (lhs.length != rhs.length)
                {
                    return false;
                }
                
                for (let i in lhs)
                {
                    if (!isEqual.call(this, lhs[i], rhs[i], field, original))
                    {
                        return false;
                    }
                }
                
                return true;
            }
        }
            
        return original.call(this, lhs, rhs, field);
    }
    
    var _isEqual1 = Ext.data.Model.prototype.isEqual; 
    Ext.data.Model.prototype.isEqual = function(lhs, rhs, field) { return isEqual.call(this, lhs, rhs, field, _isEqual1); };
    var _isEqual2 = Ext.data.field.Field.prototype.isEqual;
    Ext.data.field.Field.prototype.isEqual = function(lhs, rhs) { return isEqual.call(this, lhs, rhs, null, _isEqual2); };
})();

(function()
{
    Ext.override(Ext.JSON, {
        /**
         * @member Ext.JSON
         * @ametys
         * @since Ametys-Runtime-3.9
         * Converts an object to a readable json string as a HTML fragment.
         * @param {Object} value The value to encode.
         * @param {Number} [offset=0] The offset to indent the text
         * @param {Function} [renderer] A renderer function to pass when a custom output is wanted. Can call Ext.JSON#prettyEncode for recursion.
         * @param {Object} renderer.value The value to encode
         * @param {Number} renderer.offset The offset to indent the text (to use only when generating new lines).
         * @param {String/Boolean} renderer.return The rendered value or `false` to get the standard rendering for this value.
         * @param {Number} [startClosedAtOffset=2] When reaching this offset, the arrays or objects are closed 
         * @return {String} The rendered value, as a HTML fragment.
         */
        prettyEncode: function (value, offset, renderer, startClosedAtOffset)
        {
            function openArrayOrObject(separator, value)
            {
                function closedText(values) 
                {
                    let valuesLabel = values != 1 ? "{{i18n PLUGINS_CORE_UI_JSON_VALUES}}" : "{{i18n PLUGINS_CORE_UI_JSON_VALUE}}";
                    return "... " + values + " " + valuesLabel + " ...";
                }
                return '<div class="json-array' + (offset < (startClosedAtOffset || 2) ? '' : ' json-closed') + '">' 
                        + '<span class="json-char" onclick="Ext.get(this.parentNode).toggleCls(\'json-closed\')" oncontextmenu="Ext.get(this.parentNode).removeCls(\'json-closed\'); Ext.get(this.parentNode).select(\'div.json-closed\').removeCls(\'json-closed\'); return false;">' 
                                + separator 
                        + '</span>' 
                        + '<span class="json-closed" style="display: none">' + closedText(Ext.isArray(value) ? value.length : Ext.Object.getSize(value)) + '</span>' 
                        + '<span>';
            }
            function closeArrayOrObject(separator)
            {
                return "</span>" + separator + "</div>";
            }
            function insertOffset(offset)
            {
                var s = "";
                for (var i = 0; i < offset; i++)
                {
                    s += "&#160;&#160;&#160;&#160;";
                }
                return s;
            }
            
            function pretty(value, offset)
            {
                var s = "";
                var result;
                if (Ext.isFunction(renderer) && (result = renderer(value, offset)))
                {
                    return result;
                }
                else if (value != null && value.$className)
                {
                    return pretty("Object " + value.$className + (typeof(value.getId) == 'function' ? ('@' + value.getId()) : ''));
                }
                else if (typeof(value) == "function")
                {
                    return "null";
                }
                else if (Ext.isArray(value))
                {
                    if (value.length == 0)
                    {
                        s += '<div class="json-array">[ ]</div>';
                    }
                    else
                    {
                        s += openArrayOrObject("[", value);
                        
                        var hasOne = false;
                        for (var id = 0; id < value.length; id++)
                        {
                            if (hasOne)
                            {
                                s += ",";
                            }
                            hasOne = true;
                            s += "<br/>"
                            s += insertOffset(offset+1);
                            s += pretty(value[id], offset + 1);
                        }
                        
                        if (hasOne)
                        {
                            s += "<br/>";
                            s += insertOffset(offset);
                        }
                        else
                        {
                            s += " ";
                        }
                        s += closeArrayOrObject("]");
                    }
                }
                else if (Ext.isObject(value))
                {
                    if (Ext.Object.isEmpty(value))
                    {
                        s += '<div class="json-object">{ }</div>';
                    }
                    else
                    {
                        s += openArrayOrObject("{", value);
                        
                        var hasOne = false;
                        for (var id in value)
                        {
                            if (hasOne)
                            {
                                s += ",";
                            }
                            hasOne = true;
                            s += "<br/>"
                            s += insertOffset(offset+1);
                            s += "<strong>" + Ext.JSON.encodeValue(id) + "</strong>: ";
                            s += pretty(value[id], offset + 2);
                        }
        
                        if (hasOne)
                        {
                            s += "<br/>";
                            s += insertOffset(offset);
                        }
                        else
                        {
                            s += " ";
                        }
                        s += closeArrayOrObject("}");
                    }
                }
                else if (value === undefined)
                {
                    s += "undefined";
                }
                else
                {
                    s += Ext.String.htmlEncode(Ext.JSON.encodeValue(value));
                }
                
                return s;            
            }

            offset = offset || 0;
            return insertOffset(offset) + pretty(value, offset);
        }
    });
})();

(function()
{
    Ext.define("Ametys.ux.IFrame", {
        override: 'Ext.ux.IFrame',
        
        loadMask: "{{i18n PLUGINS_CORE_UI_IFRAME_LOADING}}"
    });
})();

(function()
{
    /**
     * @override Ext.form.field.Tag
     */
    Ext.define("Ametys.form.field.Tag", {
        override: 'Ext.form.field.Tag',
        
        /** 
         * @since Ametys-Runtime-3.9
         * @ametys
         * @cfg {Boolean} [labelHTML=false] If true the labelTpl will not be encoded 
         */
        labelHTML: false,
        
        getMultiSelectItemMarkup: function()
        {
            var value = this.callParent(arguments);
            
            if (this.labelHTML)
            {
                var me = this;
                this.multiSelectItemTpl.getItemLabel = function(values) { return me.labelTpl.apply(values); };
                return this.multiSelectItemTpl.apply(this.valueCollection.getRange());
            }
            else
            {
                return value;
            }
        },
        
        /**
         * @member Ext.form.field.Tag
         * @method addValue
         * @since Ametys-Runtime-4.1
         * @ametys
         * 
         * To be able to add value (where setValue only replace).
         */
        addValue: function(value)
        {
            if (value != null)
            {
                var oldValue = this.getValue() || [],
                    valueToAdd = Ext.isArray(value) ? value : [value];
                return this.setValue(Ext.Array.merge(oldValue, valueToAdd));
            }
        },
        
        /* 
         * Override #setValue to be able to set unknown values more than once.
         * If {@link #queryMode} is `remote`, the store will load as soon as #setValue is called with unknown records.
         * Infinite loop is prevented thanks to `skipLoad` argument.
         */
        setValue: function(value, add, skipLoad)
        {
        	var me = this,
	            valueStore = me.valueStore,
	            valueField = me.valueField,
	            unknownValues = [],
	            store = me.store,
	            record, len, i, valueRecord, cls, params, isNull;
            
            // fix for CMS-9638 avoid running this function on a destroyed component and have null store, etc.
            if (me.destroyed)
            {
                return;
            }
	
	        if (Ext.isEmpty(value)) {
	            value = null;
	            isNull = true;
	        } else if (Ext.isString(value) && me.multiSelect) {
	            value = value.split(me.delimiter);
	        } else {
	            value = Ext.Array.from(value, true);
	        }
	
	        // Fix: remove unloaded condition to call #setValue with unknown values more than once
	        if (!isNull && me.queryMode === 'remote' && !store.isEmptyStore && skipLoad !== true) {
	            for (i = 0, len = value.length; i < len; i++) {
	                record = value[i];
	                if (!record || !record.isModel) {
	                    valueRecord = valueStore.findExact(valueField, record);
	                    if (valueRecord > -1) {
	                        value[i] = valueStore.getAt(valueRecord);
	                    } else {
	                        valueRecord = me.findRecord(valueField, record);
	                        if (!valueRecord) {
	                            if (me.forceSelection) {
	                                unknownValues.push(record);
	                            } else {
	                                valueRecord = {};
	                                valueRecord[me.valueField] = record;
	                                valueRecord[me.displayField] = record;
	
	                                cls = me.valueStore.getModel();
	                                valueRecord = new cls(valueRecord);
	                            }
	                        }
	                        if (valueRecord) {
	                            value[i] = valueRecord;
	                        }
	                    }
	                }
	            }
	
	            if (unknownValues.length) {
	                params = {};
	                params[me.valueParam || me.valueField] = unknownValues.join(me.delimiter);
	                store.loading = true; // for some reasong the following load may not set this boolean as soon as we require
	                store.loaded = false; // for some reasong the following load may not set this boolean as soon as we require
	                store.load({
	                    params: params,
	                    callback: function(record, op, success) {
                            // In case of server error, set null value (to avoid infinite loop)
                            me.lastValue = success ? value : null; // Avoid checkChange to fire change event CMS-11998
                            me.setValue(success ? value : null, add, true);
                            me.validate(); // As we cheated on lastValue before the setValue, the validation have to be manually relaunched
                            me.autoSize();
                            me.lastQuery = false;
                            store.loading = false;
                            store.loaded = true;
	                    }
	                });
	                return false;
	            }
	        }
	
	        // For single-select boxes, use the last good (formal record) value if possible
	        if (!isNull && !me.multiSelect && value.length > 0) {
	            for (i = value.length - 1; i >= 0; i--) {
	                if (value[i].isModel) {
	                    value = value[i];
	                    break;
	                }
	            }
	            if (Ext.isArray(value)) {
	                value = value[value.length - 1];
	            }
	        }
	
	        return me.callSuper([value, add]);
        }
    });    
})();

(function()
{
		/*
	     * Override Ext.picker.Color to support rgb and rgba css format
	     */
	    Ext.define("Ametys.picker.Color", {
	        override: 'Ext.picker.Color',
	        
	        colorRe: /(?:^|\s)a-color-([0-9]+)(?:\s|$)/,
	        
	        /** 
             * @property {String[]} colors An array of color code strings. The supported formats are 6-digit color hex code strings (with or without the # symbol),
             * rgb or rgba string values. This array can contain any number of colors, and each code should be unique.
             */
	        
	        /** 
             * @member Ext.picker.Color
             * @since Ametys-Runtime-4.0
             * @ametys
             * @private
             * @property {RegExp} hexColorRe The regexp for 6-digit color hex code without the # symbol
             */
	        hexColorRe: /^([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/,
	        
	        renderTpl: [
                '<tpl for="colors">',
                    '<a href="#" role="button" class="a-color-{#} {parent.itemCls}" hidefocus="on">',
                        '<span class="{parent.itemCls}-inner" style="background:{.}">&#160;</span>',
                    '</a>',
                '</tpl>'
            ],
            
            initRenderData : function(){
                var me = this;
                return Ext.apply(me.callParent(), {
                    itemCls: me.itemCls,
                    colors: me._formatColors(me.colors)
                });
            },
            
            /** 
             * @member Ext.picker.Color
             * @since Ametys-Runtime-4.0
             * @ametys
             * @private
             * Format the colors to be use directly as background CSS property
             */
            _formatColors: function (colors)
            {
            	var me = this,
            		formatColors = [];
            	
            	Ext.Array.each (colors, function (color) {
            		if (me.hexColorRe.test(color))
            		{
            			formatColors.push('#' + color);
            		}
            		else
            		{
            			formatColors.push(color);
            		}
            	});
            	return formatColors;
            },
            
            handleClick: function(event) {
                var me = this,
                    colorIndex,
                    color;
                event.stopEvent();
                
                if (!me.disabled) {
                	colorIndex = event.currentTarget.className.match(me.colorRe)[1];
                	color = me.colors[colorIndex - 1];
                    me.select(color);
                }
            },
            
            select : function(color, suppressEvent){

                var me = this,
                    selectedCls = me.selectedCls,
                    value = me.value,
                    el, item;

                if (!me.rendered) {
                    me.value = color;
                    return;
                }

                if (color !== value || me.allowReselect) {
                    el = me.el;

                    if (me.value) {
                    	item = el.down('a.' + selectedCls, true);
                    	if (!item)
                    	{
                    		var index = Ext.Array.indexOf(me.colors, me.value);
                    		item = el.down('a.a-color-' + (index + 1), true);
                    	}
                        Ext.fly(item).removeCls(selectedCls);
                    }
                    
                    var index = Ext.Array.indexOf(me.colors, color);
            		item = el.down('a.a-color-' + (index + 1), true);
            		if (item)
            		{
            			Ext.fly(item).addCls(selectedCls);
            		}
                    me.value = color;
                    if (suppressEvent !== true) {
                        me.fireEvent('select', me, color);
                    }
                }
            },
            
            clear: function(){
                var me = this,
                    value = me.value,
                    el;
                    
                if (value && me.rendered) {
                	var index = Ext.Array.indexOf(me.colors, value);
                	el = me.el.down('a.' + me.selectedCls, true);
                	if (el)
                	{
                		Ext.fly(el).removeCls(me.selectedCls);
                	}
                }
                me.value = null;  
            }
	    });    
})();

(function()
{
    Ext.override(Ext.String, {
        
        /**
         * Escape the characters of the String using HTML entities. 
         * Prevent JavaScript injection (when string is rendered in HTML context).
         * @param {String} s The untrusted string
         * @return {String} The escaped string
         */
        escapeHtml: function(s)
        {
            if (!Ext.isString(s))
            {
                return s;
            }
            
            return s.replace(/&/g, "&amp;")
                    .replace(/</g, "&lt;");
        },
        
        /**
         * Convert the stacktrace of an exception to a readable HTML string.
         * @param {String/Error} stack The exception or the exception stacktrace.
         * @param {Number} [linesToRemove=0] The number of items to remove from stack. Depending on your stack you may know that the X first element are always the same
         * @return {String} A HTML string 
         */
        stacktraceToHTML: function(stack, linesToRemove)
        {
            linesToRemove = linesToRemove || 0;
            
            if (!stack)
            {
            	return "";
            }
            
            if (!Ext.isString(stack))
            {
                stack = stack.stack;
            }
            
            var stack2 = stack.replace(/\r?\n/g, "<br/>");
            
            if (stack2.substring(0,5) == "Error")
            {
                linesToRemove++;
            }
            
            for (var i = 0; i < linesToRemove; i ++)
            {
                stack2 = stack2.substring(stack2.indexOf("<br/>") + 5);
            }
            
            var currentUrl = (document.location.origin || document.location.href.replace(new RegExp("^(https?://[^/]*(:[0-9]*)?)(/.*)?$"), "$1")) + Ametys.CONTEXT_PATH;
            var stack3 = "";
            Ext.each(stack2.split('<br/>'), function(node, index) 
                    {
                        // Firefox
                        node = node.replace(/^([^@]*)@(.*) line ([0-9]*) > Function:([0-9]*):([0-9]*)$/, "<span class='method'>$1</span> (<a class='filename' href='$2' target='_blank' title='$2 ($3 > Function $4:$5)'>$2</a>:<span class='line'>$3</span> > Function <span class='line'>$4</span>:<span class='line'>$5</span>)");
                        node = node.replace(/^([^@]*)@(.*):([0-9]*):([0-9]*)$/, "<span class='method'>$1</span> (<a class='filename' href='$2' target='_blank' title='$2 ($3:$4)'>$2</a>:<span class='line'>$3</span>:<span class='line'>$4</span>)");
                        node = node.replace(/^([^@]*)@(.*):([0-9]*)$/, "<span class='method'>$1</span> (<a class='filename' href='$2' target='_blank' title='$2 ($3)'>$2</a>:<span class='line'>$3</span>)");
                
                        // IE - Chrome
                        node = node.replace(/^.*at (.*) \((.*):([0-9]*):([0-9]*)\).*$/, "<span class='method'>$1</span> (<a class='filename' href='$2' target='_blank' title='$2 ($3:$4)'>$2</a>:<span class='line'>$3</span>:<span class='line'>$4</span>)");
                        node = node.replace(/^.*at (.*):([0-9]*):([0-9]*).*$/, "<a class='filename' href='$1' target='_blank' title='$1 ($2:$3)'>$1</a>:<span class='line'>$2</span>:<span class='line'>$3</span>");
                        node = node.replace(/^.*at (.*) \((.*)\)$/, "<span class='method'>$1</span> (<a class='filename' href='$2' target='_blank' title='$2'>$2</a>)");
                        
                        stack3 += node.replace(new RegExp("([^'])" + currentUrl, "g"), "$1") + "<br/>"; // removing http://xxx except in the tooltip
                    }
            );
            return "<div class='callstack'>" + stack3.substring(0, stack3.length - 5) + "</div>"; // remove last <br/>
        },
        
/**
         * Format a JAVA callstack to a HTML string
         * @param {String} stack The callstack
         * @param {String} [regExpPrefix="\\s*at "] The regexp that defined fixed begin of lines with java call
         * @param {String} [transformedPrefix="<span style='margin-left: 15px'></span>at "] The replaced version of the lines begin 
         * @return {String} the formated string
         */
        stacktraceJavaToHTML: function(stack, regExpPrefix, transformedPrefix)
        {
            regExpPrefix = regExpPrefix === undefined ? "\\s*at " : regExpPrefix;
            transformedPrefix = transformedPrefix === undefined ? "<span style='margin-left: 15px'></span>at " : transformedPrefix;
            
            let hardPrefix = "TODO##";
            let causedBySeparator = "<em>Caused by:</em>";
            
            var stack2 = stack.replace(/\r?\n/g, "<br/>");
            var stack3 = "";
            Ext.each(stack2.split('<br/>'), function(node, index) 
                {
                    node = Ext.String.htmlEncode(node); 
                    
                    node = node.replace(/^(.*)$/, hardPrefix + "$1"); // Adding a hard prefix to avoid multiple match
                    
                    node = node.replace(new RegExp("^" + hardPrefix + "Caused by:(.*)$"), causedBySeparator + "$1");
                    node = node.replace(new RegExp("^" + hardPrefix + regExpPrefix + "(org\\.ametys\\..*)\\.([^\\.]+)\\((.*):([0-9-]*)\\)$"), "<span class='ametyscallstack'>" + transformedPrefix + "<span class='filename'>$1</span><span class='method'>#$2</span>:<span class='line'>$4</span></span>"); // highlight methods with files and line in "at" lines
                    node = node.replace(new RegExp("^" + hardPrefix + regExpPrefix + "(.*)\\.([^\\.]+)\\((.*):([0-9-]*)\\)$"), "<span class='notametyscallstack'>" + transformedPrefix + "<span class='filename'>$1</span><span class='method'>#$2</span>:<span class='line'>$4</span></span>"); // highlight methods with files and line in "at" lines
                    node = node.replace(new RegExp("^" + hardPrefix + regExpPrefix + "(.*)\\.([^\\.]+)\\(([^:]*)\\)$"), "<span class='notametyscallstack'>" + transformedPrefix + "<span class='filename'>$1</span><span class='method'>#$2</span> ($3)</span>"); // hightlight "at" method event if no file or line is specified.
    
                    node = node.replace(new RegExp("^" + hardPrefix + regExpPrefix + "(.*)$"), "<span class='notametyscallstack'><span style='margin-left: 15px'></span>at $1</span>"); // indent "at" lines if previous rules did not matched.
                    
                    node = node.replace(new RegExp("^" + hardPrefix + "(.*)$"), "$1"); // Removing the hard prefix on unmatched lines
    
                    stack3 += node + "<br/>";
                }
            );
            
            var stack4 = stack3.substring(0, stack3.length - "<br/>".length);
            
            var stacks5 = stack4.split(causedBySeparator);
            
            var stack6 = "";
            if (stacks5.length > 1)
            {
                stacks5.forEach(function(s, index, array) {
                    stack6 += "<input type='checkbox'" + (index == array.length - 1 ? " checked='checked'" : "") +  "/><div class='stackpart'>" + (index != 0 ? causedBySeparator : "") + s + "</div>"
                });
            }
            else
            {
                stack6 = stack4
            }
            
            return "<div class='callstack'>" + stack6 + "</div>"; // remove last <br/>
        }        
    });    
})();

(function()
{
    Ext.override(Ext.tree.View, {
    	toggleOnDblClick: false
    });
})();

(function()
{
    Ext.override(Ext.util.Format, {
        /**
         * @member Ext.util.Format
         * @method duration
         * @since Ametys-Runtime-4.0
         * @ametys
         * Simple format for a duration
         * @param {Number} duration The duration in milliseconds
         * @return {String} The formatted duration
         */
        duration: (function()
        {
            function _leading0(t)
            {
                if (t < 10)
                {
                    return "0" + t;
                }
                else
                {
                    return "" + t;
                }
            }
            
            var millisLimit = 1000,
                secLimit = 60 * millisLimit,
                minuteLimit = 60 * secLimit,
                hourLimit = 24 * minuteLimit;
            
            return function(duration)
            {
                if (!isFinite(duration))
                {
                    return (duration > 0 ? "": "-") + "∞"
                }
                else if (duration < millisLimit)
                {
                    return duration + " " + "{{i18n PLUGINS_CORE_UI_DURATION_FORMAT_MILLISECONDS}}";
                }
                else if (duration < secLimit)
                {
                    return (duration/1000).toString().replace(/\./, "{{i18n PLUGINS_CORE_UI_DURATION_FORMAT_DECIMAL_SEPARATOR}}") + " " + "{{i18n PLUGINS_CORE_UI_DURATION_FORMAT_SECONDS}}";
                }
                else if (duration < minuteLimit)
                {
                    var minutes = Math.floor(duration / 1000 / 60),
                        milliseconds = duration - minutes * 60 * 1000;
                    return minutes + " " + "{{i18n PLUGINS_CORE_UI_DURATION_FORMAT_MINUTES}}" + " " + _leading0(Math.floor(milliseconds / 1000)) + " " + "{{i18n PLUGINS_CORE_UI_DURATION_FORMAT_SECONDS}}";
                }
                else if (duration < hourLimit)
                {
                    var hours = Math.floor(duration / 1000 / 60 / 60),
                        milliseconds = duration - hours * 60 * 60 * 1000;
                    return hours + " " + "{{i18n PLUGINS_CORE_UI_DURATION_FORMAT_HOURS}}" + " " + _leading0(Math.floor(milliseconds / 1000 / 60)) + " " + "{{i18n PLUGINS_CORE_UI_DURATION_FORMAT_MINUTES}}";
                }
                else
                {
                    var totalHours = Math.floor(duration / 1000 / 60 / 60),
                        days = Math.floor(totalHours / 24),
                        hours = totalHours - days * 24,
                        milliseconds = duration - totalHours * 60 * 60 * 1000;
                    return days + " " + "{{i18n PLUGINS_CORE_UI_DURATION_FORMAT_DAYS}}" + " " + hours + " " + "{{i18n PLUGINS_CORE_UI_DURATION_FORMAT_HOURS}}" + " " + Math.floor(milliseconds / 1000 / 60) + " " + "{{i18n PLUGINS_CORE_UI_DURATION_FORMAT_MINUTES}}";
                }
            }
        })()
    });
})();

(function()
{
    Ext.override(Ext.layout.container.Box, {
        updateVertical: function(vertical)
        {
            this.callParent(arguments);

            this.owner && this.owner.rendered && this.owner.updateLayout();
        }
    });
})();

(function()
{
	Ext.override(Ext.view.View, {
		onItemKeyDown: function(record, node, index, e, eOpts) {
			var parentDialog = this.findParentByType('dialog');
    		if (e.getKey() == e.ENTER && parentDialog)
    		{
    			parentDialog.body.fireEvent('keydown', e, parentDialog.body, eOpts);
			}
		}
	});
})();
	
(function()
{
    /**
     * @member Ext.panel.Table
     * @since Ametys-Runtime-4.1
     * @ametys
     * @cfg {Number} showColumnMenuFilteringLimit 
     * The limit number when displaying a filter on the columns in the menu to show/hide the columns of the grid.
     * When the number of hideable columns is equal or greater to this number, the filter will display.
     * Null (or equal or less than 0) to never display a filter in the column menu.
     */
    /**
     * @member Ext.panel.Table
     * @since Ametys-Runtime-4.2
     * @ametys
     * @cfg {Boolean} showColumnMenuSortOption 
     * True to display an option for 'alphabetic order sort' in the menu of columns (only if the filtering field is also present).
     */
    
	Ext.override(Ext.grid.header.Container, {
	    
	    statics: {
	        /**
	         * @readonly
	         * @private
	         * @property {String} _FILTER_ITEM_ID the id of column filter item
	         */
            _FILTER_ITEM_ID: 'filter-columns-textfield',
            /**
             * @readonly
             * @private
             * @property {String} _SORT_COLUMN_ITEM_ID the id of column sort item
             */
            _SORT_COLUMN_ITEM_ID: 'sort-columns-button',
            /**
             * @readonly
             * @private
             * @property {String} _FILTER_ITEM_ID the id of column selector item
             */
            _SELECT_COLUMN_ITEM_ID: 'select-columns-button',
            /**
             * @readonly
             * @private
             * @property {String} _GROUP_COLUMN_ITEM_ID the id of column group item
             */
            _GROUP_COLUMN_ITEM_ID: 'group-columns-button',
	    },
	    
	    constructor: function(config) {
	        this.callParent(arguments);
            this.on("columnhide", this._onColumnHideOrShow, this);
            this.on("columnshow", this._onColumnHideOrShow, this);
	        this._itemsWhichStayOnTop = [this.statics()._FILTER_ITEM_ID, this.statics()._SORT_COLUMN_ITEM_ID, this.statics()._SELECT_COLUMN_ITEM_ID, this.statics()._GROUP_COLUMN_ITEM_ID]
	    },
	    
        getColumnMenu: function(headerContainer) {
            var menuItems = this.callParent(arguments),
                grid = this.ownerCt,
                filterFeatureEnabled = grid.showColumnMenuFilteringLimit != null && grid.showColumnMenuFilteringLimit > 0 && menuItems.length > grid.showColumnMenuFilteringLimit,
                sortFeatureEnabled = filterFeatureEnabled && grid.showColumnMenuSortOption === true,
                itemsToInsert = [];
            var me = this;
                
            if (menuItems != null && sortFeatureEnabled)
            {
                Ext.Array.forEach(menuItems, function(colMenuItem, index) {
                    colMenuItem['_initialOrder'] = index;
                });

                var sortId = this.statics()._SORT_COLUMN_ITEM_ID;
                var groupId = this.statics()._GROUP_COLUMN_ITEM_ID;
                itemsToInsert.push(Ext.create('Ext.menu.CheckItem', {
                    itemId: this.statics()._SORT_COLUMN_ITEM_ID,
                    text: "{{i18n PLUGINS_CORE_UI_GRID_HEADER_COLUMN_MENU_SORT_BUTTON}}",
                    hideOnClick: false,
                    listeners: {
                        click: {
                            fn: function(item, event, eOpts)
                            {
                                Ext.suspendLayouts();
                                try
                                {
                                    groupComponent = item.container.component.getComponent(groupId)
                                    
                                    item.parentMenu.items.sortBy(me._getSortFn(item.checked, groupComponent.checked));
                                    item.parentMenu.updateLayout();
                                }
                                finally
                                {
                                    Ext.resumeLayouts(true);
                                }
                            }
                        }
                    }
                }));
                
                itemsToInsert.push(Ext.create('Ext.menu.CheckItem', {
                    itemId: this.statics()._GROUP_COLUMN_ITEM_ID,
                    text: "{{i18n PLUGINS_CORE_UI_GRID_HEADER_COLUMN_MENU_GROUP_CHECKED_COLUMNS}}",
                    hideOnClick: false,
                    listeners: {
                        click: {
                            fn: function(item, event, eOpts)
                            {
                                Ext.suspendLayouts();
                                try
                                {
                                    sortComponent = item.container.component.getComponent(sortId)
                                    
                                    item.parentMenu.items.sortBy(me._getSortFn(sortComponent.checked, item.checked));
                                    item.parentMenu.updateLayout();
                                }
                                finally
                                {
                                    Ext.resumeLayouts(true);
                                }
                            }
                        }
                    }
                }));
                
            }
            
            if (menuItems != null && filterFeatureEnabled)
            {
                Ext.Array.forEach(menuItems, function(colMenuItem) {
                    colMenuItem['_isColumn'] = true;
                });
                
                itemsToInsert.push(Ext.create('Ext.form.field.Text', {
                    itemId: this.statics()._FILTER_ITEM_ID,
                    emptyText: "{{i18n PLUGINS_CORE_UI_GRID_HEADER_COLUMN_MENU_SEARCH_COLUMNS_EMPTY_TEXT}}",
                    hideEmptyLabel: false,
                    labelClsExtra: Ext.baseCSSPrefix + 'grid-filters-icon ' + Ext.baseCSSPrefix + 'grid-filters-find',
                    labelSeparator: '',
                    labelWidth: 29,
                    margin: 0,
                    selectOnFocus: true,
                    listeners: {
                        'change': Ext.Function.createBuffered(function(f, nv, ov) {this._filterColumns(f, nv, ov, menuItems)}, 500, this)
                    }
                }));
            }
            
            if (itemsToInsert.length)
            {
                itemsToInsert.push(Ext.create('Ext.menu.Separator', {}));

                if (menuItems != null && sortFeatureEnabled)
                {

                    itemsToInsert.push(Ext.create('Ext.menu.CheckItem', {
                        itemId: this.statics()._SELECT_COLUMN_ITEM_ID,
                        text: "{{i18n PLUGINS_CORE_UI_GRID_HEADER_COLUMN_MENU_SELECT_ALL_BUTTON}}",
                        hideOnClick: false,
                        listeners: {
                            click: {
                                fn: function(item)
                                {
                                    Ext.suspendLayouts();
                                    try
                                    {
                                        var firstCheckedItem;
                                        var selectAll = item.checked;
                                        var firstCheckedItem;

                                        // Skip the first checked item on unselect, as we always need at least one active column.
                                        // First check if any column hidden by filter is check
                                        Ext.Array.forEach(menuItems, function(menuItem) {
                                            if(!Ext.Array.contains(me._itemsWhichStayOnTop, menuItem.itemId) && menuItem.checked != null)
                                            {
                                                if(menuItem.checked && firstCheckedItem == null && menuItem.hidden)
                                                {
                                                    firstCheckedItem = menuItem;
                                                }
                                            }
                                        });
                                        
                                        Ext.Array.forEach(menuItems, function(menuItem) {
                                            if(!Ext.Array.contains(me._itemsWhichStayOnTop, menuItem.itemId) && menuItem.checked != null)
                                            {
                                                // If there isn't any hidden checked column, get the first checked item
                                                if(menuItem.checked && firstCheckedItem == null)
                                                {
                                                    firstCheckedItem = menuItem;
                                                }
                                                
                                                if(menuItem != firstCheckedItem && !menuItem.hidden)
                                                {
                                                    menuItem.setChecked(selectAll)
                                                }
                                            }
                                        });
                                        
                                        item.parentMenu.updateLayout();
                                    }
                                    finally
                                    {
                                        Ext.resumeLayouts(true);
                                    }
                                    
                                }
                            }
                        }
                        
                    }));
                }
                Ext.Array.insert(menuItems, 0, itemsToInsert);
            }
            return menuItems;
        },

        /**
         * @private
         * @method _getSortFn
         * @since Ametys-Runtime-4.4
         * @ametys
         * Given parameters, sort columns.
         * @param {Boolean} alphabetic If true, sort columns by alphabetic order
         * @param {Boolean} groupByChecked If true, group checked and unchecked columns in two different groups
         * @return {Function} The sort function
         */
        _getSortFn: function(alphabetic, groupByChecked)
        {
            var me = this;
            
            function isTop(itemId, id)
            {
                return Ext.Array.contains(me._itemsWhichStayOnTop, itemId) || id.startsWith("menuseparator");
            }
            
            var propertyForSort = alphabetic ? 'text' : '_initialOrder';
            return function sortFn(item1, item2)
            {
                var a = item1.itemId,
                    b = item2.itemId;
                if (isTop(a, item1.id) && isTop(b, item2.id))
                {
                    // Keep former order between those two
                    return 0;
                }
                else if (isTop(a, item1.id))
                {
                    // item1 (stays on top) < item2
                    return -1;
                }
                else if (isTop(b, item2.id))
                {
                    // item1 > item2 (stays on top)
                    return 1;
                }
                else if (groupByChecked && item1.checked && !item2.checked)
                {
                    // item1 (checked) < item2
                    return -1;
                }
                else if (groupByChecked && !item1.checked && item2.checked)
                {
                    // item1 > item2 (checked)
                    return 1;
                }
                else
                {
                    // compare the [text | initialOrder] of the two columns
                    return Ext.data.SortTypes.asNonAccentedUCString(item1[propertyForSort]) < Ext.data.SortTypes.asNonAccentedUCString(item2[propertyForSort])
                        ? -1 : 1;
                }
            }
        },
        
        /**
         * @private
         * @member Ext.grid.header.Container
         * @method _filterColumns
         * @since Ametys-Runtime-4.1
         * @ametys
         * Given the input in the filter textfield, show/hide columns check items of the column menu.
         * @param {Ext.form.field.Text} field The textfield
         * @param {Object} newValue The new value of the filter textfield
         * @param {Object} oldValue The original value of the filter textfield
         * @param {Array} menuItems The items of the column menu (including the textfield, separators, ...)
         */
        _filterColumns: function(field, newValue, oldValue, menuItems)
        {
            function formatValue(s)
            {
                return Ext.util.Format.lowercase(Ext.String.deemphasize(Ext.String.trim(s)));
            }
            
            var fNewValue = formatValue(newValue);
            if (fNewValue == field._filterValue)
            {
                // No real change, do nothing
                return;
            }
            field._filterValue = fNewValue;
            
            Ext.suspendLayouts();
            try
            {
                if (fNewValue == '')
                {
                    // Clear filter
                    Ext.Array.forEach(menuItems, function(menuItem) {
                        if (menuItem._isColumn)
                        {
                            menuItem.show();
                        }
                    });
                }
                else
                {
                    // Filter..
                    Ext.Array.forEach(menuItems, function(menuItem) {
                        if (menuItem._isColumn)
                        {
                            menuItem[formatValue(menuItem.text).includes(fNewValue) ? 'show' : 'hide']();
                        }
                    });
                }
            }
            finally
            {
                selectAllComponent = field.container.component.getComponent(this.statics()._SELECT_COLUMN_ITEM_ID)
                if (selectAllComponent != null)
                {
                    this._refreshSelect(menuItems, selectAllComponent)
                }
                Ext.resumeLayouts(true);
            }
        },
        

        _refreshSelect: function(menuItems, selectAllComponent)
        {
            var selectAll = false
            Ext.Array.forEach(menuItems, function(menuItem) {
                if(!Ext.Array.contains(this._itemsWhichStayOnTop, menuItem.itemId) && menuItem.checked != null && !menuItem.hidden)
                {
                    selectAll = !menuItem.checked || selectAll;
                }
            }, this);
            selectAllComponent.setChecked(!selectAll)
            selectAllComponent.parentMenu.updateLayout();
        },
        
        // Override those two methods: #getMenu and #beforeMenuShow (because these two are able to creating the columnMenu and having a reference to the menu)
        // in order to set focus on filter textfield (if it exists) when activating columnMenu
        getMenu: function()
        {
            var menu = this.callParent(arguments);
            this._addListenerToColumnMenu(menu);
            
            return menu;
        },
        
        beforeMenuShow: function(menu)
        {
            this.callParent(arguments);
            this._addListenerToColumnMenu(menu);
        },
        
        /**
         * @private
         * @member Ext.grid.header.Container
         * @method _addListenerToColumnMenu
         * @since Ametys-Runtime-4.1
         * @ametys
         * This method tries to add a 'activate' listener on the columnItem object (only if the listener does not already exist)
         * to focus the textfield.
         * @param {Ext.menu.Menu} menu The header menu
         */
        _addListenerToColumnMenu: function(menu)
        {
            var columnItem = menu.child('#columnItem');
            if (columnItem)
            {
                var textfield = columnItem.menu.items.get('filter-columns-textfield');
                if (textfield && !textfield._hasListenerToFocus)
                {
                    columnItem.onAfter('activate', function(columnItem) {
                        var pos = textfield.getValue().length; // the selectedText arg of #focus() seems bugged when passing `false`..
                        textfield.focus([pos, pos], 500);
                    });
                    textfield._hasListenerToFocus = true;
                }
            }
        },
        
        _onColumnHideOrShow: function()
        {
            if (this.menu && this.menu.getComponent('columnItem'))
            {
                var columnItem = this.menu.getComponent('columnItem').menu
                var selectAllComponent = columnItem.getComponent(this.statics()._SELECT_COLUMN_ITEM_ID)
                var sortComponent = columnItem.getComponent(this.statics()._SORT_COLUMN_ITEM_ID)
                var groupComponent = columnItem.getComponent(this.statics()._GROUP_COLUMN_ITEM_ID)
                var menuItems = columnItem.items.items
                
                if (selectAllComponent)
                {
                    this._refreshSelect(menuItems, selectAllComponent)
                }
                
                if (sortComponent != null && groupComponent != null)
                {
                    sortComponent.parentMenu.items.sortBy(this._getSortFn(sortComponent.checked, groupComponent.checked));
                    sortComponent.parentMenu.updateLayout();
                }
                
            }
        }
        
    });
})();

(function()
{
    /**
     * @member Ext.grid.filters.filter.List
     * @since Ametys-Runtime-4.6
     * @ametys
     * @cfg {Number} showFilteringLimit 
     * The limit number when displaying a filter on the filter items to show/hide them.
     * When the number of filter items is equal or greater to this number, a search input will be displayed.
     */
    
    Ext.override(Ext.grid.filters.filter.List, {
        createMenuItems: function(store) {
	        
            this.callParent(arguments);
            
            var me = this,
	            menu = me.menu,
	            len = store.getCount(),
	            filterFeatureEnabled = this.showFilteringLimit != null && this.showFilteringLimit > 0 && len > this.showFilteringLimit;
            
	        if (len && menu && filterFeatureEnabled) {
	            menu.suspendLayouts();
	            
                menu.insert(0, Ext.create('Ext.menu.Separator', {}));
                
                // Insert search input at first position
                menu.insert(0, Ext.create('Ext.form.field.Text', {
                    emptyText: "{{i18n PLUGINS_CORE_UI_GRID_HEADER_COLUMN_MENU_SEARCH_COLUMNS_EMPTY_TEXT}}",
                    hideEmptyLabel: false,
                    labelClsExtra: Ext.baseCSSPrefix + 'grid-filters-icon ' + Ext.baseCSSPrefix + 'grid-filters-find',
                    labelSeparator: '',
                    labelWidth: 29,
                    margin: '2 3 0 0',
                    selectOnFocus: true,
                    listeners: {
                        'change': Ext.Function.createBuffered(function(f, nv, ov) {this._filterList(f, nv, ov, menu)}, 500, this)
                    }
                }));
                
	            menu.resumeLayouts(true);
	        }
	    },
        
        /**
         * @private
         * @member Ext.grid.filters.filter.List
         * @method _filterList
         * @since Ametys-Runtime-4.6
         * @ametys
         * Given the input in the filter textfield, show/hide filters items of the filter menu.
         * @param {Ext.form.field.Text} field The textfield
         * @param {Object} newValue The new value of the filter textfield
         * @param {Object} oldValue The original value of the filter textfield
         * @param {Object[]} menu The filter menu items
         */
        _filterList: function(field, newValue, oldValue, menu)
        {
            function formatValue(s)
            {
                return Ext.util.Format.lowercase(Ext.String.deemphasize(Ext.String.trim(s)));
            }
            
            var fNewValue = formatValue(newValue);
            if (fNewValue == field._filterValue)
            {
                // No real change, do nothing
                return;
            }
            field._filterValue = fNewValue;
            
            if (fNewValue == '')
            {
                // Clear filter
                menu.items.each(function(menuItem) {
                    menuItem.show()
                });
            }
            else
            {
                // Filter..
                menu.items.each(function(menuItem) {
                    if (menuItem instanceof Ext.menu.CheckItem)
                    {
                        menuItem[formatValue(menuItem.text).includes(fNewValue) ? 'show' : 'hide']();
                    }
                });
            }
        }
    });
})();

(function(){
    Ext.define("Ametys.util.Format", {
        override: 'Ext.util.Format',
        
        fileSize: (function() {
            var byteLimit = 1024,
                kbLimit = 1048576,
                mbLimit = 1073741824,
                gbLimit = 1099511627776;
                
            return function(size) {
                var out;

                if (size === 1)
                {
                    out = Ext.String.format("{{i18n PLUGINS_CORE_UI_FORMAT_FILE_SIZE_BYTE}}", size);
                }
                else if (size < byteLimit) {
                    out = Ext.String.format("{{i18n PLUGINS_CORE_UI_FORMAT_FILE_SIZE_BYTES}}", size);
                }
                else if (size < kbLimit) {
                    out = Ext.String.format("{{i18n PLUGINS_CORE_UI_FORMAT_FILE_SIZE_KB}}", Math.round(((size * 10) / byteLimit)) / 10);
                }
                else if (size < mbLimit) {
                    out = Ext.String.format("{{i18n PLUGINS_CORE_UI_FORMAT_FILE_SIZE_MB}}", Math.round(((size * 10) / kbLimit)) / 10);
                }
                else if (size < gbLimit) {
                    out = Ext.String.format("{{i18n PLUGINS_CORE_UI_FORMAT_FILE_SIZE_GB}}", Math.round(((size * 10) / mbLimit)) / 10);
                }
                else {
                    out = Ext.String.format("{{i18n PLUGINS_CORE_UI_FORMAT_FILE_SIZE_TB}}", Math.round(((size * 10) / gbLimit)) / 10);
                }
                
                return out;
            };
        })()
    });
})();

(function(){
    Ext.define("Ametys.data.request.Ajax", {
        override: 'Ext.data.request.Ajax',
        
        openRequest: function(options, requestOptions, isAsync, username, password)
        {
            let xhr = this.callParent(arguments);
            
            xhr.upload.addEventListener("progress", Ext.bind(this._progressHandler, this), false);
            
            return xhr;
        },
        
        /**
         * @event progress
         * Fires to handle progress.
         * @param {Ext.data.Connection} conn This Connection object.
         * @param {Object} options The options config object passed to the {@link #request} method.
         */
        
        /**
         * @private
         * @member Ext.data.request.Ajax
         * @method _progressHandler
         * @since Ametys-Runtime-4.8
         * @ametys
         * Fire event of progress handler.
         * 
         * @param {Object} event The event of progress.
         */
        _progressHandler: function(event) 
        {
            if (this.owner.hasListeners.progress) 
            {
                this.owner.fireEvent('progress', this.owner, this.options);
            }
            
            if (this.options.progress) 
            {
                let percent = event.loaded / event.total;
                Ext.callback(this.options.progress, this.options.scope, [percent, this.options]);
            }
        }
    });
})();

(function(){
    Ext.define("Ametys.view.BoundListKeyNav", {
        override: 'Ext.view.BoundListKeyNav',
        
        /**
         * @member Ext.view.BoundListKeyNav
         * @cfg {Boolean} enterCanDeselect When true (default value), the Enter key will select and unselect. When false, the return key will only select.
         * @since Ametys-Runtime-4.8
         * @ametys
         */
        
        selectHighlighted: function(e) {
            var me = this,
                boundList = me.view,
                selModel = boundList.getSelectionModel(),
                highlightedRec,
                highlightedPosition = me.recordIndex;
    
            // If all options have been filtered out, then do NOT add most recently highlighted.
            if (boundList.all.getCount()) {
                highlightedRec = me.getRecord();
    
                if (highlightedRec) {
                    // Select if not already selected.
                    // If already selected, selecting with no CTRL flag will deselect the record.
                    if (e.getKey() === e.ENTER 
                            && this.view.enterCanDeselect !== false // <-------------------------- FIX HERE 
                        || !selModel.isSelected(highlightedRec)) {
                        selModel.selectWithEvent(highlightedRec, e);
    
                        // If the result of that selection is that the record is removed
                        // or filtered out, jump to the next one.
                        if (!boundList.store.data.contains(highlightedRec)) {
                            me.setPosition(
                                Math.min(highlightedPosition, boundList.store.getCount() - 1)
                            );
                        }
                    }
                }
            }
        },        
    });
})();

(function() {
    // Adding grid tooltips
    function onmousemove(event, target) 
    {
        // Is mouse cursor over a grid cell?
        if (target.matches("td.x-grid-cell *"))
        {
            // Check if "..." are displayed
            let cell = target.closest("td.x-grid-cell *");
            
            if (!supportsAutoQtip(cell))
            {
                return;
            }
            
            if (cell.offsetWidth < cell.scrollWidth) // Only support 1 line ellipsis
            {
                cell.setAttribute("title", cell.textContent);
                cell.setAttribute("data-auto-qtip", "true");
            } 
            else
            {
                // Remove existing that do not apply anymore
                cell.removeAttribute("title");
                cell.removeAttribute("data-auto-qtip");
            }
        }
    }
    
    function supportsAutoQtip(cell)
    {
        if (cell.getAttribute("data-auto-qtip") == "true")
        {
            // Already computed means the title is ours
            return true;
        }
        else if (cell.getAttribute("title") !== null
                || cell.getAttribute("data-qtip") !== null
                || cell.querySelector("*[title]")
                || cell.querySelector("*[data-qtip]"))
        {
            // Alreay a manual title somewhere
            return false;
        }
        return true;
    }
    
    Ext.get(document).on('mousemove', onmousemove);
})();

// Avoid messages about closable tabs
Ext.ariaWarn = Ext.emptyFn;