diff options
Diffstat (limited to 'deluge/ui/web/js/extjs/ext-extensions')
20 files changed, 2927 insertions, 0 deletions
diff --git a/deluge/ui/web/js/extjs/ext-extensions/JSLoader.js b/deluge/ui/web/js/extjs/ext-extensions/JSLoader.js new file mode 100644 index 0000000..e4691b0 --- /dev/null +++ b/deluge/ui/web/js/extjs/ext-extensions/JSLoader.js @@ -0,0 +1,40 @@ +Ext.ux.JSLoader = function (options) { + Ext.ux.JSLoader.scripts[++Ext.ux.JSLoader.index] = { + url: options.url, + success: true, + jsLoadObj: null, + options: options, + onLoad: options.onLoad || Ext.emptyFn, + onError: options.onError || Ext.ux.JSLoader.stdError, + scope: options.scope || this, + }; + + Ext.Ajax.request({ + url: options.url, + scriptIndex: Ext.ux.JSLoader.index, + success: function (response, options) { + var script = Ext.ux.JSLoader.scripts[options.scriptIndex]; + try { + eval(response.responseText); + } catch (e) { + script.success = false; + script.onError(script.options, e); + } + if (script.success) { + script.onLoad.call(script.scope, script.options); + } + }, + failure: function (response, options) { + var script = Ext.ux.JSLoader.scripts[options.scriptIndex]; + script.success = false; + script.onError(script.options, response.status); + }, + }); +}; +Ext.ux.JSLoader.index = 0; +Ext.ux.JSLoader.scripts = []; +Ext.ux.JSLoader.stdError = function (options, e) { + window.alert( + 'Error loading script:\n\n' + options.url + '\n\nstatus: ' + e + ); +}; diff --git a/deluge/ui/web/js/extjs/ext-extensions/Spinner.js b/deluge/ui/web/js/extjs/ext-extensions/Spinner.js new file mode 100644 index 0000000..ccef895 --- /dev/null +++ b/deluge/ui/web/js/extjs/ext-extensions/Spinner.js @@ -0,0 +1,474 @@ +/** + * Ext JS Library 3.4.0 + * Copyright(c) 2006-2011 Sencha Inc. + * licensing@sencha.com + * http://www.sencha.com/license + */ +/** + * @class Ext.ux.Spinner + * @extends Ext.util.Observable + * Creates a Spinner control utilized by Ext.ux.form.SpinnerField + */ +Ext.ux.Spinner = Ext.extend(Ext.util.Observable, { + incrementValue: 1, + alternateIncrementValue: 5, + triggerClass: 'x-form-spinner-trigger', + splitterClass: 'x-form-spinner-splitter', + alternateKey: Ext.EventObject.shiftKey, + defaultValue: 0, + accelerate: false, + + constructor: function (config) { + Ext.ux.Spinner.superclass.constructor.call(this, config); + Ext.apply(this, config); + this.mimicing = false; + }, + + init: function (field) { + this.field = field; + + field.afterMethod('onRender', this.doRender, this); + field.afterMethod('onEnable', this.doEnable, this); + field.afterMethod('onDisable', this.doDisable, this); + field.afterMethod('afterRender', this.doAfterRender, this); + field.afterMethod('onResize', this.doResize, this); + field.afterMethod('onFocus', this.doFocus, this); + field.beforeMethod('onDestroy', this.doDestroy, this); + }, + + doRender: function (ct, position) { + var el = (this.el = this.field.getEl()); + var f = this.field; + + if (!f.wrap) { + f.wrap = this.wrap = el.wrap({ + cls: 'x-form-field-wrap', + }); + } else { + this.wrap = f.wrap.addClass('x-form-field-wrap'); + } + + this.trigger = this.wrap.createChild({ + tag: 'img', + src: Ext.BLANK_IMAGE_URL, + cls: 'x-form-trigger ' + this.triggerClass, + }); + + if (!f.width) { + this.wrap.setWidth(el.getWidth() + this.trigger.getWidth()); + } + + this.splitter = this.wrap.createChild({ + tag: 'div', + cls: this.splitterClass, + style: 'width:13px; height:2px;', + }); + this.splitter + .setRight(Ext.isIE ? 1 : 2) + .setTop(10) + .show(); + + this.proxy = this.trigger.createProxy('', this.splitter, true); + this.proxy.addClass('x-form-spinner-proxy'); + this.proxy.setStyle('left', '0px'); + this.proxy.setSize(14, 1); + this.proxy.hide(); + this.dd = new Ext.dd.DDProxy(this.splitter.dom.id, 'SpinnerDrag', { + dragElId: this.proxy.id, + }); + + this.initTrigger(); + this.initSpinner(); + }, + + doAfterRender: function () { + var y; + if (Ext.isIE && this.el.getY() != (y = this.trigger.getY())) { + this.el.position(); + this.el.setY(y); + } + }, + + doEnable: function () { + if (this.wrap) { + this.disabled = false; + this.wrap.removeClass(this.field.disabledClass); + } + }, + + doDisable: function () { + if (this.wrap) { + this.disabled = true; + this.wrap.addClass(this.field.disabledClass); + this.el.removeClass(this.field.disabledClass); + } + }, + + doResize: function (w, h) { + if (typeof w == 'number') { + this.el.setWidth(w - this.trigger.getWidth()); + } + this.wrap.setWidth(this.el.getWidth() + this.trigger.getWidth()); + }, + + doFocus: function () { + if (!this.mimicing) { + this.wrap.addClass('x-trigger-wrap-focus'); + this.mimicing = true; + Ext.get(Ext.isIE ? document.body : document).on( + 'mousedown', + this.mimicBlur, + this, + { + delay: 10, + } + ); + this.el.on('keydown', this.checkTab, this); + } + }, + + // private + checkTab: function (e) { + if (e.getKey() == e.TAB) { + this.triggerBlur(); + } + }, + + // private + mimicBlur: function (e) { + if (!this.wrap.contains(e.target) && this.field.validateBlur(e)) { + this.triggerBlur(); + } + }, + + // private + triggerBlur: function () { + this.mimicing = false; + Ext.get(Ext.isIE ? document.body : document).un( + 'mousedown', + this.mimicBlur, + this + ); + this.el.un('keydown', this.checkTab, this); + this.field.beforeBlur(); + this.wrap.removeClass('x-trigger-wrap-focus'); + this.field.onBlur.call(this.field); + }, + + initTrigger: function () { + this.trigger.addClassOnOver('x-form-trigger-over'); + this.trigger.addClassOnClick('x-form-trigger-click'); + }, + + initSpinner: function () { + this.field.addEvents({ + spin: true, + spinup: true, + spindown: true, + }); + + this.keyNav = new Ext.KeyNav(this.el, { + up: function (e) { + e.preventDefault(); + this.onSpinUp(); + }, + + down: function (e) { + e.preventDefault(); + this.onSpinDown(); + }, + + pageUp: function (e) { + e.preventDefault(); + this.onSpinUpAlternate(); + }, + + pageDown: function (e) { + e.preventDefault(); + this.onSpinDownAlternate(); + }, + + scope: this, + }); + + this.repeater = new Ext.util.ClickRepeater(this.trigger, { + accelerate: this.accelerate, + }); + this.field.mon(this.repeater, 'click', this.onTriggerClick, this, { + preventDefault: true, + }); + + this.field.mon(this.trigger, { + mouseover: this.onMouseOver, + mouseout: this.onMouseOut, + mousemove: this.onMouseMove, + mousedown: this.onMouseDown, + mouseup: this.onMouseUp, + scope: this, + preventDefault: true, + }); + + this.field.mon(this.wrap, 'mousewheel', this.handleMouseWheel, this); + + this.dd.setXConstraint(0, 0, 10); + this.dd.setYConstraint(1500, 1500, 10); + this.dd.endDrag = this.endDrag.createDelegate(this); + this.dd.startDrag = this.startDrag.createDelegate(this); + this.dd.onDrag = this.onDrag.createDelegate(this); + }, + + onMouseOver: function () { + if (this.disabled) { + return; + } + var middle = this.getMiddle(); + this.tmpHoverClass = + Ext.EventObject.getPageY() < middle + ? 'x-form-spinner-overup' + : 'x-form-spinner-overdown'; + this.trigger.addClass(this.tmpHoverClass); + }, + + //private + onMouseOut: function () { + this.trigger.removeClass(this.tmpHoverClass); + }, + + //private + onMouseMove: function () { + if (this.disabled) { + return; + } + var middle = this.getMiddle(); + if ( + (Ext.EventObject.getPageY() > middle && + this.tmpHoverClass == 'x-form-spinner-overup') || + (Ext.EventObject.getPageY() < middle && + this.tmpHoverClass == 'x-form-spinner-overdown') + ) { + } + }, + + //private + onMouseDown: function () { + if (this.disabled) { + return; + } + var middle = this.getMiddle(); + this.tmpClickClass = + Ext.EventObject.getPageY() < middle + ? 'x-form-spinner-clickup' + : 'x-form-spinner-clickdown'; + this.trigger.addClass(this.tmpClickClass); + }, + + //private + onMouseUp: function () { + this.trigger.removeClass(this.tmpClickClass); + }, + + //private + onTriggerClick: function () { + if (this.disabled || this.el.dom.readOnly) { + return; + } + var middle = this.getMiddle(); + var ud = Ext.EventObject.getPageY() < middle ? 'Up' : 'Down'; + this['onSpin' + ud](); + }, + + //private + getMiddle: function () { + var t = this.trigger.getTop(); + var h = this.trigger.getHeight(); + var middle = t + h / 2; + return middle; + }, + + //private + //checks if control is allowed to spin + isSpinnable: function () { + if (this.disabled || this.el.dom.readOnly) { + Ext.EventObject.preventDefault(); //prevent scrolling when disabled/readonly + return false; + } + return true; + }, + + handleMouseWheel: function (e) { + //disable scrolling when not focused + if (this.wrap.hasClass('x-trigger-wrap-focus') == false) { + return; + } + + var delta = e.getWheelDelta(); + if (delta > 0) { + this.onSpinUp(); + e.stopEvent(); + } else if (delta < 0) { + this.onSpinDown(); + e.stopEvent(); + } + }, + + //private + startDrag: function () { + this.proxy.show(); + this._previousY = Ext.fly(this.dd.getDragEl()).getTop(); + }, + + //private + endDrag: function () { + this.proxy.hide(); + }, + + //private + onDrag: function () { + if (this.disabled) { + return; + } + var y = Ext.fly(this.dd.getDragEl()).getTop(); + var ud = ''; + + if (this._previousY > y) { + ud = 'Up'; + } //up + if (this._previousY < y) { + ud = 'Down'; + } //down + if (ud != '') { + this['onSpin' + ud](); + } + + this._previousY = y; + }, + + //private + onSpinUp: function () { + if (this.isSpinnable() == false) { + return; + } + if (Ext.EventObject.shiftKey == true) { + this.onSpinUpAlternate(); + return; + } else { + this.spin(false, false); + } + this.field.fireEvent('spin', this); + this.field.fireEvent('spinup', this); + }, + + //private + onSpinDown: function () { + if (this.isSpinnable() == false) { + return; + } + if (Ext.EventObject.shiftKey == true) { + this.onSpinDownAlternate(); + return; + } else { + this.spin(true, false); + } + this.field.fireEvent('spin', this); + this.field.fireEvent('spindown', this); + }, + + //private + onSpinUpAlternate: function () { + if (this.isSpinnable() == false) { + return; + } + this.spin(false, true); + this.field.fireEvent('spin', this); + this.field.fireEvent('spinup', this); + }, + + //private + onSpinDownAlternate: function () { + if (this.isSpinnable() == false) { + return; + } + this.spin(true, true); + this.field.fireEvent('spin', this); + this.field.fireEvent('spindown', this); + }, + + spin: function (down, alternate) { + var v = parseFloat(this.field.getValue()); + var incr = + alternate == true + ? this.alternateIncrementValue + : this.incrementValue; + down == true ? (v -= incr) : (v += incr); + + v = isNaN(v) ? this.defaultValue : v; + v = this.fixBoundries(v); + this.field.setRawValue(v); + }, + + fixBoundries: function (value) { + var v = value; + + if (this.field.minValue != undefined && v < this.field.minValue) { + v = this.field.minValue; + } + if (this.field.maxValue != undefined && v > this.field.maxValue) { + v = this.field.maxValue; + } + + return this.fixPrecision(v); + }, + + // private + fixPrecision: function (value) { + var nan = isNaN(value); + if ( + !this.field.allowDecimals || + this.field.decimalPrecision == -1 || + nan || + !value + ) { + return nan ? '' : value; + } + return parseFloat( + parseFloat(value).toFixed(this.field.decimalPrecision) + ); + }, + + doDestroy: function () { + if (this.trigger) { + this.trigger.remove(); + } + if (this.wrap) { + this.wrap.remove(); + delete this.field.wrap; + } + + if (this.splitter) { + this.splitter.remove(); + } + + if (this.dd) { + this.dd.unreg(); + this.dd = null; + } + + if (this.proxy) { + this.proxy.remove(); + } + + if (this.repeater) { + this.repeater.purgeListeners(); + } + if (this.mimicing) { + Ext.get(Ext.isIE ? document.body : document).un( + 'mousedown', + this.mimicBlur, + this + ); + } + }, +}); + +//backwards compat +Ext.form.Spinner = Ext.ux.Spinner; diff --git a/deluge/ui/web/js/extjs/ext-extensions/StatusBar.js b/deluge/ui/web/js/extjs/ext-extensions/StatusBar.js new file mode 100644 index 0000000..1f6a5f0 --- /dev/null +++ b/deluge/ui/web/js/extjs/ext-extensions/StatusBar.js @@ -0,0 +1,422 @@ +/** + * Ext JS Library 3.4.0 + * Copyright(c) 2006-2011 Sencha Inc. + * licensing@sencha.com + * http://www.sencha.com/license + */ +/** + * @class Ext.ux.StatusBar + * <p>Basic status bar component that can be used as the bottom toolbar of any {@link Ext.Panel}. In addition to + * supporting the standard {@link Ext.Toolbar} interface for adding buttons, menus and other items, the StatusBar + * provides a greedy status element that can be aligned to either side and has convenient methods for setting the + * status text and icon. You can also indicate that something is processing using the {@link #showBusy} method.</p> + * <pre><code> +new Ext.Panel({ + title: 'StatusBar', + // etc. + bbar: new Ext.ux.StatusBar({ + id: 'my-status', + + // defaults to use when the status is cleared: + defaultText: 'Default status text', + defaultIconCls: 'default-icon', + + // values to set initially: + text: 'Ready', + iconCls: 'ready-icon', + + // any standard Toolbar items: + items: [{ + text: 'A Button' + }, '-', 'Plain Text'] + }) +}); + +// Update the status bar later in code: +var sb = Ext.getCmp('my-status'); +sb.setStatus({ + text: 'OK', + iconCls: 'ok-icon', + clear: true // auto-clear after a set interval +}); + +// Set the status bar to show that something is processing: +sb.showBusy(); + +// processing.... + +sb.clearStatus(); // once completeed +</code></pre> + * @extends Ext.Toolbar + * @constructor + * Creates a new StatusBar + * @param {Object/Array} config A config object + */ +Ext.ux.StatusBar = Ext.extend(Ext.Toolbar, { + /** + * @cfg {String} statusAlign + * The alignment of the status element within the overall StatusBar layout. When the StatusBar is rendered, + * it creates an internal div containing the status text and icon. Any additional Toolbar items added in the + * StatusBar's {@link #items} config, or added via {@link #add} or any of the supported add* methods, will be + * rendered, in added order, to the opposite side. The status element is greedy, so it will automatically + * expand to take up all sapce left over by any other items. Example usage: + * <pre><code> +// Create a left-aligned status bar containing a button, +// separator and text item that will be right-aligned (default): +new Ext.Panel({ + title: 'StatusBar', + // etc. + bbar: new Ext.ux.StatusBar({ + defaultText: 'Default status text', + id: 'status-id', + items: [{ + text: 'A Button' + }, '-', 'Plain Text'] + }) +}); + +// By adding the statusAlign config, this will create the +// exact same toolbar, except the status and toolbar item +// layout will be reversed from the previous example: +new Ext.Panel({ + title: 'StatusBar', + // etc. + bbar: new Ext.ux.StatusBar({ + defaultText: 'Default status text', + id: 'status-id', + statusAlign: 'right', + items: [{ + text: 'A Button' + }, '-', 'Plain Text'] + }) +}); +</code></pre> + */ + /** + * @cfg {String} defaultText + * The default {@link #text} value. This will be used anytime the status bar is cleared with the + * <tt>useDefaults:true</tt> option (defaults to ''). + */ + /** + * @cfg {String} defaultIconCls + * The default {@link #iconCls} value (see the iconCls docs for additional details about customizing the icon). + * This will be used anytime the status bar is cleared with the <tt>useDefaults:true</tt> option (defaults to ''). + */ + /** + * @cfg {String} text + * A string that will be <b>initially</b> set as the status message. This string + * will be set as innerHTML (html tags are accepted) for the toolbar item. + * If not specified, the value set for <code>{@link #defaultText}</code> + * will be used. + */ + /** + * @cfg {String} iconCls + * A CSS class that will be <b>initially</b> set as the status bar icon and is + * expected to provide a background image (defaults to ''). + * Example usage:<pre><code> +// Example CSS rule: +.x-statusbar .x-status-custom { + padding-left: 25px; + background: transparent url(images/custom-icon.gif) no-repeat 3px 2px; +} + +// Setting a default icon: +var sb = new Ext.ux.StatusBar({ + defaultIconCls: 'x-status-custom' +}); + +// Changing the icon: +sb.setStatus({ + text: 'New status', + iconCls: 'x-status-custom' +}); +</code></pre> + */ + + /** + * @cfg {String} cls + * The base class applied to the containing element for this component on render (defaults to 'x-statusbar') + */ + cls: 'x-statusbar', + /** + * @cfg {String} busyIconCls + * The default <code>{@link #iconCls}</code> applied when calling + * <code>{@link #showBusy}</code> (defaults to <tt>'x-status-busy'</tt>). + * It can be overridden at any time by passing the <code>iconCls</code> + * argument into <code>{@link #showBusy}</code>. + */ + busyIconCls: 'x-status-busy', + /** + * @cfg {String} busyText + * The default <code>{@link #text}</code> applied when calling + * <code>{@link #showBusy}</code> (defaults to <tt>'Loading...'</tt>). + * It can be overridden at any time by passing the <code>text</code> + * argument into <code>{@link #showBusy}</code>. + */ + busyText: 'Loading...', + /** + * @cfg {Number} autoClear + * The number of milliseconds to wait after setting the status via + * <code>{@link #setStatus}</code> before automatically clearing the status + * text and icon (defaults to <tt>5000</tt>). Note that this only applies + * when passing the <tt>clear</tt> argument to <code>{@link #setStatus}</code> + * since that is the only way to defer clearing the status. This can + * be overridden by specifying a different <tt>wait</tt> value in + * <code>{@link #setStatus}</code>. Calls to <code>{@link #clearStatus}</code> + * always clear the status bar immediately and ignore this value. + */ + autoClear: 5000, + + /** + * @cfg {String} emptyText + * The text string to use if no text has been set. Defaults to + * <tt>' '</tt>). If there are no other items in the toolbar using + * an empty string (<tt>''</tt>) for this value would end up in the toolbar + * height collapsing since the empty string will not maintain the toolbar + * height. Use <tt>''</tt> if the toolbar should collapse in height + * vertically when no text is specified and there are no other items in + * the toolbar. + */ + emptyText: ' ', + + // private + activeThreadId: 0, + + // private + initComponent: function () { + if (this.statusAlign == 'right') { + this.cls += ' x-status-right'; + } + Ext.ux.StatusBar.superclass.initComponent.call(this); + }, + + // private + afterRender: function () { + Ext.ux.StatusBar.superclass.afterRender.call(this); + + var right = this.statusAlign == 'right'; + this.currIconCls = this.iconCls || this.defaultIconCls; + this.statusEl = new Ext.Toolbar.TextItem({ + cls: 'x-status-text ' + (this.currIconCls || ''), + text: this.text || this.defaultText || '', + }); + + if (right) { + this.add('->'); + this.add(this.statusEl); + } else { + this.insert(0, this.statusEl); + this.insert(1, '->'); + } + this.doLayout(); + }, + + /** + * Sets the status {@link #text} and/or {@link #iconCls}. Also supports automatically clearing the + * status that was set after a specified interval. + * @param {Object/String} config A config object specifying what status to set, or a string assumed + * to be the status text (and all other options are defaulted as explained below). A config + * object containing any or all of the following properties can be passed:<ul> + * <li><tt>text</tt> {String} : (optional) The status text to display. If not specified, any current + * status text will remain unchanged.</li> + * <li><tt>iconCls</tt> {String} : (optional) The CSS class used to customize the status icon (see + * {@link #iconCls} for details). If not specified, any current iconCls will remain unchanged.</li> + * <li><tt>clear</tt> {Boolean/Number/Object} : (optional) Allows you to set an internal callback that will + * automatically clear the status text and iconCls after a specified amount of time has passed. If clear is not + * specified, the new status will not be auto-cleared and will stay until updated again or cleared using + * {@link #clearStatus}. If <tt>true</tt> is passed, the status will be cleared using {@link #autoClear}, + * {@link #defaultText} and {@link #defaultIconCls} via a fade out animation. If a numeric value is passed, + * it will be used as the callback interval (in milliseconds), overriding the {@link #autoClear} value. + * All other options will be defaulted as with the boolean option. To customize any other options, + * you can pass an object in the format:<ul> + * <li><tt>wait</tt> {Number} : (optional) The number of milliseconds to wait before clearing + * (defaults to {@link #autoClear}).</li> + * <li><tt>anim</tt> {Number} : (optional) False to clear the status immediately once the callback + * executes (defaults to true which fades the status out).</li> + * <li><tt>useDefaults</tt> {Number} : (optional) False to completely clear the status text and iconCls + * (defaults to true which uses {@link #defaultText} and {@link #defaultIconCls}).</li> + * </ul></li></ul> + * Example usage:<pre><code> +// Simple call to update the text +statusBar.setStatus('New status'); + +// Set the status and icon, auto-clearing with default options: +statusBar.setStatus({ + text: 'New status', + iconCls: 'x-status-custom', + clear: true +}); + +// Auto-clear with custom options: +statusBar.setStatus({ + text: 'New status', + iconCls: 'x-status-custom', + clear: { + wait: 8000, + anim: false, + useDefaults: false + } +}); +</code></pre> + * @return {Ext.ux.StatusBar} this + */ + setStatus: function (o) { + o = o || {}; + + if (typeof o == 'string') { + o = { text: o }; + } + if (o.text !== undefined) { + this.setText(o.text); + } + if (o.iconCls !== undefined) { + this.setIcon(o.iconCls); + } + + if (o.clear) { + var c = o.clear, + wait = this.autoClear, + defaults = { useDefaults: true, anim: true }; + + if (typeof c == 'object') { + c = Ext.applyIf(c, defaults); + if (c.wait) { + wait = c.wait; + } + } else if (typeof c == 'number') { + wait = c; + c = defaults; + } else if (typeof c == 'boolean') { + c = defaults; + } + + c.threadId = this.activeThreadId; + this.clearStatus.defer(wait, this, [c]); + } + return this; + }, + + /** + * Clears the status {@link #text} and {@link #iconCls}. Also supports clearing via an optional fade out animation. + * @param {Object} config (optional) A config object containing any or all of the following properties. If this + * object is not specified the status will be cleared using the defaults below:<ul> + * <li><tt>anim</tt> {Boolean} : (optional) True to clear the status by fading out the status element (defaults + * to false which clears immediately).</li> + * <li><tt>useDefaults</tt> {Boolean} : (optional) True to reset the text and icon using {@link #defaultText} and + * {@link #defaultIconCls} (defaults to false which sets the text to '' and removes any existing icon class).</li> + * </ul> + * @return {Ext.ux.StatusBar} this + */ + clearStatus: function (o) { + o = o || {}; + + if (o.threadId && o.threadId !== this.activeThreadId) { + // this means the current call was made internally, but a newer + // thread has set a message since this call was deferred. Since + // we don't want to overwrite a newer message just ignore. + return this; + } + + var text = o.useDefaults ? this.defaultText : this.emptyText, + iconCls = o.useDefaults + ? this.defaultIconCls + ? this.defaultIconCls + : '' + : ''; + + if (o.anim) { + // animate the statusEl Ext.Element + this.statusEl.el.fadeOut({ + remove: false, + useDisplay: true, + scope: this, + callback: function () { + this.setStatus({ + text: text, + iconCls: iconCls, + }); + + this.statusEl.el.show(); + }, + }); + } else { + // hide/show the el to avoid jumpy text or icon + this.statusEl.hide(); + this.setStatus({ + text: text, + iconCls: iconCls, + }); + this.statusEl.show(); + } + return this; + }, + + /** + * Convenience method for setting the status text directly. For more flexible options see {@link #setStatus}. + * @param {String} text (optional) The text to set (defaults to '') + * @return {Ext.ux.StatusBar} this + */ + setText: function (text) { + this.activeThreadId++; + this.text = text || ''; + if (this.rendered) { + this.statusEl.setText(this.text); + } + return this; + }, + + /** + * Returns the current status text. + * @return {String} The status text + */ + getText: function () { + return this.text; + }, + + /** + * Convenience method for setting the status icon directly. For more flexible options see {@link #setStatus}. + * See {@link #iconCls} for complete details about customizing the icon. + * @param {String} iconCls (optional) The icon class to set (defaults to '', and any current icon class is removed) + * @return {Ext.ux.StatusBar} this + */ + setIcon: function (cls) { + this.activeThreadId++; + cls = cls || ''; + + if (this.rendered) { + if (this.currIconCls) { + this.statusEl.removeClass(this.currIconCls); + this.currIconCls = null; + } + if (cls.length > 0) { + this.statusEl.addClass(cls); + this.currIconCls = cls; + } + } else { + this.currIconCls = cls; + } + return this; + }, + + /** + * Convenience method for setting the status text and icon to special values that are pre-configured to indicate + * a "busy" state, usually for loading or processing activities. + * @param {Object/String} config (optional) A config object in the same format supported by {@link #setStatus}, or a + * string to use as the status text (in which case all other options for setStatus will be defaulted). Use the + * <tt>text</tt> and/or <tt>iconCls</tt> properties on the config to override the default {@link #busyText} + * and {@link #busyIconCls} settings. If the config argument is not specified, {@link #busyText} and + * {@link #busyIconCls} will be used in conjunction with all of the default options for {@link #setStatus}. + * @return {Ext.ux.StatusBar} this + */ + showBusy: function (o) { + if (typeof o == 'string') { + o = { text: o }; + } + o = Ext.applyIf(o || {}, { + text: this.busyText, + iconCls: this.busyIconCls, + }); + return this.setStatus(o); + }, +}); +Ext.reg('statusbar', Ext.ux.StatusBar); diff --git a/deluge/ui/web/js/extjs/ext-extensions/form/FileUploadField.js b/deluge/ui/web/js/extjs/ext-extensions/form/FileUploadField.js new file mode 100644 index 0000000..3ab2347 --- /dev/null +++ b/deluge/ui/web/js/extjs/ext-extensions/form/FileUploadField.js @@ -0,0 +1,208 @@ +/** + * Ext JS Library 3.4.0 + * Copyright(c) 2006-2011 Sencha Inc. + * licensing@sencha.com + * http://www.sencha.com/license + */ +Ext.ns('Ext.ux.form'); + +/** + * @class Ext.ux.form.FileUploadField + * @extends Ext.form.TextField + * Creates a file upload field. + * @xtype fileuploadfield + */ +Ext.ux.form.FileUploadField = Ext.extend(Ext.form.TextField, { + /** + * @cfg {String} buttonText The button text to display on the upload button (defaults to + * 'Browse...'). Note that if you supply a value for {@link #buttonCfg}, the buttonCfg.text + * value will be used instead if available. + */ + buttonText: 'Browse...', + /** + * @cfg {Boolean} buttonOnly True to display the file upload field as a button with no visible + * text field (defaults to false). If true, all inherited TextField members will still be available. + */ + buttonOnly: false, + /** + * @cfg {Number} buttonOffset The number of pixels of space reserved between the button and the text field + * (defaults to 3). Note that this only applies if {@link #buttonOnly} = false. + */ + buttonOffset: 3, + + /** + * @cfg {Boolean} multiple True to select more than one file. (defaults to false). + * Note that this only applies if the HTML doc is using HTML5. + */ + multiple: false, + + /** + * @cfg {Object} buttonCfg A standard {@link Ext.Button} config object. + */ + + // private + readOnly: true, + + /** + * @hide + * @method autoSize + */ + autoSize: Ext.emptyFn, + + // private + initComponent: function () { + Ext.ux.form.FileUploadField.superclass.initComponent.call(this); + + this.addEvents( + /** + * @event fileselected + * Fires when the underlying file input field's value has changed from the user + * selecting a new file from the system file selection dialog. + * @param {Ext.ux.form.FileUploadField} this + * @param {String} value The file value returned by the underlying file input field + */ + 'fileselected' + ); + }, + + // private + onRender: function (ct, position) { + Ext.ux.form.FileUploadField.superclass.onRender.call( + this, + ct, + position + ); + + this.wrap = this.el.wrap({ cls: 'x-form-field-wrap x-form-file-wrap' }); + this.el.addClass('x-form-file-text'); + this.el.dom.removeAttribute('name'); + this.createFileInput(); + + var btnCfg = Ext.applyIf(this.buttonCfg || {}, { + text: this.buttonText, + }); + this.button = new Ext.Button( + Ext.apply(btnCfg, { + renderTo: this.wrap, + cls: 'x-form-file-btn' + (btnCfg.iconCls ? ' x-btn-icon' : ''), + }) + ); + + if (this.buttonOnly) { + this.el.hide(); + this.wrap.setWidth(this.button.getEl().getWidth()); + } + + this.bindListeners(); + this.resizeEl = this.positionEl = this.wrap; + }, + + bindListeners: function () { + this.fileInput.on({ + scope: this, + mouseenter: function () { + this.button.addClass(['x-btn-over', 'x-btn-focus']); + }, + mouseleave: function () { + this.button.removeClass([ + 'x-btn-over', + 'x-btn-focus', + 'x-btn-click', + ]); + }, + mousedown: function () { + this.button.addClass('x-btn-click'); + }, + mouseup: function () { + this.button.removeClass([ + 'x-btn-over', + 'x-btn-focus', + 'x-btn-click', + ]); + }, + change: function () { + var value = this.fileInput.dom.files; + // Fallback to value. + if (!value) value = this.fileInput.dom.value; + this.setValue(value); + this.fireEvent('fileselected', this, value); + }, + }); + }, + + createFileInput: function () { + this.fileInput = this.wrap.createChild({ + id: this.getFileInputId(), + name: this.name || this.getId(), + cls: 'x-form-file', + tag: 'input', + type: 'file', + size: 1, + }); + this.fileInput.dom.multiple = this.multiple; + }, + + reset: function () { + if (this.rendered) { + this.fileInput.remove(); + this.createFileInput(); + this.bindListeners(); + } + Ext.ux.form.FileUploadField.superclass.reset.call(this); + }, + + // private + getFileInputId: function () { + return this.id + '-file'; + }, + + // private + onResize: function (w, h) { + Ext.ux.form.FileUploadField.superclass.onResize.call(this, w, h); + + this.wrap.setWidth(w); + + if (!this.buttonOnly) { + var w = + this.wrap.getWidth() - + this.button.getEl().getWidth() - + this.buttonOffset; + this.el.setWidth(w); + } + }, + + // private + onDestroy: function () { + Ext.ux.form.FileUploadField.superclass.onDestroy.call(this); + Ext.destroy(this.fileInput, this.button, this.wrap); + }, + + onDisable: function () { + Ext.ux.form.FileUploadField.superclass.onDisable.call(this); + this.doDisable(true); + }, + + onEnable: function () { + Ext.ux.form.FileUploadField.superclass.onEnable.call(this); + this.doDisable(false); + }, + + // private + doDisable: function (disabled) { + this.fileInput.dom.disabled = disabled; + this.button.setDisabled(disabled); + }, + + // private + preFocus: Ext.emptyFn, + + // private + alignErrorIcon: function () { + this.errorIcon.alignTo(this.wrap, 'tl-tr', [2, 0]); + }, +}); + +Ext.reg('fileuploadfield', Ext.ux.form.FileUploadField); + +// backwards compat +Ext.form.FileUploadField = Ext.ux.form.FileUploadField; diff --git a/deluge/ui/web/js/extjs/ext-extensions/form/RadioGroupFix.js b/deluge/ui/web/js/extjs/ext-extensions/form/RadioGroupFix.js new file mode 100644 index 0000000..416c098 --- /dev/null +++ b/deluge/ui/web/js/extjs/ext-extensions/form/RadioGroupFix.js @@ -0,0 +1,50 @@ +/** + * Ext.ux.form.RadioGroup.js + * + * Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com> + * + * This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with + * the additional special exception to link portions of this program with the OpenSSL library. + * See LICENSE for more details. + */ + +// Allow radiogroups to be treated as a single form element. +Ext.override(Ext.form.RadioGroup, { + afterRender: function () { + this.items.each(function (i) { + this.relayEvents(i, ['check']); + }, this); + if (this.lazyValue) { + this.setValue(this.value); + delete this.value; + delete this.lazyValue; + } + Ext.form.RadioGroup.superclass.afterRender.call(this); + }, + + getName: function () { + return this.items.first().getName(); + }, + + getValue: function () { + return this.items.first().getGroupValue(); + }, + + setValue: function (v) { + if (!this.items.each) { + this.value = v; + this.lazyValue = true; + return; + } + this.items.each(function (item) { + if (item.rendered) { + var checked = item.el.getValue() == String(v); + item.el.dom.checked = checked; + item.el.dom.defaultChecked = checked; + item.wrap[checked ? 'addClass' : 'removeClass']( + item.checkedCls + ); + } + }); + }, +}); diff --git a/deluge/ui/web/js/extjs/ext-extensions/form/SpinnerField.js b/deluge/ui/web/js/extjs/ext-extensions/form/SpinnerField.js new file mode 100644 index 0000000..890fb5b --- /dev/null +++ b/deluge/ui/web/js/extjs/ext-extensions/form/SpinnerField.js @@ -0,0 +1,68 @@ +/** + * Ext JS Library 3.4.0 + * Copyright(c) 2006-2011 Sencha Inc. + * licensing@sencha.com + * http://www.sencha.com/license + */ +Ext.ns('Ext.ux.form'); + +/** + * @class Ext.ux.form.SpinnerField + * @extends Ext.form.NumberField + * Creates a field utilizing Ext.ux.Spinner + * @xtype spinnerfield + */ +Ext.ux.form.SpinnerField = Ext.extend(Ext.form.NumberField, { + actionMode: 'wrap', + deferHeight: true, + autoSize: Ext.emptyFn, + onBlur: Ext.emptyFn, + adjustSize: Ext.BoxComponent.prototype.adjustSize, + + constructor: function (config) { + var spinnerConfig = Ext.copyTo( + {}, + config, + 'incrementValue,alternateIncrementValue,accelerate,defaultValue,triggerClass,splitterClass' + ); + + var spl = (this.spinner = new Ext.ux.Spinner(spinnerConfig)); + + var plugins = config.plugins + ? Ext.isArray(config.plugins) + ? config.plugins.push(spl) + : [config.plugins, spl] + : spl; + + Ext.ux.form.SpinnerField.superclass.constructor.call( + this, + Ext.apply(config, { plugins: plugins }) + ); + }, + + // private + getResizeEl: function () { + return this.wrap; + }, + + // private + getPositionEl: function () { + return this.wrap; + }, + + // private + alignErrorIcon: function () { + if (this.wrap) { + this.errorIcon.alignTo(this.wrap, 'tl-tr', [2, 0]); + } + }, + + validateBlur: function () { + return true; + }, +}); + +Ext.reg('spinnerfield', Ext.ux.form.SpinnerField); + +//backwards compat +Ext.form.SpinnerField = Ext.ux.form.SpinnerField; diff --git a/deluge/ui/web/js/extjs/ext-extensions/form/SpinnerFieldFix.js b/deluge/ui/web/js/extjs/ext-extensions/form/SpinnerFieldFix.js new file mode 100644 index 0000000..6784ae0 --- /dev/null +++ b/deluge/ui/web/js/extjs/ext-extensions/form/SpinnerFieldFix.js @@ -0,0 +1,13 @@ +/** + * Ext.ux.form.SpinnerField.js + * + * Copyright (c) Damien Churchill 2010 <damoxc@gmail.com> + * + * This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with + * the additional special exception to link portions of this program with the OpenSSL library. + * See LICENSE for more details. + */ + +Ext.override(Ext.ux.form.SpinnerField, { + onBlur: Ext.form.Field.prototype.onBlur, +}); diff --git a/deluge/ui/web/js/extjs/ext-extensions/form/SpinnerGroup.js b/deluge/ui/web/js/extjs/ext-extensions/form/SpinnerGroup.js new file mode 100644 index 0000000..ee761aa --- /dev/null +++ b/deluge/ui/web/js/extjs/ext-extensions/form/SpinnerGroup.js @@ -0,0 +1,205 @@ +/** + * Ext.ux.form.SpinnerGroup.js + * + * Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com> + * + * This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with + * the additional special exception to link portions of this program with the OpenSSL library. + * See LICENSE for more details. + */ +Ext.ns('Ext.ux.form'); + +/** + * + */ +Ext.ux.form.SpinnerGroup = Ext.extend(Ext.form.CheckboxGroup, { + // private + defaultType: 'spinnerfield', + anchor: '98%', + + // private + groupCls: 'x-form-spinner-group', + + colCfg: {}, + + // private + onRender: function (ct, position) { + if (!this.el) { + var panelCfg = { + cls: this.groupCls, + layout: 'column', + border: false, + renderTo: ct, + }; + var colCfg = Ext.apply( + { + defaultType: this.defaultType, + layout: 'form', + border: false, + labelWidth: 60, + defaults: { + hideLabel: true, + anchor: '60%', + }, + }, + this.colCfg + ); + + if (this.items[0].items) { + // The container has standard ColumnLayout configs, so pass them in directly + + Ext.apply(panelCfg, { + layoutConfig: { columns: this.items.length }, + defaults: this.defaults, + items: this.items, + }); + for (var i = 0, len = this.items.length; i < len; i++) { + Ext.applyIf(this.items[i], colCfg); + } + } else { + // The container has field item configs, so we have to generate the column + // panels first then move the items into the columns as needed. + + var numCols, + cols = []; + + if (typeof this.columns == 'string') { + // 'auto' so create a col per item + this.columns = this.items.length; + } + if (!Ext.isArray(this.columns)) { + var cs = []; + for (var i = 0; i < this.columns; i++) { + cs.push((100 / this.columns) * 0.01); // distribute by even % + } + this.columns = cs; + } + + numCols = this.columns.length; + + // Generate the column configs with the correct width setting + for (var i = 0; i < numCols; i++) { + var cc = Ext.apply({ items: [] }, colCfg); + cc[this.columns[i] <= 1 ? 'columnWidth' : 'width'] = + this.columns[i]; + if (this.defaults) { + cc.defaults = Ext.apply( + cc.defaults || {}, + this.defaults + ); + } + cols.push(cc); + } + + // Distribute the original items into the columns + if (this.vertical) { + var rows = Math.ceil(this.items.length / numCols), + ri = 0; + for (var i = 0, len = this.items.length; i < len; i++) { + if (i > 0 && i % rows == 0) { + ri++; + } + if (this.items[i].fieldLabel) { + this.items[i].hideLabel = false; + } + cols[ri].items.push(this.items[i]); + } + } else { + for (var i = 0, len = this.items.length; i < len; i++) { + var ci = i % numCols; + if (this.items[i].fieldLabel) { + this.items[i].hideLabel = false; + } + cols[ci].items.push(this.items[i]); + } + } + + Ext.apply(panelCfg, { + layoutConfig: { columns: numCols }, + items: cols, + }); + } + + this.panel = new Ext.Panel(panelCfg); + this.el = this.panel.getEl(); + + if (this.forId && this.itemCls) { + var l = this.el.up(this.itemCls).child('label', true); + if (l) { + l.setAttribute('htmlFor', this.forId); + } + } + + var fields = this.panel.findBy(function (c) { + return c.isFormField; + }, this); + + this.items = new Ext.util.MixedCollection(); + this.items.addAll(fields); + + this.items.each(function (field) { + field.on('spin', this.onFieldChange, this); + field.on('change', this.onFieldChange, this); + }, this); + + if (this.lazyValueSet) { + this.setValue(this.value); + delete this.value; + delete this.lazyValueSet; + } + + if (this.lazyRawValueSet) { + this.setRawValue(this.rawValue); + delete this.rawValue; + delete this.lazyRawValueSet; + } + } + + Ext.ux.form.SpinnerGroup.superclass.onRender.call(this, ct, position); + }, + + onFieldChange: function (spinner) { + this.fireEvent('change', this, this.getValue()); + }, + + initValue: Ext.emptyFn, + + getValue: function () { + var value = [this.items.getCount()]; + this.items.each(function (item, i) { + value[i] = Number(item.getValue()); + }); + return value; + }, + + getRawValue: function () { + var value = [this.items.getCount()]; + this.items.each(function (item, i) { + value[i] = Number(item.getRawValue()); + }); + return value; + }, + + setValue: function (value) { + if (!this.rendered) { + this.value = value; + this.lazyValueSet = true; + } else { + this.items.each(function (item, i) { + item.setValue(value[i]); + }); + } + }, + + setRawValue: function (value) { + if (!this.rendered) { + this.rawValue = value; + this.lazyRawValueSet = true; + } else { + this.items.each(function (item, i) { + item.setRawValue(value[i]); + }); + } + }, +}); +Ext.reg('spinnergroup', Ext.ux.form.SpinnerGroup); diff --git a/deluge/ui/web/js/extjs/ext-extensions/form/ToggleField.js b/deluge/ui/web/js/extjs/ext-extensions/form/ToggleField.js new file mode 100644 index 0000000..eb60c3b --- /dev/null +++ b/deluge/ui/web/js/extjs/ext-extensions/form/ToggleField.js @@ -0,0 +1,72 @@ +/** + * Ext.ux.form.ToggleField.js + * + * Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com> + * + * This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with + * the additional special exception to link portions of this program with the OpenSSL library. + * See LICENSE for more details. + */ +Ext.namespace('Ext.ux.form'); + +/** + * Ext.ux.form.ToggleField class + * + * @author Damien Churchill + * @version v0.1 + * + * @class Ext.ux.form.ToggleField + * @extends Ext.form.TriggerField + */ +Ext.ux.form.ToggleField = Ext.extend(Ext.form.Field, { + cls: 'x-toggle-field', + + initComponent: function () { + Ext.ux.form.ToggleField.superclass.initComponent.call(this); + + this.toggle = new Ext.form.Checkbox(); + this.toggle.on('check', this.onToggleCheck, this); + + this.input = new Ext.form.TextField({ + disabled: true, + }); + }, + + onRender: function (ct, position) { + if (!this.el) { + this.panel = new Ext.Panel({ + cls: this.groupCls, + layout: 'table', + layoutConfig: { + columns: 2, + }, + border: false, + renderTo: ct, + }); + this.panel.ownerCt = this; + this.el = this.panel.getEl(); + + this.panel.add(this.toggle); + this.panel.add(this.input); + this.panel.doLayout(); + + this.toggle.getEl().parent().setStyle('padding-right', '10px'); + } + Ext.ux.form.ToggleField.superclass.onRender.call(this, ct, position); + }, + + // private + onResize: function (w, h) { + this.panel.setSize(w, h); + this.panel.doLayout(); + + // we substract 10 for the padding :-) + var inputWidth = w - this.toggle.getSize().width - 25; + this.input.setSize(inputWidth, h); + }, + + onToggleCheck: function (toggle, checked) { + this.input.setDisabled(!checked); + }, +}); +Ext.reg('togglefield', Ext.ux.form.ToggleField); diff --git a/deluge/ui/web/js/extjs/ext-extensions/grid/BufferView.js b/deluge/ui/web/js/extjs/ext-extensions/grid/BufferView.js new file mode 100644 index 0000000..0fce3b4 --- /dev/null +++ b/deluge/ui/web/js/extjs/ext-extensions/grid/BufferView.js @@ -0,0 +1,270 @@ +/** + * Ext JS Library 3.4.0 + * Copyright(c) 2006-2011 Sencha Inc. + * licensing@sencha.com + * http://www.sencha.com/license + */ +Ext.ns('Ext.ux.grid'); + +/** + * @class Ext.ux.grid.BufferView + * @extends Ext.grid.GridView + * A custom GridView which renders rows on an as-needed basis. + */ +Ext.ux.grid.BufferView = Ext.extend(Ext.grid.GridView, { + /** + * @cfg {Number} rowHeight + * The height of a row in the grid. + */ + rowHeight: 19, + + /** + * @cfg {Number} borderHeight + * The combined height of border-top and border-bottom of a row. + */ + borderHeight: 2, + + /** + * @cfg {Boolean/Number} scrollDelay + * The number of milliseconds before rendering rows out of the visible + * viewing area. Defaults to 100. Rows will render immediately with a config + * of false. + */ + scrollDelay: 100, + + /** + * @cfg {Number} cacheSize + * The number of rows to look forward and backwards from the currently viewable + * area. The cache applies only to rows that have been rendered already. + */ + cacheSize: 20, + + /** + * @cfg {Number} cleanDelay + * The number of milliseconds to buffer cleaning of extra rows not in the + * cache. + */ + cleanDelay: 500, + + initTemplates: function () { + Ext.ux.grid.BufferView.superclass.initTemplates.call(this); + var ts = this.templates; + // empty div to act as a place holder for a row + ts.rowHolder = new Ext.Template( + '<div class="x-grid3-row {alt}" style="{tstyle}"></div>' + ); + ts.rowHolder.disableFormats = true; + ts.rowHolder.compile(); + + ts.rowBody = new Ext.Template( + '<table class="x-grid3-row-table" border="0" cellspacing="0" cellpadding="0" style="{tstyle}">', + '<tbody><tr>{cells}</tr>', + this.enableRowBody + ? '<tr class="x-grid3-row-body-tr" style="{bodyStyle}"><td colspan="{cols}" class="x-grid3-body-cell" tabIndex="0" hidefocus="on"><div class="x-grid3-row-body">{body}</div></td></tr>' + : '', + '</tbody></table>' + ); + ts.rowBody.disableFormats = true; + ts.rowBody.compile(); + }, + + getStyleRowHeight: function () { + return Ext.isBorderBox + ? this.rowHeight + this.borderHeight + : this.rowHeight; + }, + + getCalculatedRowHeight: function () { + return this.rowHeight + this.borderHeight; + }, + + getVisibleRowCount: function () { + var rh = this.getCalculatedRowHeight(), + visibleHeight = this.scroller.dom.clientHeight; + return visibleHeight < 1 ? 0 : Math.ceil(visibleHeight / rh); + }, + + getVisibleRows: function () { + var count = this.getVisibleRowCount(), + sc = this.scroller.dom.scrollTop, + start = + sc === 0 + ? 0 + : Math.floor(sc / this.getCalculatedRowHeight()) - 1; + return { + first: Math.max(start, 0), + last: Math.min(start + count + 2, this.ds.getCount() - 1), + }; + }, + + doRender: function (cs, rs, ds, startRow, colCount, stripe, onlyBody) { + var ts = this.templates, + ct = ts.cell, + rt = ts.row, + rb = ts.rowBody, + last = colCount - 1, + rh = this.getStyleRowHeight(), + vr = this.getVisibleRows(), + tstyle = 'width:' + this.getTotalWidth() + ';height:' + rh + 'px;', + // buffers + buf = [], + cb, + c, + p = {}, + rp = { tstyle: tstyle }, + r; + for (var j = 0, len = rs.length; j < len; j++) { + r = rs[j]; + cb = []; + var rowIndex = j + startRow, + visible = rowIndex >= vr.first && rowIndex <= vr.last; + if (visible) { + for (var i = 0; i < colCount; i++) { + c = cs[i]; + p.id = c.id; + p.css = + i === 0 + ? 'x-grid3-cell-first ' + : i == last + ? 'x-grid3-cell-last ' + : ''; + p.attr = p.cellAttr = ''; + p.value = c.renderer(r.data[c.name], p, r, rowIndex, i, ds); + p.style = c.style; + if (p.value === undefined || p.value === '') { + p.value = ' '; + } + if (r.dirty && typeof r.modified[c.name] !== 'undefined') { + p.css += ' x-grid3-dirty-cell'; + } + cb[cb.length] = ct.apply(p); + } + } + var alt = []; + if (stripe && (rowIndex + 1) % 2 === 0) { + alt[0] = 'x-grid3-row-alt'; + } + if (r.dirty) { + alt[1] = ' x-grid3-dirty-row'; + } + rp.cols = colCount; + if (this.getRowClass) { + alt[2] = this.getRowClass(r, rowIndex, rp, ds); + } + rp.alt = alt.join(' '); + rp.cells = cb.join(''); + buf[buf.length] = !visible + ? ts.rowHolder.apply(rp) + : onlyBody + ? rb.apply(rp) + : rt.apply(rp); + } + return buf.join(''); + }, + + isRowRendered: function (index) { + var row = this.getRow(index); + return row && row.childNodes.length > 0; + }, + + syncScroll: function () { + Ext.ux.grid.BufferView.superclass.syncScroll.apply(this, arguments); + this.update(); + }, + + // a (optionally) buffered method to update contents of gridview + update: function () { + if (this.scrollDelay) { + if (!this.renderTask) { + this.renderTask = new Ext.util.DelayedTask(this.doUpdate, this); + } + this.renderTask.delay(this.scrollDelay); + } else { + this.doUpdate(); + } + }, + + onRemove: function (ds, record, index, isUpdate) { + Ext.ux.grid.BufferView.superclass.onRemove.apply(this, arguments); + if (isUpdate !== true) { + this.update(); + } + }, + + doUpdate: function () { + if (this.getVisibleRowCount() > 0) { + var g = this.grid, + cm = g.colModel, + ds = g.store, + cs = this.getColumnData(), + vr = this.getVisibleRows(), + row; + for (var i = vr.first; i <= vr.last; i++) { + // if row is NOT rendered and is visible, render it + if (!this.isRowRendered(i) && (row = this.getRow(i))) { + var html = this.doRender( + cs, + [ds.getAt(i)], + ds, + i, + cm.getColumnCount(), + g.stripeRows, + true + ); + row.innerHTML = html; + } + } + this.clean(); + } + }, + + // a buffered method to clean rows + clean: function () { + if (!this.cleanTask) { + this.cleanTask = new Ext.util.DelayedTask(this.doClean, this); + } + this.cleanTask.delay(this.cleanDelay); + }, + + doClean: function () { + if (this.getVisibleRowCount() > 0) { + var vr = this.getVisibleRows(); + vr.first -= this.cacheSize; + vr.last += this.cacheSize; + + var i = 0, + rows = this.getRows(); + // if first is less than 0, all rows have been rendered + // so lets clean the end... + if (vr.first <= 0) { + i = vr.last + 1; + } + for (var len = this.ds.getCount(); i < len; i++) { + // if current row is outside of first and last and + // has content, update the innerHTML to nothing + if ((i < vr.first || i > vr.last) && rows[i].innerHTML) { + rows[i].innerHTML = ''; + } + } + } + }, + + removeTask: function (name) { + var task = this[name]; + if (task && task.cancel) { + task.cancel(); + this[name] = null; + } + }, + + destroy: function () { + this.removeTask('cleanTask'); + this.removeTask('renderTask'); + Ext.ux.grid.BufferView.superclass.destroy.call(this); + }, + + layout: function () { + Ext.ux.grid.BufferView.superclass.layout.call(this); + this.update(); + }, +}); diff --git a/deluge/ui/web/js/extjs/ext-extensions/layout/FormLayoutFix.js b/deluge/ui/web/js/extjs/ext-extensions/layout/FormLayoutFix.js new file mode 100644 index 0000000..3ccce2b --- /dev/null +++ b/deluge/ui/web/js/extjs/ext-extensions/layout/FormLayoutFix.js @@ -0,0 +1,39 @@ +/** + * Ext.ux.layout.FormLayoutFix.js + * + * Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com> + * + * This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with + * the additional special exception to link portions of this program with the OpenSSL library. + * See LICENSE for more details. + */ + +// Taken from http://extjs.com/forum/showthread.php?t=75273 +// remove spaces for hidden elements and make show(), hide(), enable() and disable() act on +// the label. don't use hideLabel with this. +Ext.override(Ext.layout.FormLayout, { + renderItem: function (c, position, target) { + if ( + c && + !c.rendered && + (c.isFormField || c.fieldLabel) && + c.inputType != 'hidden' + ) { + var args = this.getTemplateArgs(c); + if (typeof position == 'number') { + position = target.dom.childNodes[position] || null; + } + if (position) { + c.formItem = this.fieldTpl.insertBefore(position, args, true); + } else { + c.formItem = this.fieldTpl.append(target, args, true); + } + c.actionMode = 'formItem'; + c.render('x-form-el-' + c.id); + c.container = c.formItem; + c.actionMode = 'container'; + } else { + Ext.layout.FormLayout.superclass.renderItem.apply(this, arguments); + } + }, +}); diff --git a/deluge/ui/web/js/extjs/ext-extensions/tree/MultiSelectionModelFix.js b/deluge/ui/web/js/extjs/ext-extensions/tree/MultiSelectionModelFix.js new file mode 100644 index 0000000..ba26a72 --- /dev/null +++ b/deluge/ui/web/js/extjs/ext-extensions/tree/MultiSelectionModelFix.js @@ -0,0 +1,68 @@ +/** + * Ext.ux.tree.MultiSelectionModelFix.js + * + * Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com> + * + * This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with + * the additional special exception to link portions of this program with the OpenSSL library. + * See LICENSE for more details. + */ + +/** + * This enhances the MSM to allow for shift selecting in tree grids etc. + * @author Damien Churchill <damoxc@gmail.com> + */ +Ext.override(Ext.tree.MultiSelectionModel, { + onNodeClick: function (node, e) { + if (e.ctrlKey && this.isSelected(node)) { + this.unselect(node); + } else if (e.shiftKey && !this.isSelected(node)) { + var parentNode = node.parentNode; + // We can only shift select files in the same node + if (this.lastSelNode.parentNode.id != parentNode.id) return; + + // Get the node indexes + var fi = parentNode.indexOf(node), + li = parentNode.indexOf(this.lastSelNode); + + // Select the last clicked node and wipe old selections + this.select(this.lastSelNode, e, false, true); + + // Swap the values if required + if (fi > li) { + (fi = fi + li), (li = fi - li), (fi = fi - li); + } + + // Select all the nodes + parentNode.eachChild(function (n) { + var i = parentNode.indexOf(n); + if (fi < i && i < li) { + this.select(n, e, true, true); + } + }, this); + + // Select the clicked node + this.select(node, e, true); + } else { + this.select(node, e, e.ctrlKey); + } + }, + + select: function (node, e, keepExisting, suppressEvent) { + if (keepExisting !== true) { + this.clearSelections(true); + } + if (this.isSelected(node)) { + this.lastSelNode = node; + return node; + } + this.selNodes.push(node); + this.selMap[node.id] = node; + this.lastSelNode = node; + node.ui.onSelectedChange(true); + if (suppressEvent !== true) { + this.fireEvent('selectionchange', this, this.selNodes); + } + return node; + }, +}); diff --git a/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGrid.js b/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGrid.js new file mode 100644 index 0000000..7a74360 --- /dev/null +++ b/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGrid.js @@ -0,0 +1,468 @@ +/** + * Ext JS Library 3.4.0 + * Copyright(c) 2006-2011 Sencha Inc. + * licensing@sencha.com + * http://www.sencha.com/license + */ +Ext.ns('Ext.ux.tree'); + +/** + * @class Ext.ux.tree.TreeGrid + * @extends Ext.tree.TreePanel + * + * @xtype treegrid + */ +Ext.ux.tree.TreeGrid = Ext.extend(Ext.tree.TreePanel, { + rootVisible: false, + useArrows: true, + lines: false, + borderWidth: Ext.isBorderBox ? 0 : 2, // the combined left/right border for each cell + cls: 'x-treegrid', + + columnResize: true, + enableSort: true, + reserveScrollOffset: true, + enableHdMenu: true, + + columnsText: 'Columns', + + initComponent: function () { + if (!this.root) { + this.root = new Ext.tree.AsyncTreeNode({ text: 'Root' }); + } + + // initialize the loader + var l = this.loader; + if (!l) { + l = new Ext.ux.tree.TreeGridLoader({ + dataUrl: this.dataUrl, + requestMethod: this.requestMethod, + store: this.store, + }); + } else if (Ext.isObject(l) && !l.load) { + l = new Ext.ux.tree.TreeGridLoader(l); + } + this.loader = l; + + Ext.ux.tree.TreeGrid.superclass.initComponent.call(this); + + this.initColumns(); + + if (this.enableSort) { + this.treeGridSorter = new Ext.ux.tree.TreeGridSorter( + this, + this.enableSort + ); + } + + if (this.columnResize) { + this.colResizer = new Ext.tree.ColumnResizer(this.columnResize); + this.colResizer.init(this); + } + + var c = this.columns; + if (!this.internalTpl) { + this.internalTpl = new Ext.XTemplate( + '<div class="x-grid3-header">', + '<div class="x-treegrid-header-inner">', + '<div class="x-grid3-header-offset">', + '<table style="table-layout: fixed;" cellspacing="0" cellpadding="0" border="0"><colgroup><tpl for="columns"><col /></tpl></colgroup>', + '<thead><tr class="x-grid3-hd-row">', + '<tpl for="columns">', + '<td class="x-grid3-hd x-grid3-cell x-treegrid-hd" style="text-align: {align};" id="', + this.id, + '-xlhd-{#}">', + '<div class="x-grid3-hd-inner x-treegrid-hd-inner" unselectable="on">', + this.enableHdMenu + ? '<a class="x-grid3-hd-btn" href="#"></a>' + : '', + '{header}<img class="x-grid3-sort-icon" src="', + Ext.BLANK_IMAGE_URL, + '" />', + '</div>', + '</td></tpl>', + '</tr></thead>', + '</table>', + '</div></div>', + '</div>', + '<div class="x-treegrid-root-node">', + '<table class="x-treegrid-root-table" cellpadding="0" cellspacing="0" style="table-layout: fixed;"></table>', + '</div>' + ); + } + + if (!this.colgroupTpl) { + this.colgroupTpl = new Ext.XTemplate( + '<colgroup><tpl for="columns"><col style="width: {width}px"/></tpl></colgroup>' + ); + } + }, + + initColumns: function () { + var cs = this.columns, + len = cs.length, + columns = [], + i, + c; + + for (i = 0; i < len; i++) { + c = cs[i]; + if (!c.isColumn) { + c.xtype = c.xtype + ? /^tg/.test(c.xtype) + ? c.xtype + : 'tg' + c.xtype + : 'tgcolumn'; + c = Ext.create(c); + } + c.init(this); + columns.push(c); + + if (this.enableSort !== false && c.sortable !== false) { + c.sortable = true; + this.enableSort = true; + } + } + + this.columns = columns; + }, + + onRender: function () { + Ext.tree.TreePanel.superclass.onRender.apply(this, arguments); + + this.el.addClass('x-treegrid'); + + this.outerCt = this.body.createChild({ + cls: + 'x-tree-root-ct x-treegrid-ct ' + + (this.useArrows + ? 'x-tree-arrows' + : this.lines + ? 'x-tree-lines' + : 'x-tree-no-lines'), + }); + + this.internalTpl.overwrite(this.outerCt, { columns: this.columns }); + + this.mainHd = Ext.get(this.outerCt.dom.firstChild); + this.innerHd = Ext.get(this.mainHd.dom.firstChild); + this.innerBody = Ext.get(this.outerCt.dom.lastChild); + this.innerCt = Ext.get(this.innerBody.dom.firstChild); + + this.colgroupTpl.insertFirst(this.innerCt, { columns: this.columns }); + + if (this.hideHeaders) { + this.el.child('.x-grid3-header').setDisplayed('none'); + } else if (this.enableHdMenu !== false) { + this.hmenu = new Ext.menu.Menu({ id: this.id + '-hctx' }); + if (this.enableColumnHide !== false) { + this.colMenu = new Ext.menu.Menu({ + id: this.id + '-hcols-menu', + }); + this.colMenu.on({ + scope: this, + beforeshow: this.beforeColMenuShow, + itemclick: this.handleHdMenuClick, + }); + this.hmenu.add({ + itemId: 'columns', + hideOnClick: false, + text: this.columnsText, + menu: this.colMenu, + iconCls: 'x-cols-icon', + }); + } + this.hmenu.on('itemclick', this.handleHdMenuClick, this); + } + }, + + setRootNode: function (node) { + node.attributes.uiProvider = Ext.ux.tree.TreeGridRootNodeUI; + node = Ext.ux.tree.TreeGrid.superclass.setRootNode.call(this, node); + if (this.innerCt) { + this.colgroupTpl.insertFirst(this.innerCt, { + columns: this.columns, + }); + } + return node; + }, + + clearInnerCt: function () { + if (Ext.isIE) { + var dom = this.innerCt.dom; + while (dom.firstChild) { + dom.removeChild(dom.firstChild); + } + } else { + Ext.ux.tree.TreeGrid.superclass.clearInnerCt.call(this); + } + }, + + initEvents: function () { + Ext.ux.tree.TreeGrid.superclass.initEvents.apply(this, arguments); + + this.mon(this.innerBody, 'scroll', this.syncScroll, this); + this.mon(this.innerHd, 'click', this.handleHdDown, this); + this.mon(this.mainHd, { + scope: this, + mouseover: this.handleHdOver, + mouseout: this.handleHdOut, + }); + }, + + onResize: function (w, h) { + Ext.ux.tree.TreeGrid.superclass.onResize.apply(this, arguments); + + var bd = this.innerBody.dom; + var hd = this.innerHd.dom; + + if (!bd) { + return; + } + + if (Ext.isNumber(h)) { + bd.style.height = + this.body.getHeight(true) - hd.offsetHeight + 'px'; + } + + if (Ext.isNumber(w)) { + var sw = Ext.num(this.scrollOffset, Ext.getScrollBarWidth()); + if ( + this.reserveScrollOffset || + bd.offsetWidth - bd.clientWidth > 10 + ) { + this.setScrollOffset(sw); + } else { + var me = this; + setTimeout(function () { + me.setScrollOffset( + bd.offsetWidth - bd.clientWidth > 10 ? sw : 0 + ); + }, 10); + } + } + }, + + updateColumnWidths: function () { + var cols = this.columns, + colCount = cols.length, + groups = this.outerCt.query('colgroup'), + groupCount = groups.length, + c, + g, + i, + j; + + for (i = 0; i < colCount; i++) { + c = cols[i]; + for (j = 0; j < groupCount; j++) { + g = groups[j]; + g.childNodes[i].style.width = (c.hidden ? 0 : c.width) + 'px'; + } + } + + for ( + i = 0, groups = this.innerHd.query('td'), len = groups.length; + i < len; + i++ + ) { + c = Ext.fly(groups[i]); + if (cols[i] && cols[i].hidden) { + c.addClass('x-treegrid-hd-hidden'); + } else { + c.removeClass('x-treegrid-hd-hidden'); + } + } + + var tcw = this.getTotalColumnWidth(); + Ext.fly(this.innerHd.dom.firstChild).setWidth( + tcw + (this.scrollOffset || 0) + ); + this.outerCt.select('table').setWidth(tcw); + this.syncHeaderScroll(); + }, + + getVisibleColumns: function () { + var columns = [], + cs = this.columns, + len = cs.length, + i; + + for (i = 0; i < len; i++) { + if (!cs[i].hidden) { + columns.push(cs[i]); + } + } + return columns; + }, + + getTotalColumnWidth: function () { + var total = 0; + for ( + var i = 0, cs = this.getVisibleColumns(), len = cs.length; + i < len; + i++ + ) { + total += cs[i].width; + } + return total; + }, + + setScrollOffset: function (scrollOffset) { + this.scrollOffset = scrollOffset; + this.updateColumnWidths(); + }, + + // private + handleHdDown: function (e, t) { + var hd = e.getTarget('.x-treegrid-hd'); + + if (hd && Ext.fly(t).hasClass('x-grid3-hd-btn')) { + var ms = this.hmenu.items, + cs = this.columns, + index = this.findHeaderIndex(hd), + c = cs[index], + sort = c.sortable; + + e.stopEvent(); + Ext.fly(hd).addClass('x-grid3-hd-menu-open'); + this.hdCtxIndex = index; + + this.fireEvent('headerbuttonclick', ms, c, hd, index); + + this.hmenu.on( + 'hide', + function () { + Ext.fly(hd).removeClass('x-grid3-hd-menu-open'); + }, + this, + { single: true } + ); + + this.hmenu.show(t, 'tl-bl?'); + } else if (hd) { + var index = this.findHeaderIndex(hd); + this.fireEvent('headerclick', this.columns[index], hd, index); + } + }, + + // private + handleHdOver: function (e, t) { + var hd = e.getTarget('.x-treegrid-hd'); + if (hd && !this.headersDisabled) { + index = this.findHeaderIndex(hd); + this.activeHdRef = t; + this.activeHdIndex = index; + var el = Ext.get(hd); + this.activeHdRegion = el.getRegion(); + el.addClass('x-grid3-hd-over'); + this.activeHdBtn = el.child('.x-grid3-hd-btn'); + if (this.activeHdBtn) { + this.activeHdBtn.dom.style.height = + hd.firstChild.offsetHeight - 1 + 'px'; + } + } + }, + + // private + handleHdOut: function (e, t) { + var hd = e.getTarget('.x-treegrid-hd'); + if (hd && (!Ext.isIE || !e.within(hd, true))) { + this.activeHdRef = null; + Ext.fly(hd).removeClass('x-grid3-hd-over'); + hd.style.cursor = ''; + } + }, + + findHeaderIndex: function (hd) { + hd = hd.dom || hd; + var cs = hd.parentNode.childNodes; + for (var i = 0, c; (c = cs[i]); i++) { + if (c == hd) { + return i; + } + } + return -1; + }, + + // private + beforeColMenuShow: function () { + var cols = this.columns, + colCount = cols.length, + i, + c; + this.colMenu.removeAll(); + for (i = 1; i < colCount; i++) { + c = cols[i]; + if (c.hideable !== false) { + this.colMenu.add( + new Ext.menu.CheckItem({ + itemId: 'col-' + i, + text: c.header, + checked: !c.hidden, + hideOnClick: false, + disabled: c.hideable === false, + }) + ); + } + } + }, + + // private + handleHdMenuClick: function (item) { + var index = this.hdCtxIndex, + id = item.getItemId(); + + if ( + this.fireEvent( + 'headermenuclick', + this.columns[index], + id, + index + ) !== false + ) { + index = id.substr(4); + if (index > 0 && this.columns[index]) { + this.setColumnVisible(index, !item.checked); + } + } + + return true; + }, + + setColumnVisible: function (index, visible) { + this.columns[index].hidden = !visible; + this.updateColumnWidths(); + }, + + /** + * Scrolls the grid to the top + */ + scrollToTop: function () { + this.innerBody.dom.scrollTop = 0; + this.innerBody.dom.scrollLeft = 0; + }, + + // private + syncScroll: function () { + this.syncHeaderScroll(); + var mb = this.innerBody.dom; + this.fireEvent('bodyscroll', mb.scrollLeft, mb.scrollTop); + }, + + // private + syncHeaderScroll: function () { + var mb = this.innerBody.dom; + this.innerHd.dom.scrollLeft = mb.scrollLeft; + this.innerHd.dom.scrollLeft = mb.scrollLeft; // second time for IE (1/2 time first fails, other browsers ignore) + }, + + registerNode: function (n) { + Ext.ux.tree.TreeGrid.superclass.registerNode.call(this, n); + if (!n.uiProvider && !n.isRoot && !n.ui.isTreeGridNodeUI) { + n.ui = new Ext.ux.tree.TreeGridNodeUI(n); + } + }, +}); + +Ext.reg('treegrid', Ext.ux.tree.TreeGrid); diff --git a/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGridColumnResizer.js b/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGridColumnResizer.js new file mode 100644 index 0000000..de73608 --- /dev/null +++ b/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGridColumnResizer.js @@ -0,0 +1,123 @@ +/** + * Ext JS Library 3.4.0 + * Copyright(c) 2006-2011 Sencha Inc. + * licensing@sencha.com + * http://www.sencha.com/license + */ +/** + * @class Ext.tree.ColumnResizer + * @extends Ext.util.Observable + */ +Ext.tree.ColumnResizer = Ext.extend(Ext.util.Observable, { + /** + * @cfg {Number} minWidth The minimum width the column can be dragged to. + * Defaults to <tt>14</tt>. + */ + minWidth: 14, + + constructor: function (config) { + Ext.apply(this, config); + Ext.tree.ColumnResizer.superclass.constructor.call(this); + }, + + init: function (tree) { + this.tree = tree; + tree.on('render', this.initEvents, this); + }, + + initEvents: function (tree) { + tree.mon(tree.innerHd, 'mousemove', this.handleHdMove, this); + this.tracker = new Ext.dd.DragTracker({ + onBeforeStart: this.onBeforeStart.createDelegate(this), + onStart: this.onStart.createDelegate(this), + onDrag: this.onDrag.createDelegate(this), + onEnd: this.onEnd.createDelegate(this), + tolerance: 3, + autoStart: 300, + }); + this.tracker.initEl(tree.innerHd); + tree.on('beforedestroy', this.tracker.destroy, this.tracker); + }, + + handleHdMove: function (e, t) { + var hw = 5, + x = e.getPageX(), + hd = e.getTarget('.x-treegrid-hd', 3, true); + + if (hd) { + var r = hd.getRegion(), + ss = hd.dom.style, + pn = hd.dom.parentNode; + + if (x - r.left <= hw && hd.dom !== pn.firstChild) { + var ps = hd.dom.previousSibling; + while (ps && Ext.fly(ps).hasClass('x-treegrid-hd-hidden')) { + ps = ps.previousSibling; + } + if (ps) { + this.activeHd = Ext.get(ps); + ss.cursor = Ext.isWebKit ? 'e-resize' : 'col-resize'; + } + } else if (r.right - x <= hw) { + var ns = hd.dom; + while (ns && Ext.fly(ns).hasClass('x-treegrid-hd-hidden')) { + ns = ns.previousSibling; + } + if (ns) { + this.activeHd = Ext.get(ns); + ss.cursor = Ext.isWebKit ? 'w-resize' : 'col-resize'; + } + } else { + delete this.activeHd; + ss.cursor = ''; + } + } + }, + + onBeforeStart: function (e) { + this.dragHd = this.activeHd; + return !!this.dragHd; + }, + + onStart: function (e) { + this.dragHeadersDisabled = this.tree.headersDisabled; + this.tree.headersDisabled = true; + this.proxy = this.tree.body.createChild({ cls: 'x-treegrid-resizer' }); + this.proxy.setHeight(this.tree.body.getHeight()); + + var x = this.tracker.getXY()[0]; + + this.hdX = this.dragHd.getX(); + this.hdIndex = this.tree.findHeaderIndex(this.dragHd); + + this.proxy.setX(this.hdX); + this.proxy.setWidth(x - this.hdX); + + this.maxWidth = + this.tree.outerCt.getWidth() - + this.tree.innerBody.translatePoints(this.hdX).left; + }, + + onDrag: function (e) { + var cursorX = this.tracker.getXY()[0]; + this.proxy.setWidth( + (cursorX - this.hdX).constrain(this.minWidth, this.maxWidth) + ); + }, + + onEnd: function (e) { + var nw = this.proxy.getWidth(), + tree = this.tree, + disabled = this.dragHeadersDisabled; + + this.proxy.remove(); + delete this.dragHd; + + tree.columns[this.hdIndex].width = nw; + tree.updateColumnWidths(); + + setTimeout(function () { + tree.headersDisabled = disabled; + }, 100); + }, +}); diff --git a/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGridColumns.js b/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGridColumns.js new file mode 100644 index 0000000..0c88f17 --- /dev/null +++ b/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGridColumns.js @@ -0,0 +1,40 @@ +/** + * Ext JS Library 3.4.0 + * Copyright(c) 2006-2011 Sencha Inc. + * licensing@sencha.com + * http://www.sencha.com/license + */ +(function () { + Ext.override(Ext.list.Column, { + init: function () { + var types = Ext.data.Types, + st = this.sortType; + + if (this.type) { + if (Ext.isString(this.type)) { + this.type = + Ext.data.Types[this.type.toUpperCase()] || types.AUTO; + } + } else { + this.type = types.AUTO; + } + + // named sortTypes are supported, here we look them up + if (Ext.isString(st)) { + this.sortType = Ext.data.SortTypes[st]; + } else if (Ext.isEmpty(st)) { + this.sortType = this.type.sortType; + } + }, + }); + + Ext.tree.Column = Ext.extend(Ext.list.Column, {}); + Ext.tree.NumberColumn = Ext.extend(Ext.list.NumberColumn, {}); + Ext.tree.DateColumn = Ext.extend(Ext.list.DateColumn, {}); + Ext.tree.BooleanColumn = Ext.extend(Ext.list.BooleanColumn, {}); + + Ext.reg('tgcolumn', Ext.tree.Column); + Ext.reg('tgnumbercolumn', Ext.tree.NumberColumn); + Ext.reg('tgdatecolumn', Ext.tree.DateColumn); + Ext.reg('tgbooleancolumn', Ext.tree.BooleanColumn); +})(); diff --git a/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGridLoader.js b/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGridLoader.js new file mode 100644 index 0000000..db14848 --- /dev/null +++ b/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGridLoader.js @@ -0,0 +1,18 @@ +/** + * Ext JS Library 3.4.0 + * Copyright(c) 2006-2011 Sencha Inc. + * licensing@sencha.com + * http://www.sencha.com/license + */ +/** + * @class Ext.ux.tree.TreeGridLoader + * @extends Ext.tree.TreeLoader + */ +Ext.ux.tree.TreeGridLoader = Ext.extend(Ext.tree.TreeLoader, { + createNode: function (attr) { + if (!attr.uiProvider) { + attr.uiProvider = Ext.ux.tree.TreeGridNodeUI; + } + return Ext.tree.TreeLoader.prototype.createNode.call(this, attr); + }, +}); diff --git a/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGridNodeUI.js b/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGridNodeUI.js new file mode 100644 index 0000000..09b1718 --- /dev/null +++ b/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGridNodeUI.js @@ -0,0 +1,149 @@ +/** + * Ext JS Library 3.4.0 + * Copyright(c) 2006-2011 Sencha Inc. + * licensing@sencha.com + * http://www.sencha.com/license + */ +/** + * @class Ext.ux.tree.TreeGridNodeUI + * @extends Ext.tree.TreeNodeUI + */ +Ext.ux.tree.TreeGridNodeUI = Ext.extend(Ext.tree.TreeNodeUI, { + isTreeGridNodeUI: true, + + renderElements: function (n, a, targetNode, bulkRender) { + var t = n.getOwnerTree(), + cols = t.columns, + c = cols[0], + i, + buf, + len; + + this.indentMarkup = n.parentNode + ? n.parentNode.ui.getChildIndent() + : ''; + + buf = [ + '<tbody class="x-tree-node">', + '<tr ext:tree-node-id="', + n.id, + '" class="x-tree-node-el x-tree-node-leaf ', + a.cls, + '">', + '<td class="x-treegrid-col">', + '<span class="x-tree-node-indent">', + this.indentMarkup, + '</span>', + '<img src="', + this.emptyIcon, + '" class="x-tree-ec-icon x-tree-elbow" />', + '<img src="', + a.icon || this.emptyIcon, + '" class="x-tree-node-icon', + a.icon ? ' x-tree-node-inline-icon' : '', + a.iconCls ? ' ' + a.iconCls : '', + '" unselectable="on" />', + '<a hidefocus="on" class="x-tree-node-anchor" href="', + a.href ? a.href : '#', + '" tabIndex="1" ', + a.hrefTarget ? ' target="' + a.hrefTarget + '"' : '', + '>', + '<span unselectable="on">', + c.tpl ? c.tpl.apply(a) : a[c.dataIndex] || c.text, + '</span></a>', + '</td>', + ]; + + for (i = 1, len = cols.length; i < len; i++) { + c = cols[i]; + buf.push( + '<td class="x-treegrid-col ', + c.cls ? c.cls : '', + '">', + '<div unselectable="on" class="x-treegrid-text"', + c.align ? ' style="text-align: ' + c.align + ';"' : '', + '>', + c.tpl ? c.tpl.apply(a) : a[c.dataIndex], + '</div>', + '</td>' + ); + } + + buf.push( + '</tr><tr class="x-tree-node-ct"><td colspan="', + cols.length, + '">', + '<table class="x-treegrid-node-ct-table" cellpadding="0" cellspacing="0" style="table-layout: fixed; display: none; width: ', + t.innerCt.getWidth(), + 'px;"><colgroup>' + ); + for (i = 0, len = cols.length; i < len; i++) { + buf.push( + '<col style="width: ', + cols[i].hidden ? 0 : cols[i].width, + 'px;" />' + ); + } + buf.push('</colgroup></table></td></tr></tbody>'); + + if (bulkRender !== true && n.nextSibling && n.nextSibling.ui.getEl()) { + this.wrap = Ext.DomHelper.insertHtml( + 'beforeBegin', + n.nextSibling.ui.getEl(), + buf.join('') + ); + } else { + this.wrap = Ext.DomHelper.insertHtml( + 'beforeEnd', + targetNode, + buf.join('') + ); + } + + this.elNode = this.wrap.childNodes[0]; + this.ctNode = this.wrap.childNodes[1].firstChild.firstChild; + var cs = this.elNode.firstChild.childNodes; + this.indentNode = cs[0]; + this.ecNode = cs[1]; + this.iconNode = cs[2]; + this.anchor = cs[3]; + this.textNode = cs[3].firstChild; + }, + + // private + animExpand: function (cb) { + this.ctNode.style.display = ''; + Ext.ux.tree.TreeGridNodeUI.superclass.animExpand.call(this, cb); + }, +}); + +Ext.ux.tree.TreeGridRootNodeUI = Ext.extend(Ext.tree.TreeNodeUI, { + isTreeGridNodeUI: true, + + // private + render: function () { + if (!this.rendered) { + this.wrap = this.ctNode = this.node.ownerTree.innerCt.dom; + this.node.expanded = true; + } + + if (Ext.isWebKit) { + // weird table-layout: fixed issue in webkit + var ct = this.ctNode; + ct.style.tableLayout = null; + (function () { + ct.style.tableLayout = 'fixed'; + }.defer(1)); + } + }, + + destroy: function () { + if (this.elNode) { + Ext.dd.Registry.unregister(this.elNode.id); + } + delete this.node; + }, + + collapse: Ext.emptyFn, + expand: Ext.emptyFn, +}); diff --git a/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGridNodeUIFix.js b/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGridNodeUIFix.js new file mode 100644 index 0000000..7708bd7 --- /dev/null +++ b/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGridNodeUIFix.js @@ -0,0 +1,33 @@ +/** + * Ext.ux.tree.TreeGridNodeUIFix.js + * + * Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com> + * + * This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with + * the additional special exception to link portions of this program with the OpenSSL library. + * See LICENSE for more details. + */ + +Ext.override(Ext.ux.tree.TreeGridNodeUI, { + updateColumns: function () { + if (!this.rendered) return; + + var a = this.node.attributes, + t = this.node.getOwnerTree(), + cols = t.columns, + c = cols[0]; + + // Update the first column + this.anchor.firstChild.innerHTML = c.tpl + ? c.tpl.apply(a) + : a[c.dataIndex] || c.text; + + // Update the remaining columns + for (i = 1, len = cols.length; i < len; i++) { + c = cols[i]; + this.elNode.childNodes[i].firstChild.innerHTML = c.tpl + ? c.tpl.apply(a) + : a[c.dataIndex] || c.text; + } + }, +}); diff --git a/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGridRenderColumn.js b/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGridRenderColumn.js new file mode 100644 index 0000000..ed95d95 --- /dev/null +++ b/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGridRenderColumn.js @@ -0,0 +1,9 @@ +Ext.tree.RenderColumn = Ext.extend(Ext.tree.Column, { + constructor: function (c) { + c.tpl = c.tpl || new Ext.XTemplate('{' + c.dataIndex + ':this.format}'); + c.tpl.format = c.renderer; + c.tpl.col = this; + Ext.tree.RenderColumn.superclass.constructor.call(this, c); + }, +}); +Ext.reg('tgrendercolumn', Ext.tree.RenderColumn); diff --git a/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGridSorter.js b/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGridSorter.js new file mode 100644 index 0000000..fdf1f38 --- /dev/null +++ b/deluge/ui/web/js/extjs/ext-extensions/tree/TreeGridSorter.js @@ -0,0 +1,158 @@ +/** + * Ext JS Library 3.4.0 + * Copyright(c) 2006-2011 Sencha Inc. + * licensing@sencha.com + * http://www.sencha.com/license + */ +Ext.ns('Ext.ux.tree'); + +/** + * @class Ext.ux.tree.TreeGridSorter + * @extends Ext.tree.TreeSorter + * Provides sorting of nodes in a {@link Ext.ux.tree.TreeGrid}. The TreeGridSorter automatically monitors events on the + * associated TreeGrid that might affect the tree's sort order (beforechildrenrendered, append, insert and textchange). + * Example usage:<br /> + * <pre><code> + new Ext.ux.tree.TreeGridSorter(myTreeGrid, { + folderSort: true, + dir: "desc", + sortType: function(node) { + // sort by a custom, typed attribute: + return parseInt(node.id, 10); + } + }); + </code></pre> + * @constructor + * @param {TreeGrid} tree + * @param {Object} config + */ +Ext.ux.tree.TreeGridSorter = Ext.extend(Ext.tree.TreeSorter, { + /** + * @cfg {Array} sortClasses The CSS classes applied to a header when it is sorted. (defaults to <tt>['sort-asc', 'sort-desc']</tt>) + */ + sortClasses: ['sort-asc', 'sort-desc'], + /** + * @cfg {String} sortAscText The text displayed in the 'Sort Ascending' menu item (defaults to <tt>'Sort Ascending'</tt>) + */ + sortAscText: 'Sort Ascending', + /** + * @cfg {String} sortDescText The text displayed in the 'Sort Descending' menu item (defaults to <tt>'Sort Descending'</tt>) + */ + sortDescText: 'Sort Descending', + + constructor: function (tree, config) { + if (!Ext.isObject(config)) { + config = { + property: tree.columns[0].dataIndex || 'text', + folderSort: true, + }; + } + + Ext.ux.tree.TreeGridSorter.superclass.constructor.apply( + this, + arguments + ); + + this.tree = tree; + tree.on('headerclick', this.onHeaderClick, this); + tree.ddAppendOnly = true; + + var me = this; + this.defaultSortFn = function (n1, n2) { + var desc = me.dir && me.dir.toLowerCase() == 'desc', + prop = me.property || 'text', + sortType = me.sortType, + caseSensitive = me.caseSensitive === true, + leafAttr = me.leafAttr || 'leaf', + attr1 = n1.attributes, + attr2 = n2.attributes; + + if (me.folderSort) { + if (attr1[leafAttr] && !attr2[leafAttr]) { + return 1; + } + if (!attr1[leafAttr] && attr2[leafAttr]) { + return -1; + } + } + var prop1 = attr1[prop], + prop2 = attr2[prop], + v1 = sortType + ? sortType(prop1) + : caseSensitive + ? prop1 + : prop1.toUpperCase(); + v2 = sortType + ? sortType(prop2) + : caseSensitive + ? prop2 + : prop2.toUpperCase(); + + if (v1 < v2) { + return desc ? +1 : -1; + } else if (v1 > v2) { + return desc ? -1 : +1; + } else { + return 0; + } + }; + + tree.on('afterrender', this.onAfterTreeRender, this, { single: true }); + tree.on('headermenuclick', this.onHeaderMenuClick, this); + }, + + onAfterTreeRender: function () { + if (this.tree.hmenu) { + this.tree.hmenu.insert( + 0, + { + itemId: 'asc', + text: this.sortAscText, + cls: 'xg-hmenu-sort-asc', + }, + { + itemId: 'desc', + text: this.sortDescText, + cls: 'xg-hmenu-sort-desc', + } + ); + } + this.updateSortIcon(0, 'asc'); + }, + + onHeaderMenuClick: function (c, id, index) { + if (id === 'asc' || id === 'desc') { + this.onHeaderClick(c, null, index); + return false; + } + }, + + onHeaderClick: function (c, el, i) { + if (c && !this.tree.headersDisabled) { + var me = this; + + me.property = c.dataIndex; + me.dir = c.dir = c.dir === 'desc' ? 'asc' : 'desc'; + me.sortType = c.sortType; + me.caseSensitive === Ext.isBoolean(c.caseSensitive) + ? c.caseSensitive + : this.caseSensitive; + me.sortFn = c.sortFn || this.defaultSortFn; + + this.tree.root.cascade(function (n) { + if (!n.isLeaf()) { + me.updateSort(me.tree, n); + } + }); + + this.updateSortIcon(i, c.dir); + } + }, + + // private + updateSortIcon: function (col, dir) { + var sc = this.sortClasses, + hds = this.tree.innerHd.select('td').removeClass(sc); + hds.item(col).addClass(sc[dir == 'desc' ? 1 : 0]); + }, +}); |