/** * The FocusManager is responsible for managing the following according to WAI ARIA practices: * * 1. Component focus * 2. Keyboard navigation * 3. Provide a visual cue for focused components, in the form of a focus ring/frame. * */ Ext.define('Ext.aria.FocusManager', { singleton: true, requires: [ 'Ext.util.KeyNav', 'Ext.util.Observable' ], mixins: { observable: 'Ext.util.Observable' }, /** * @property {Boolean} enabled * Whether or not the FocusManager is currently enabled */ enabled: false, /** * @event disable * Fires when the FocusManager is disabled * @param {Ext.aria.FocusManager} fm A reference to the FocusManager singleton */ /** * @event enable * Fires when the FocusManager is enabled * @param {Ext.aria.FocusManager} fm A reference to the FocusManager singleton */ // Array to keep track of open windows windows: [], constructor: function(config) { var me = this, whitelist = me.whitelist, cache, i, len; me.mixins.observable.constructor.call(me, config); }, /** * @private * Enables the FocusManager by turning on window management and keyboard navigation */ enable: function() { var me = this, doc = Ext.getDoc(); if (me.enabled) { return; } // initDom will call addFocusListener which needs the FocusManager to be enabled me.enabled = true; // map F6 to toggle focus among open windows me.toggleKeyMap = new Ext.util.KeyMap({ target: doc, scope: me, defaultEventAction: 'stopEvent', key: Ext.event.Event.F6, fn: me.toggleWindow }); me.fireEvent('enable', me); }, onComponentBlur: function(cmp, e) { var me = this; if (me.focusedCmp === cmp) { me.previousFocusedCmp = cmp; } Ext.globalEvents.fireEvent('componentblur', me, cmp, me.previousFocusedCmp); return false; }, onComponentFocus: function(cmp, e) { var me = this; if (Ext.globalEvents.fireEvent('beforecomponentfocus', me, cmp, me.previousFocusedCmp) === false) { me.clearComponent(cmp); return; } me.focusedCmp = cmp; return false; }, // This should be fixed in https://sencha.jira.com/browse/EXTJS-14124 onComponentHide: Ext.emptyFn, toggleWindow: function(key, e) { var me = this, windows = me.windows, length = windows.length, focusedCmp = me.focusedCmp, curIndex = 0, newIndex = 0, current; if (length === 1) { return; } current = focusedCmp.isWindow ? focusedCmp : focusedCmp.up('window'); if (current) { curIndex = me.findWindowIndex(current); } if (e.shiftKey) { newIndex = curIndex - 1; if (newIndex < 0) { newIndex = length - 1; } } else { newIndex = curIndex + 1; if (newIndex === length) { newIndex = 0; } } current = windows[newIndex]; if (current.cmp.isWindow) { current.cmp.toFront(); } current.cmp.focus(false, 100); return false; }, addWindow: function(window) { var me = this, win = { cmp: window }; me.windows.push(win); }, removeWindow: function(window) { var me = this, windows = me.windows, current; if (windows.length === 1) { return; } current = me.findWindowIndex(window); if (current >= 0) { Ext.Array.erase(windows, current, 1); } }, findWindowIndex: function(window) { var me = this, windows = me.windows, length = windows.length, curIndex = -1, i; for (i = 0; i < length; i++) { if (windows[i].cmp === window) { curIndex = i; break; } } return curIndex; } }, function() { var mgr = Ext['FocusManager'] = Ext.aria.FocusManager; Ext.onReady(function() { mgr.enable(); }); }); /** */ Ext.define('Ext.aria.Component', { override: 'Ext.Component', requires: [ 'Ext.aria.FocusManager' ], /** * @cfg {String} [ariaRole] ARIA role for this Component, defaults to no role. * With no role, no other ARIA attributes are set. */ /** * @cfg {String} [ariaLabel] ARIA label for this Component. It is best to use * {@link #ariaLabelledBy} option instead, because screen readers prefer * aria-labelledby attribute to aria-label. {@link #ariaLabel} and {@link #ariaLabelledBy} * config options are mutually exclusive. */ /** * @cfg {String} [ariaLabelledBy] DOM selector for a child element that is to be used * as label for this Component, set in aria-labelledby attribute. * If the selector is by #id, the label element can be any existing element, * not necessarily a child of the main Component element. * * {@link #ariaLabelledBy} and {@link #ariaLabel} config options are mutually exclusive, * and ariaLabel has the higher precedence. */ /** * @cfg {String} [ariaDescribedBy] DOM selector for a child element that is to be used * as description for this Component, set in aria-describedby attribute. * The selector works the same way as {@link #ariaLabelledBy} */ /** * @cfg {Object} [ariaAttributes] An object containing ARIA attributes to be set * on this Component's ARIA element. Use this to set the attributes that cannot be * determined by the Component's state, such as `aria-live`, `aria-flowto`, etc. */ /** * @cfg {Boolean} [ariaRenderAttributesToElement=true] ARIA attributes are usually * rendered into the main element of the Component using autoEl config option. * However for certain Components (form fields, etc.) the main element is * presentational and ARIA attributes should be rendered into child elements * of the Component markup; this is done using the Component templates. * * If this flag is set to `true` (default), the ARIA attributes will be applied * to the main element. * @private */ ariaRenderAttributesToElement: true, statics: { ariaHighContrastModeCls: Ext.baseCSSPrefix + 'aria-highcontrast' }, // Several of the attributes, like aria-controls and aria-activedescendant // need to refer to element ids which are not available at render time ariaApplyAfterRenderAttributes: function() { var me = this, role = me.ariaRole, attrs; if (role !== 'presentation') { attrs = me.ariaGetAfterRenderAttributes(); me.ariaUpdate(attrs); } }, ariaGetRenderAttributes: function() { var me = this, role = me.ariaRole, attrs = { role: role }; // It does not make much sense to set ARIA attributes // on purely presentational Component, or on a Component // with no ARIA role specified if (role === 'presentation' || role === undefined) { return attrs; } if (me.hidden) { attrs['aria-hidden'] = true; } if (me.disabled) { attrs['aria-disabled'] = true; } if (me.ariaLabel) { attrs['aria-label'] = me.ariaLabel; } Ext.apply(attrs, me.ariaAttributes); return attrs; }, ariaGetAfterRenderAttributes: function() { var me = this, attrs = {}, el; if (!me.ariaLabel && me.ariaLabelledBy) { el = me.ariaGetLabelEl(me.ariaLabelledBy); if (el) { attrs['aria-labelledby'] = el.id; } } if (me.ariaDescribedBy) { el = me.ariaGetLabelEl(me.ariaDescribedBy); if (el) { attrs['aria-describedby'] = el.id; } } return attrs; }, /** * Updates the component's element properties * @private * @param {Ext.Element} [el] The element to set properties on * @param {Object[]} props Array of properties (name: value) */ ariaUpdate: function(el, props) { // The one argument form updates the default ariaEl if (arguments.length === 1) { props = el; el = this.ariaGetEl(); } if (!el) { return; } el.set(props); }, /** * Return default ARIA element for this Component * @private * @return {Ext.Element} ARIA element */ ariaGetEl: function() { return this.el; }, /** * @private * Return default ARIA labelled-by element for this Component, if any * * @param {String} [selector] Element selector * * @return {Ext.Element} Label element, or null */ ariaGetLabelEl: function(selector) { var me = this, el = null; if (selector) { if (/^#/.test(selector)) { selector = selector.replace(/^#/, ''); el = Ext.get(selector); } else { el = me.ariaGetEl().down(selector); } } return el; }, // Unlike getFocusEl, this one always returns Ext.Element ariaGetFocusEl: function() { var el = this.getFocusEl(); while (el.isComponent) { el = el.getFocusEl(); } return el; }, onFocus: function(e, t, eOpts) { var me = this, mgr = Ext.aria.FocusManager, tip, el; me.callParent(arguments); if (me.tooltip && Ext.quickTipsActive) { tip = Ext.tip.QuickTipManager.getQuickTip(); el = me.ariaGetEl(); tip.cancelShow(el); tip.showByTarget(el); } if (me.hasFocus && mgr.enabled) { return mgr.onComponentFocus(me); } }, onBlur: function(e, t, eOpts) { var me = this, mgr = Ext.aria.FocusManager; me.callParent(arguments); if (me.tooltip && Ext.quickTipsActive) { Ext.tip.QuickTipManager.getQuickTip().cancelShow(me.ariaGetEl()); } if (!me.hasFocus && mgr.enabled) { return mgr.onComponentBlur(me); } }, onDisable: function() { var me = this; me.callParent(arguments); me.ariaUpdate({ 'aria-disabled': true }); }, onEnable: function() { var me = this; me.callParent(arguments); me.ariaUpdate({ 'aria-disabled': false }); }, onHide: function() { var me = this; me.callParent(arguments); me.ariaUpdate({ 'aria-hidden': true }); }, onShow: function() { var me = this; me.callParent(arguments); me.ariaUpdate({ 'aria-hidden': false }); } }, function() { function detectHighContrastMode() { /* Absolute URL for test image * (data URIs are not supported by all browsers, and not properly removed when images are disabled in Firefox) */ var imgSrc = "http://www.html5accessibility.com/tests/clear.gif", supports = {}, div = document.createElement("div"), divEl = Ext.get(div), divStyle = div.style, img = document.createElement("img"), supports = { images: true, backgroundImages: true, borderColors: true, highContrastMode: false, lightOnDark: false }; /* create div for testing if high contrast mode is on or images are turned off */ div.id = "ui-helper-high-contrast"; div.className = "ui-helper-hidden-accessible"; divStyle.borderWidth = "1px"; divStyle.borderStyle = "solid"; divStyle.borderTopColor = "#F00"; divStyle.borderRightColor = "#FF0"; divStyle.backgroundColor = "#FFF"; divStyle.width = "2px"; /* For IE, div must be wider than the image inside it when hidden off screen */ img.alt = ""; div.appendChild(img); document.body.appendChild(div); divStyle.backgroundImage = "url(" + imgSrc + ")"; img.src = imgSrc; var getColorValue = function(colorTxt) { var values = [], colorValue = 0, match; if (colorTxt.indexOf("rgb(") !== -1) { values = colorTxt.replace("rgb(", "").replace(")", "").split(", "); } else if (colorTxt.indexOf("#") !== -1) { match = colorTxt.match(colorTxt.length === 7 ? /^#(\S\S)(\S\S)(\S\S)$/ : /^#(\S)(\S)(\S)$/); if (match) { values = [ "0x" + match[1], "0x" + match[2], "0x" + match[3] ]; } } for (var i = 0; i < values.length; i++) { colorValue += parseInt(values[i]); } return colorValue; }; var performCheck = function(event) { var bkImg = divEl.getStyle("backgroundImage"), body = Ext.getBody(); supports.images = img.offsetWidth === 1; supports.backgroundImages = !(bkImg !== null && (bkImg === "none" || bkImg === "url(invalid-url:)")); supports.borderColors = !(divEl.getStyle("borderTopColor") === divEl.getStyle("borderRightColor")); supports.highContrastMode = !supports.images || !supports.backgroundImages; supports.lightOnDark = getColorValue(divEl.getStyle("color")) - getColorValue(divEl.getStyle("backgroundColor")) > 0; if (Ext.isIE) { div.outerHTML = ""; } else /* prevent mixed-content warning, see http://support.microsoft.com/kb/925014 */ { document.body.removeChild(div); } }; performCheck(); return supports; } Ext.enableAria = true; Ext.onReady(function() { var supports = Ext.supports, flags, div; flags = Ext.isWindows ? detectHighContrastMode() : {}; supports.HighContrastMode = !!flags.highContrastMode; if (supports.HighContrastMode) { Ext.getBody().addCls(Ext.Component.ariaHighContrastModeCls); } }); }); /** */ Ext.define('Ext.aria.Img', { override: 'Ext.Img', getElConfig: function() { var me = this, config; config = me.callParent(); // Screen reader software requires images to have tabIndex config.tabIndex = -1; return config; }, onRender: function() { var me = this; // if (!me.alt) { Ext.log.warn('For ARIA compliance, IMG elements SHOULD have an alt attribute'); } // me.callParent(); } }); /** */ Ext.define('Ext.aria.panel.Tool', { override: 'Ext.panel.Tool', requires: [ 'Ext.aria.Component', 'Ext.util.KeyMap' ], tabIndex: 0, destroy: function() { if (this.keyMap) { this.keyMap.destroy(); } this.callParent(); }, ariaAddKeyMap: function(params) { var me = this; me.keyMap = new Ext.util.KeyMap(Ext.apply({ target: me.el }, params)); }, ariaGetRenderAttributes: function() { var me = this, attrs; attrs = me.callParent(arguments); if (me.tooltip && me.tooltipType === 'qtip') { attrs['aria-label'] = me.tooltip; } return attrs; } }); /** */ Ext.define('Ext.aria.panel.Panel', { override: 'Ext.panel.Panel', closeText: 'Close Panel', collapseText: 'Collapse Panel', expandText: 'Expand Panel', untitledText: 'Untitled Panel', onBoxReady: function() { var me = this, Event = Ext.event.Event, collapseTool = me.collapseTool, header, tools, i, len; me.callParent(); if (collapseTool) { collapseTool.ariaUpdate({ 'aria-label': me.collapsed ? me.expandText : me.collapseText }); collapseTool.ariaAddKeyMap({ key: [ Event.ENTER, Event.SPACE ], handler: me.toggleCollapse, scope: me }); } if (me.closable) { toolBtn = me.down('tool[type=close]'); if (toolBtn) { toolBtn.ariaUpdate({ 'aria-label': me.closeText }); toolBtn.ariaAddKeyMap({ key: [ Event.ENTER, Event.SPACE ], handler: me.close, scope: me }); } } header = me.getHeader(); }, setTitle: function(newTitle) { var me = this; me.callParent(arguments); me.ariaUpdate({ 'aria-label': newTitle }); }, createReExpander: function(direction, defaults) { var me = this, Event = Ext.event.Event, opposite, result, tool; opposite = me.getOppositeDirection(direction); result = me.callParent(arguments); tool = result.down('tool[type=expand-' + opposite + ']'); if (tool) { tool.on('boxready', function() { tool.ariaUpdate({ 'aria-label': me.collapsed ? me.expandText : me.collapseText }); tool.ariaAddKeyMap({ key: [ Event.ENTER, Event.SPACE ], handler: me.toggleCollapse, scope: me }); }, { single: true }); } return result; }, ariaGetRenderAttributes: function() { var me = this, attrs; attrs = me.callParent(); if (me.collapsible) { attrs['aria-expanded'] = !me.collapsed; } return attrs; }, ariaGetAfterRenderAttributes: function() { var me = this, newAttrs = {}, attrs, toolBtn, textEl; attrs = me.callParent(arguments); if (me.ariaRole === 'presentation') { return attrs; } if (me.title) { textEl = me.ariaGetTitleTextEl(); if (textEl) { newAttrs = { 'aria-labelledby': textEl.id }; } else { newAttrs = { 'aria-label': me.title }; } } else if (me.ariaLabel) { newAttrs = { 'aria-label': me.ariaLabel }; } Ext.apply(attrs, newAttrs); return attrs; }, ariaGetTitleTextEl: function() { var header = this.header; return header && header.titleCmp && header.titleCmp.textEl || null; }, afterExpand: function() { var me = this; me.callParent(arguments); me.ariaUpdate({ 'aria-expanded': true }); if (me.collapseTool) { me.ariaUpdate(me.collapseTool.getEl(), { 'aria-label': me.collapseText }); } }, afterCollapse: function() { var me = this; me.callParent(arguments); me.ariaUpdate({ 'aria-expanded': false }); if (me.collapseTool) { me.ariaUpdate(me.collapseTool.getEl(), { 'aria-label': me.expandText }); } } }); /** */ Ext.define('Ext.aria.form.field.Base', { override: 'Ext.form.field.Base', requires: [ 'Ext.util.Format', 'Ext.aria.Component' ], /** * @cfg {String} formatText The text to use for the field format announcement * placed in the `title` attribute of the input field. This format will not * be used if the title attribute is configured explicitly. */ ariaRenderAttributesToElement: false, msgTarget: 'side', // use this scheme because it is the only one working for now getSubTplData: function() { var me = this, fmt = Ext.util.Format.attributes, data, attrs; data = me.callParent(arguments); attrs = me.ariaGetRenderAttributes(); // Role is rendered separately delete attrs.role; data.inputAttrTpl = [ data.inputAttrTpl, fmt(attrs) ].join(' '); return data; }, ariaGetEl: function() { return this.inputEl; }, ariaGetRenderAttributes: function() { var me = this, readOnly = me.readOnly, formatText = me.formatText, attrs; attrs = me.callParent(); if (readOnly != null) { attrs['aria-readonly'] = !!readOnly; } if (formatText && !attrs.title) { attrs.title = Ext.String.format(formatText, me.format); } return attrs; }, ariaGetAfterRenderAttributes: function() { var me = this, labelEl = me.labelEl, attrs; attrs = me.callParent(); if (labelEl) { attrs['aria-labelledby'] = labelEl.id; } return attrs; }, setReadOnly: function(readOnly) { var me = this; me.callParent(arguments); me.ariaUpdate({ 'aria-readonly': readOnly }); }, markInvalid: function(f, isValid) { var me = this; me.callParent(arguments); me.ariaUpdate({ 'aria-invalid': true }); }, clearInvalid: function() { var me = this; me.callParent(arguments); me.ariaUpdate({ 'aria-invalid': false }); } }); /** */ Ext.define('Ext.aria.form.field.Display', { override: 'Ext.form.field.Display', requires: [ 'Ext.aria.form.field.Base' ], msgTarget: 'none', ariaGetRenderAttributes: function() { var me = this, attrs; attrs = me.callParent(); attrs['aria-readonly'] = true; return attrs; } }); /** */ Ext.define('Ext.aria.view.View', { override: 'Ext.view.View', initComponent: function() { var me = this, selModel; me.callParent(); selModel = me.getSelectionModel(); selModel.on({ scope: me, select: me.ariaSelect, deselect: me.ariaDeselect }); me.on({ scope: me, refresh: me.ariaInitViewItems, itemadd: me.ariaItemAdd, itemremove: me.ariaItemRemove }); }, ariaGetRenderAttributes: function() { var me = this, attrs, mode; attrs = me.callParent(); mode = me.getSelectionModel().getSelectionMode(); if (mode !== 'SINGLE') { attrs['aria-multiselectable'] = true; } if (me.title) { attrs['aria-label'] = me.title; } return attrs; }, // For Views, we have to apply ARIA attributes to the list items // post factum, because we have no control over the template // that is used to create the items. ariaInitViewItems: function() { var me = this, updateSize = me.pageSize || me.store.buffered, pos = me.store.requestStart + 1, nodes, node, size, i, len; nodes = me.getNodes(); size = me.store.getTotalCount(); for (i = 0 , len = nodes.length; i < len; i++) { node = nodes[i]; if (!node.id) { node.setAttribute('id', Ext.id()); } node.setAttribute('role', me.itemAriaRole); node.setAttribute('aria-selected', false); if (updateSize) { node.setAttribute('aria-setsize', size); node.setAttribute('aria-posinset', pos + i); } } }, ariaSelect: function(selModel, record) { var me = this, node; node = me.getNode(record); if (node) { node.setAttribute('aria-selected', true); me.ariaUpdate({ 'aria-activedescendant': node.id }); } }, ariaDeselect: function(selModel, record) { var me = this, node; node = me.getNode(record); if (node) { node.removeAttribute('aria-selected'); me.ariaUpdate({ 'aria-activedescendant': undefined }); } }, ariaItemRemove: function(records, index, nodes) { if (!nodes) { return; } var me = this, ariaSelected, i, len; ariaSelected = me.el.getAttribute('aria-activedescendant'); for (i = 0 , len = nodes.length; i < len; i++) { if (ariaSelected === nodes[i].id) { me.ariaUpdate({ 'aria-activedescendant': undefined }); break; } } }, ariaItemAdd: function(records, index, nodes) { this.ariaInitViewItems(records, index, nodes); }, setTitle: function(title) { var me = this; me.title = title; me.ariaUpdate({ 'aria-label': title }); } }); /** */ Ext.define('Ext.aria.view.Table', { override: 'Ext.view.Table', requires: [ 'Ext.aria.view.View' ], ariaGetRenderAttributes: function() { var me = this, plugins = me.plugins, readOnly = true, attrs, i, len; attrs = me.callParent(); if (plugins) { for (i = 0 , len = plugins.length; i < len; i++) { // Both CellEditor and RowEditor have 'editing' property if ('editing' in plugins[i]) { readOnly = false; break; } } } attrs['aria-readonly'] = readOnly; return attrs; }, // Table Views are rendered from templates that are rarely overridden, // so we can render ARIA attributes in the templates instead of applying // them after the fact. ariaItemAdd: Ext.emptyFn, ariaItemRemove: Ext.emptyFn, ariaInitViewItems: Ext.emptyFn, ariaFindNode: function(selModel, record, row, column) { var me = this, node; if (selModel.isCellModel) { // When column is hidden, its index will be -1 if (column > -1) { node = me.getCellByPosition({ row: row, column: column }); } else { node = me.getCellByPosition({ row: row, column: 0 }); } } else { node = Ext.fly(me.getNode(record)); } return node; }, ariaSelect: function(selModel, record, row, column) { var me = this, node; node = me.ariaFindNode(selModel, record, row, column); if (node) { node.set({ 'aria-selected': true }); me.ariaUpdate({ 'aria-activedescendant': node.id }); } }, ariaDeselect: function(selModel, record, row, column) { var me = this, node; node = me.ariaFindNode(selModel, record, row, column); if (node) { node.set({ 'aria-selected': undefined }); me.ariaUpdate({ 'aria-activedescendant': undefined }); } }, renderRow: function(record, rowIdx, out) { var me = this, rowValues = me.rowValues; rowValues.ariaRowAttr = 'role="row"'; return me.callParent(arguments); }, renderCell: function(column, record, recordIndex, rowIndex, columnIndex, out) { var me = this, cellValues = me.cellValues; cellValues.ariaCellAttr = 'role="gridcell"'; cellValues.ariaCellInnerAttr = ''; return me.callParent(arguments); }, collectData: function(records, startIndex) { var me = this, data; data = me.callParent(arguments); Ext.applyIf(data, { ariaTableAttr: 'role="presentation"', ariaTbodyAttr: 'role="rowgroup"' }); return data; } }); /** */ Ext.define('Ext.aria.form.field.Checkbox', { override: 'Ext.form.field.Checkbox', requires: [ 'Ext.aria.form.field.Base' ], /** * @cfg {Boolean} [required=false] Set to `true` to make screen readers announce this * checkbox as required. Note that no field validation is performed, and this option * only affects ARIA attributes set for this field. */ isFieldLabelable: false, hideLabel: true, ariaGetEl: function() { return this.inputEl; }, ariaGetRenderAttributes: function() { var me = this, attrs; attrs = me.callParent(arguments); attrs['aria-checked'] = me.getValue(); if (me.required) { attrs['aria-required'] = true; } return attrs; }, ariaGetAfterRenderAttributes: function() { var me = this, boxLabelEl = me.boxLabelEl, attrs; attrs = me.callParent(); if (me.boxLabel && !me.fieldLabel && boxLabelEl) { attrs['aria-labelledby'] = boxLabelEl.id; } return attrs; }, onChange: function() { var me = this; me.callParent(arguments); me.ariaUpdate({ 'aria-checked': me.getValue() }); } }); /** */ Ext.define('Ext.aria.grid.header.Container', { override: 'Ext.grid.header.Container', ariaGetAfterRenderAttributes: function() { var me = this, attrs; attrs = me.callParent(); delete attrs['aria-label']; return attrs; } }); /** */ Ext.define('Ext.aria.grid.column.Column', { override: 'Ext.grid.column.Column', ariaSortStates: { ASC: 'ascending', DESC: 'descending' }, ariaGetAfterRenderAttributes: function() { var me = this, sortState = me.sortState, states = me.ariaSortStates, attr; attr = me.callParent(); attr['aria-sort'] = states[sortState]; return attr; }, setSortState: function(sorter) { var me = this, states = me.ariaSortStates, oldSortState = me.sortState, newSortState; me.callParent(arguments); newSortState = me.sortState; if (oldSortState !== newSortState) { me.ariaUpdate({ 'aria-sort': states[newSortState] }); } } }); /** */ Ext.define('Ext.aria.grid.NavigationModel', { override: 'Ext.grid.NavigationModel', // WAI-ARIA recommends no wrapping around row ends in navigation mode preventWrap: true }); /** */ Ext.define('Ext.aria.form.field.Text', { override: 'Ext.form.field.Text', requires: [ 'Ext.aria.form.field.Base' ], ariaGetRenderAttributes: function() { var me = this, attrs; attrs = me.callParent(); if (me.allowBlank !== undefined) { attrs['aria-required'] = !me.allowBlank; } return attrs; } }); /** */ Ext.define('Ext.aria.button.Button', { override: 'Ext.button.Button', requires: [ 'Ext.aria.Component' ], showEmptyMenu: true, constructor: function(config) { // Don't warn if we're under the slicer if (config.menu && !Ext.theme) { this.ariaCheckMenuConfig(config); } this.callParent(arguments); }, ariaGetRenderAttributes: function() { var me = this, menu = me.menu, attrs; attrs = me.callParent(arguments); if (menu) { attrs['aria-haspopup'] = true; attrs['aria-owns'] = menu.id; } if (me.enableToggle) { attrs['aria-pressed'] = me.pressed; } return attrs; }, toggle: function(state) { var me = this; me.callParent(arguments); me.ariaUpdate({ "aria-pressed": me.pressed }); }, ariaGetLabelEl: function() { return this.btnInnerEl; }, // ARIA requires that buttons with a menu react to // Space and Enter keys by showing the menu. This // behavior conflicts with the various handler // functions we support in Ext JS; to avoid problems // we check if we have the menu *and* handlers, or // `click` event listeners, and raise an error if we do ariaCheckMenuConfig: function(cfg) { var text = cfg.text || cfg.html || 'Unknown'; if (cfg.enableToggle || cfg.toggleGroup) { Ext.log.error("According to WAI-ARIA 1.0 Authoring guide " + "(http://www.w3.org/TR/wai-aria-practices/#menubutton), " + "menu button '" + text + "'s behavior will conflict with " + "toggling"); } if (cfg.href) { Ext.log.error("According to WAI-ARIA 1.0 Authoring guide " + "(http://www.w3.org/TR/wai-aria-practices/#menubutton), " + "menu button '" + text + "' cannot behave as a link"); } if (cfg.handler || (cfg.listeners && cfg.listeners.click)) { Ext.log.error("According to WAI-ARIA 1.0 Authoring guide " + "(http://www.w3.org/TR/wai-aria-practices/#menubutton), " + "menu button '" + text + "' should display the menu " + "on SPACE or ENTER keys, which will conflict with the " + "button handler"); } } }); /** */ Ext.define('Ext.aria.tab.Tab', { override: 'Ext.tab.Tab', // closeText: 'closable', // ariaGetAfterRenderAttributes: function() { var me = this, attrs; attrs = me.callParent(arguments); attrs['aria-selected'] = !!me.active; if (me.card && me.card.getEl()) { attrs['aria-controls'] = me.card.getEl().id; } return attrs; }, activate: function(suppressEvent) { this.callParent([ suppressEvent ]); this.ariaUpdate({ 'aria-selected': true }); }, deactivate: function(suppressEvent) { this.callParent([ suppressEvent ]); this.ariaUpdate({ 'aria-selected': false }); } }); /** */ Ext.define('Ext.aria.tab.Bar', { override: 'Ext.tab.Bar', requires: [ 'Ext.aria.tab.Tab' ], findNextActivatable: function(toClose) { var me = this, next; next = me.callParent(arguments); // If the default algorithm can't find the next tab to activate, // fall back to the currently active tab. We need to have a focused // tab at all times. if (!next) { next = me.activeTab; } return next; } }); /** */ Ext.define('Ext.aria.tab.Panel', { override: 'Ext.tab.Panel', requires: [ 'Ext.layout.container.Card', 'Ext.aria.tab.Bar' ], isTabPanel: true, onAdd: function(item, index) { item.ariaRole = 'tabpanel'; this.callParent(arguments); }, setActiveTab: function(card) { var me = this, items, item, isActive, i, len; me.callParent(arguments); items = me.getRefItems(); for (i = 0 , len = items.length; i < len; i++) { item = items[i]; if (item.ariaRole === 'tabpanel') { isActive = item === card; item.ariaUpdate({ 'aria-expanded': isActive, 'aria-hidden': !isActive }); } } }, ariaIsOwnTab: function(cmp) { return cmp.isTab && cmp.isGroupedBy.ownerCt === this; } }); /** */ Ext.define('Ext.aria.window.Window', { override: 'Ext.window.Window', requires: [ 'Ext.aria.panel.Panel', 'Ext.util.ComponentDragger', 'Ext.util.Region', 'Ext.EventManager', 'Ext.aria.FocusManager' ], closeText: 'Close Window', moveText: 'Move Window', resizeText: 'Resize Window', deltaMove: 10, deltaResize: 10, initComponent: function() { var me = this, tools = me.tools; // Add buttons to move and resize the window, // unless it's a Toast if (!tools) { me.tools = tools = []; } //TODO: Create new tools if (!me.isToast) { tools.unshift({ type: 'resize', tooltip: me.resizeText }, { type: 'move', tooltip: me.moveText }); } me.callParent(); }, onBoxReady: function() { var me = this, EO = Ext.event.Event, toolBtn; me.callParent(); if (me.isToast) { return; } if (me.draggable) { toolBtn = me.down('tool[type=move]'); if (toolBtn) { me.ariaUpdate(toolBtn.getEl(), { 'aria-label': me.moveText }); toolBtn.keyMap = new Ext.util.KeyMap({ target: toolBtn.el, key: [ EO.UP, EO.DOWN, EO.LEFT, EO.RIGHT ], handler: me.moveWindow, scope: me }); } } if (me.resizable) { toolBtn = me.down('tool[type=resize]'); if (toolBtn) { me.ariaUpdate(toolBtn.getEl(), { 'aria-label': me.resizeText }); toolBtn.keyMap = new Ext.util.KeyMap({ target: toolBtn.el, key: [ EO.UP, EO.DOWN, EO.LEFT, EO.RIGHT ], handler: me.resizeWindow, scope: me }); } } }, onEsc: function(k, e) { var me = this; if (e.within(me.el)) { e.stopEvent(); me.close(); } }, onShow: function() { var me = this; me.callParent(arguments); Ext.aria.FocusManager.addWindow(me); }, afterHide: function() { var me = this; Ext.aria.FocusManager.removeWindow(me); me.callParent(arguments); }, moveWindow: function(keyCode, e) { var me = this, delta = me.deltaMove, pos = me.getPosition(), EO = Ext.event.Event; switch (keyCode) { case EO.RIGHT: pos[0] += delta; break; case EO.LEFT: pos[0] -= delta; break; case EO.UP: pos[1] -= delta; break; case EO.DOWN: pos[1] += delta; break; } me.setPagePosition(pos); e.stopEvent(); }, resizeWindow: function(keyCode, e) { var me = this, delta = me.deltaResize, width = me.getWidth(), height = me.getHeight(), EO = Ext.event.Event; switch (keyCode) { case EO.RIGHT: width += delta; break; case EO.LEFT: width -= delta; break; case EO.UP: height -= delta; break; case EO.DOWN: height += delta; break; } me.setSize(width, height); e.stopEvent(); } }); /** */ Ext.define('Ext.aria.tip.QuickTip', { override: 'Ext.tip.QuickTip', showByTarget: function(targetEl) { var me = this, target, size, xy, x, y; target = me.targets[targetEl.id]; if (!target) { return; } me.activeTarget = target; me.activeTarget.el = Ext.get(targetEl).dom; me.anchor = me.activeTarget.anchor; size = targetEl.getSize(); xy = targetEl.getXY(); me.showAt([ xy[0], xy[1] + size.height ]); } }); /** */ Ext.define('Ext.aria.button.Split', { override: 'Ext.button.Split', constructor: function(config) { var ownerCt = config.ownerCt; // Warn unless the button belongs to a date picker, // the user can't do anything about that // Also don't warn if we're under the slicer if (!Ext.theme && (!ownerCt || !ownerCt.isDatePicker)) { Ext.log.warn("Using Split buttons is not recommended in WAI-ARIA " + "compliant applications, because their behavior conflicts " + "with accessibility best practices. See WAI-ARIA 1.0 " + "Authoring guide: http://www.w3.org/TR/wai-aria-practices/#menubutton"); } this.callParent(arguments); } }); /** */ Ext.define('Ext.aria.button.Cycle', { override: 'Ext.button.Cycle', constructor: function(config) { // Don't warn if we're under the slicer if (!Ext.theme) { Ext.log.warn("Using Cycle buttons is not recommended in WAI-ARIA " + "compliant applications, because their behavior conflicts " + "with accessibility best practices. See WAI-ARIA 1.0 " + "Authoring guide: http://www.w3.org/TR/wai-aria-practices/#menubutton"); } this.callParent(arguments); } }); /** */ Ext.define('Ext.aria.container.Viewport', { override: 'Ext.container.Viewport', initComponent: function() { var me = this, items = me.items, layout = me.layout, i, len, item, el; if (items && layout === 'border' || (Ext.isObject(layout) && layout.type === 'border')) { for (i = 0 , len = items.length; i < len; i++) { item = items[i]; if (item.region) { Ext.applyIf(item, { ariaRole: 'region', headerRole: 'heading' }); } } } me.callParent(); }, ariaGetAfterRenderAttributes: function() { var attrs = this.callParent(); // Viewport's role attribute is applied to the element that is never rendered, // so we have to do it post factum attrs.role = this.ariaRole; // Viewport should not have a label, document title should be announced instead delete attrs['aria-label']; delete attrs['aria-labelledby']; return attrs; } }); /** */ Ext.define('Ext.aria.form.field.TextArea', { override: 'Ext.form.field.TextArea', requires: [ 'Ext.aria.form.field.Text' ], ariaGetRenderAttributes: function() { var me = this, attrs; attrs = me.callParent(); attrs['aria-multiline'] = true; return attrs; } }); /** */ Ext.define('Ext.aria.window.MessageBox', { override: 'Ext.window.MessageBox', requires: [ 'Ext.aria.window.Window', 'Ext.aria.form.field.Text', 'Ext.aria.form.field.TextArea', 'Ext.aria.form.field.Display', 'Ext.aria.button.Button' ] }); /** */ Ext.define('Ext.aria.form.FieldContainer', { override: 'Ext.form.FieldContainer', ariaGetAfterRenderAttributes: function() { var me = this, attrs; attrs = me.callParent(arguments); if (me.fieldLabel && me.labelEl) { attrs['aria-labelledby'] = me.labelEl.id; } return attrs; } }); /** */ Ext.define('Ext.aria.form.CheckboxGroup', { override: 'Ext.form.CheckboxGroup', requires: [ 'Ext.aria.form.FieldContainer', 'Ext.aria.form.field.Base' ], msgTarget: 'side', setReadOnly: function(readOnly) { var me = this; me.callParent(arguments); me.ariaUpdate({ 'aria-readonly': !!readOnly }); }, markInvalid: function(f, isValid) { var me = this; me.callParent(arguments); me.ariaUpdate({ 'aria-invalid': !!isValid }); }, clearInvalid: function() { var me = this; me.callParent(arguments); me.ariaUpdate({ 'aria-invalid': false }); } }); /** */ Ext.define('Ext.aria.form.FieldSet', { override: 'Ext.form.FieldSet', expandText: 'Expand', collapseText: 'Collapse', onBoxReady: function() { var me = this, checkboxCmp = me.checkboxCmp, toggleCmp = me.toggleCmp, legend = me.legend, el; me.callParent(arguments); if (!legend) { return; } // mark the legend and the checkbox or drop down inside the legend immune to collapse // so when they get focus, isVisible(deep) will not return true for them when the fieldset is collapsed legend.collapseImmune = true; legend.getInherited().collapseImmune = true; if (checkboxCmp) { checkboxCmp.collapseImmune = true; checkboxCmp.getInherited().collapseImmune = true; checkboxCmp.getActionEl().set({ title: me.expandText + ' ' + me.title }); } if (toggleCmp) { toggleCmp.collapseImmune = true; toggleCmp.getInherited().collapseImmune = true; // The toggle component is missing a key map to respond to enter and space toggleCmp.keyMap = new Ext.util.KeyMap({ target: toggleCmp.el, key: [ Ext.event.Event.ENTER, Ext.event.Event.SPACE ], handler: function(key, e, eOpt) { e.stopEvent(); me.toggle(); }, scope: me }); el = toggleCmp.getActionEl(); if (me.collapsed) { el.set({ title: me.expandText + ' ' + me.title }); } else { el.set({ title: me.collapseText + ' ' + me.title }); } } }, ariaGetRenderAttributes: function() { var me = this, attrs; attrs = me.callParent(arguments); attrs['aria-expanded'] = !me.collapsed; return attrs; }, setExpanded: function(expanded) { var me = this, toggleCmp = me.toggleCmp, el; me.callParent(arguments); me.ariaUpdate({ 'aria-expanded': expanded }); // Update the title if (toggleCmp) { el = toggleCmp.getActionEl(); if (!expanded) { el.set({ title: me.expandText + ' ' + me.title }); } else { el.set({ title: me.collapseText + ' ' + me.title }); } } } }); /** */ Ext.define('Ext.aria.form.RadioGroup', { override: 'Ext.form.RadioGroup', requires: [ 'Ext.aria.form.CheckboxGroup' ], ariaGetRenderAttributes: function() { var me = this, attrs; attrs = me.callParent(); if (me.allowBlank !== undefined) { attrs['aria-required'] = !me.allowBlank; } return attrs; }, ariaGetAfterRenderAttributes: function() { var me = this, attrs; attrs = me.callParent(); if (me.labelEl) { attrs['aria-labelledby'] = me.labelEl.id; } return attrs; } }); /** */ Ext.define('Ext.aria.form.field.Picker', { override: 'Ext.form.field.Picker', ariaGetRenderAttributes: function() { var me = this, attrs; attrs = me.callParent(); attrs['aria-haspopup'] = true; return attrs; }, ariaGetAfterRenderAttributes: function() { var me = this, attrs, picker; attrs = me.callParent(); picker = me.getPicker(); if (picker) { attrs['aria-owns'] = picker.id; } return attrs; } }); /** */ Ext.define('Ext.aria.view.BoundListKeyNav', { override: 'Ext.view.BoundListKeyNav', requires: [ 'Ext.aria.view.View' ], focusItem: function(item) { var me = this, boundList = me.view; if (typeof item === 'number') { item = boundList.all.item(item); } if (item) { boundList.ariaUpdate({ 'aria-activedescendant': Ext.id(item, me.id + '-') }); me.callParent([ item ]); } } }); /** */ Ext.define('Ext.aria.form.field.Number', { override: 'Ext.form.field.Number', ariaGetRenderAttributes: function() { var me = this, min = me.minValue, max = me.maxValue, attrs, v; attrs = me.callParent(arguments); v = me.getValue(); // Skip the defaults if (min !== Number.NEGATIVE_INFINITY) { attrs['aria-valuemin'] = isFinite(min) ? min : 'NaN'; } if (max !== Number.MAX_VALUE) { attrs['aria-valuemax'] = isFinite(max) ? max : 'NaN'; } attrs['aria-valuenow'] = v !== null && isFinite(v) ? v : 'NaN'; return attrs; }, onChange: function(f) { var me = this, v; me.callParent(arguments); v = me.getValue(); me.ariaUpdate({ 'aria-valuenow': v !== null && isFinite(v) ? v : 'NaN' }); }, setMinValue: function() { var me = this; me.callParent(arguments); me.ariaUpdate({ 'aria-valuemin': isFinite(me.minValue) ? me.minValue : 'NaN' }); }, setMaxValue: function() { var me = this; me.callParent(arguments); me.ariaUpdate({ 'aria-valuemax': isFinite(me.maxValue) ? me.maxValue : 'NaN' }); } }); /** */ Ext.define('Ext.aria.view.BoundList', { override: 'Ext.view.BoundList', onHide: function() { this.ariaUpdate({ "aria-activedescendant": Ext.emptyString }); // Maintainer: onHide takes arguments this.callParent(arguments); } }); /** */ Ext.define('Ext.aria.form.field.ComboBox', { override: 'Ext.form.field.ComboBox', requires: [ 'Ext.aria.form.field.Picker' ], createPicker: function() { var me = this, picker; picker = me.callParent(arguments); if (picker) { // update aria-activedescendant whenever the picker highlight changes me.mon(picker, { highlightitem: me.ariaUpdateActiveDescendant, scope: me }); } return picker; }, ariaGetRenderAttributes: function() { var me = this, attrs; attrs = me.callParent(); attrs['aria-readonly'] = !!(!me.editable || me.readOnly); attrs['aria-expanded'] = !!me.isExpanded; attrs['aria-autocomplete'] = "list"; return attrs; }, setReadOnly: function(readOnly) { var me = this; me.callParent(arguments); me.ariaUpdate({ 'aria-readonly': me.readOnly }); }, setEditable: function(editable) { var me = this; me.callParent(arguments); me.ariaUpdate({ 'aria-readonly': !me.editable }); }, onExpand: function() { var me = this, selected = me.picker.getSelectedNodes(); me.callParent(arguments); me.ariaUpdate({ 'aria-expanded': true, 'aria-activedescendant': (selected.length ? selected[0].id : undefined) }); }, onCollapse: function() { var me = this; me.callParent(arguments); me.ariaUpdate({ 'aria-expanded': false, 'aria-activedescendant': undefined }); }, ariaUpdateActiveDescendant: function(list) { this.ariaUpdate({ 'aria-activedescendant': list.highlightedItem ? list.highlightedItem.id : undefined }); } }); /** */ Ext.define('Ext.aria.form.field.Date', { override: 'Ext.form.field.Date', requires: [ 'Ext.aria.form.field.Picker' ], formatText: 'Expected date format {0}', /** * @private * Override because we do not want to focus the field if the collapse * was because of a tab key. Tab should move the focus to the next field. * Before collapsing the field will set doCancelFieldFocus based on the pressed key */ onCollapse: function() { var me = this; if (!me.doCancelFieldFocus) { me.focus(false, 60); } } }); /** */ Ext.define('Ext.aria.picker.Color', { override: 'Ext.picker.Color', requires: [ 'Ext.aria.Component' ], initComponent: function() { var me = this; me.callParent(arguments); }, //\\ TODO: set up KeyNav ariaGetEl: function() { return this.innerEl; }, onColorSelect: function(picker, cell) { var me = this; if (cell && cell.dom) { me.ariaUpdate(me.eventEl, { 'aria-activedescendant': cell.dom.id }); } }, privates: { getFocusEl: function() { return this.el; } } }); /** */ Ext.define('Ext.aria.form.field.Time', { override: 'Ext.form.field.Time', requires: [ 'Ext.aria.form.field.ComboBox' ], // The default format for the time field is 'g:i A', // which is hardly informative formatText: 'Expected time format HH:MM AM or PM' }); /** */ Ext.define('Ext.aria.menu.Item', { override: 'Ext.menu.Item', ariaGetRenderAttributes: function() { var me = this, attrs; attrs = me.callParent(); if (me.menu) { attrs['aria-haspopup'] = true; } return attrs; }, ariaGetAfterRenderAttributes: function() { var me = this, menu = me.menu, attrs; attrs = me.callParent(); if (menu && menu.rendered) { attrs['aria-controls'] = menu.ariaGetEl().id; } if (me.plain) { attrs['aria-label'] = me.text; } else { attrs['aria-labelledby'] = me.textEl.id; } return attrs; }, doExpandMenu: function() { var me = this, menu = me.menu; me.callParent(); if (menu && menu.rendered) { me.ariaUpdate({ 'aria-controls': menu.ariaGetEl().id }); } } }); /** */ Ext.define('Ext.aria.menu.CheckItem', { override: 'Ext.menu.CheckItem', ariaGetRenderAttributes: function() { var me = this, attrs; attrs = me.callParent(); attrs['aria-checked'] = me.menu ? 'mixed' : !!me.checked; return attrs; }, setChecked: function(checked, suppressEvents) { this.callParent([ checked, suppressEvents ]); this.ariaUpdate({ 'aria-checked': checked }); } }); /** */ Ext.define('Ext.aria.slider.Thumb', { override: 'Ext.slider.Thumb', move: function(v, animate) { var me = this, el = me.el, slider = me.slider, styleProp = slider.vertical ? 'bottom' : slider.horizontalProp, to, from; v += '%'; if (!animate) { el.dom.style[styleProp] = v; slider.fireEvent('move', slider, v, me); } else { to = {}; to[styleProp] = v; if (!Ext.supports.GetPositionPercentage) { from = {}; from[styleProp] = el.dom.style[styleProp]; } new Ext.fx.Anim({ target: el, duration: 350, from: from, to: to, callback: function() { slider.fireEvent('move', slider, v, me); } }); } } }); /** */ Ext.define('Ext.aria.slider.Tip', { override: 'Ext.slider.Tip', init: function(slider) { var me = this, timeout = slider.tipHideTimeout; me.onSlide = Ext.Function.createThrottled(me.onSlide, 50, me); me.hide = Ext.Function.createBuffered(me.hide, timeout, me); me.callParent(arguments); slider.on({ scope: me, change: me.onSlide, move: me.onSlide, changecomplete: me.hide }); } }); // There is no clear way to support multi-thumb sliders // in accessible applications, so we default to support // only single-slider ones /** */ Ext.define('Ext.aria.slider.Multi', { override: 'Ext.slider.Multi', /** * @cfg {Number} [tipHideTimeout=1000] Timeout in ms after which * the slider tip will be hidden. */ tipHideTimeout: 1000, animate: false, tabIndex: 0, ariaGetRenderAttributes: function() { var me = this, attrs; attrs = me.callParent(); attrs['aria-minvalue'] = me.minValue; attrs['aria-maxvalue'] = me.maxValue; attrs['aria-valuenow'] = me.getValue(0); return attrs; }, getSubTplData: function() { var me = this, fmt = Ext.util.Format.attributes, data, attrs; data = me.callParent(arguments); attrs = me.ariaGetRenderAttributes(); // Role is rendered separately delete attrs.role; data.inputAttrTpl = fmt(attrs); return data; }, onKeyDown: function(e) { var me = this, key, value; if (me.disabled || me.thumbs.length !== 1) { e.preventDefault(); return; } key = e.getKey(); switch (key) { case e.HOME: e.stopEvent(); me.setValue(0, me.minValue, undefined, true); return; case e.END: e.stopEvent(); me.setValue(0, me.maxValue, undefined, true); return; case e.PAGE_UP: e.stopEvent(); value = me.getValue(0) - me.keyIncrement * 10; me.setValue(0, value, undefined, true); return; case e.PAGE_DOWN: e.stopEvent(); value = me.getValue(0) + me.keyIncrement * 10; me.setValue(0, value, undefined, true); return; } me.callParent(arguments); }, setMinValue: function(value) { var me = this; me.callParent(arguments); me.ariaUpdate({ 'aria-minvalue': value }); }, setMaxValue: function(value) { var me = this; me.callParent(arguments); me.ariaUpdate({ 'aria-maxvalue': value }); }, setValue: function(index, value) { var me = this; me.callParent(arguments); if (index === 0) { me.ariaUpdate({ 'aria-valuenow': value }); } } }); /** */ Ext.define('Ext.aria.window.Toast', { override: 'Ext.window.Toast', initComponent: function() { // Close tool is not really helpful to blind users // when Toast window is set to auto-close on timeout if (this.autoClose) { this.closable = false; } this.callParent(); } }); /** * Base class from Ext.ux.TabReorderer. */ Ext.define('Ext.ux.BoxReorderer', { requires: [ 'Ext.dd.DD' ], mixins: { observable: 'Ext.util.Observable' }, /** * @cfg {String} itemSelector * A {@link Ext.DomQuery DomQuery} selector which identifies the encapsulating elements of child * Components which participate in reordering. */ itemSelector: '.x-box-item', /** * @cfg {Mixed} animate * If truthy, child reordering is animated so that moved boxes slide smoothly into position. * If this option is numeric, it is used as the animation duration in milliseconds. */ animate: 100, /** * @event StartDrag * Fires when dragging of a child Component begins. * @param {Ext.ux.BoxReorderer} this * @param {Ext.container.Container} container The owning Container * @param {Ext.Component} dragCmp The Component being dragged * @param {Number} idx The start index of the Component being dragged. */ /** * @event Drag * Fires during dragging of a child Component. * @param {Ext.ux.BoxReorderer} this * @param {Ext.container.Container} container The owning Container * @param {Ext.Component} dragCmp The Component being dragged * @param {Number} startIdx The index position from which the Component was initially dragged. * @param {Number} idx The current closest index to which the Component would drop. */ /** * @event ChangeIndex * Fires when dragging of a child Component causes its drop index to change. * @param {Ext.ux.BoxReorderer} this * @param {Ext.container.Container} container The owning Container * @param {Ext.Component} dragCmp The Component being dragged * @param {Number} startIdx The index position from which the Component was initially dragged. * @param {Number} idx The current closest index to which the Component would drop. */ /** * @event Drop * Fires when a child Component is dropped at a new index position. * @param {Ext.ux.BoxReorderer} this * @param {Ext.container.Container} container The owning Container * @param {Ext.Component} dragCmp The Component being dropped * @param {Number} startIdx The index position from which the Component was initially dragged. * @param {Number} idx The index at which the Component is being dropped. */ constructor: function() { this.mixins.observable.constructor.apply(this, arguments); }, init: function(container) { var me = this; me.container = container; // Set our animatePolicy to animate the start position (ie x for HBox, y for VBox) me.animatePolicy = {}; me.animatePolicy[container.getLayout().names.x] = true; // Initialize the DD on first layout, when the innerCt has been created. me.container.on({ scope: me, boxready: me.onBoxReady, beforedestroy: me.onContainerDestroy }); }, /** * @private Clear up on Container destroy */ onContainerDestroy: function() { var dd = this.dd; if (dd) { dd.unreg(); this.dd = null; } }, onBoxReady: function() { var me = this, layout = me.container.getLayout(), names = layout.names, dd; // Create a DD instance. Poke the handlers in. // TODO: Ext5's DD classes should apply config to themselves. // TODO: Ext5's DD classes should not use init internally because it collides with use as a plugin // TODO: Ext5's DD classes should be Observable. // TODO: When all the above are trus, this plugin should extend the DD class. dd = me.dd = new Ext.dd.DD(layout.innerCt, me.container.id + '-reorderer'); Ext.apply(dd, { animate: me.animate, reorderer: me, container: me.container, getDragCmp: me.getDragCmp, clickValidator: Ext.Function.createInterceptor(dd.clickValidator, me.clickValidator, me, false), onMouseDown: me.onMouseDown, startDrag: me.startDrag, onDrag: me.onDrag, endDrag: me.endDrag, getNewIndex: me.getNewIndex, doSwap: me.doSwap, findReorderable: me.findReorderable }); // Decide which dimension we are measuring, and which measurement metric defines // the *start* of the box depending upon orientation. dd.dim = names.width; dd.startAttr = names.beforeX; dd.endAttr = names.afterX; }, getDragCmp: function(e) { return this.container.getChildByElement(e.getTarget(this.itemSelector, 10)); }, // check if the clicked component is reorderable clickValidator: function(e) { var cmp = this.getDragCmp(e); // If cmp is null, this expression MUST be coerced to boolean so that createInterceptor is able to test it against false return !!(cmp && cmp.reorderable !== false); }, onMouseDown: function(e) { var me = this, container = me.container, containerBox, cmpEl, cmpBox; // Ascertain which child Component is being mousedowned me.dragCmp = me.getDragCmp(e); if (me.dragCmp) { cmpEl = me.dragCmp.getEl(); me.startIndex = me.curIndex = container.items.indexOf(me.dragCmp); // Start position of dragged Component cmpBox = cmpEl.getBox(); // Last tracked start position me.lastPos = cmpBox[me.startAttr]; // Calculate constraints depending upon orientation // Calculate offset from mouse to dragEl position containerBox = container.el.getBox(); if (me.dim === 'width') { me.minX = containerBox.left; me.maxX = containerBox.right - cmpBox.width; me.minY = me.maxY = cmpBox.top; me.deltaX = e.getX() - cmpBox.left; } else { me.minY = containerBox.top; me.maxY = containerBox.bottom - cmpBox.height; me.minX = me.maxX = cmpBox.left; me.deltaY = e.getY() - cmpBox.top; } me.constrainY = me.constrainX = true; } }, startDrag: function() { var me = this, dragCmp = me.dragCmp; if (dragCmp) { // For the entire duration of dragging the *Element*, defeat any positioning and animation of the dragged *Component* dragCmp.setPosition = Ext.emptyFn; dragCmp.animate = false; // Animate the BoxLayout just for the duration of the drag operation. if (me.animate) { me.container.getLayout().animatePolicy = me.reorderer.animatePolicy; } // We drag the Component element me.dragElId = dragCmp.getEl().id; me.reorderer.fireEvent('StartDrag', me, me.container, dragCmp, me.curIndex); // Suspend events, and set the disabled flag so that the mousedown and mouseup events // that are going to take place do not cause any other UI interaction. dragCmp.suspendEvents(); dragCmp.disabled = true; dragCmp.el.setStyle('zIndex', 100); } else { me.dragElId = null; } }, /** * @private * Find next or previous reorderable component index. * @param {Number} newIndex The initial drop index. * @return {Number} The index of the reorderable component. */ findReorderable: function(newIndex) { var me = this, items = me.container.items, newItem; if (items.getAt(newIndex).reorderable === false) { newItem = items.getAt(newIndex); if (newIndex > me.startIndex) { while (newItem && newItem.reorderable === false) { newIndex++; newItem = items.getAt(newIndex); } } else { while (newItem && newItem.reorderable === false) { newIndex--; newItem = items.getAt(newIndex); } } } newIndex = Math.min(Math.max(newIndex, 0), items.getCount() - 1); if (items.getAt(newIndex).reorderable === false) { return -1; } return newIndex; }, /** * @private * Swap 2 components. * @param {Number} newIndex The initial drop index. */ doSwap: function(newIndex) { var me = this, items = me.container.items, container = me.container, wasRoot = me.container._isLayoutRoot, orig, dest, tmpIndex; newIndex = me.findReorderable(newIndex); if (newIndex === -1) { return; } me.reorderer.fireEvent('ChangeIndex', me, container, me.dragCmp, me.startIndex, newIndex); orig = items.getAt(me.curIndex); dest = items.getAt(newIndex); items.remove(orig); tmpIndex = Math.min(Math.max(newIndex, 0), items.getCount() - 1); items.insert(tmpIndex, orig); items.remove(dest); items.insert(me.curIndex, dest); // Make the Box Container the topmost layout participant during the layout. container._isLayoutRoot = true; container.updateLayout(); container._isLayoutRoot = wasRoot; me.curIndex = newIndex; }, onDrag: function(e) { var me = this, newIndex; newIndex = me.getNewIndex(e.getPoint()); if ((newIndex !== undefined)) { me.reorderer.fireEvent('Drag', me, me.container, me.dragCmp, me.startIndex, me.curIndex); me.doSwap(newIndex); } }, endDrag: function(e) { if (e) { e.stopEvent(); } var me = this, layout = me.container.getLayout(), temp; if (me.dragCmp) { delete me.dragElId; // Reinstate the Component's positioning method after mouseup, and allow the layout system to animate it. delete me.dragCmp.setPosition; me.dragCmp.animate = true; // Ensure the lastBox is correct for the animation system to restore to when it creates the "from" animation frame me.dragCmp.lastBox[layout.names.x] = me.dragCmp.getPosition(true)[layout.names.widthIndex]; // Make the Box Container the topmost layout participant during the layout. me.container._isLayoutRoot = true; me.container.updateLayout(); me.container._isLayoutRoot = undefined; // Attempt to hook into the afteranimate event of the drag Component to call the cleanup temp = Ext.fx.Manager.getFxQueue(me.dragCmp.el.id)[0]; if (temp) { temp.on({ afteranimate: me.reorderer.afterBoxReflow, scope: me }); } else // If not animated, clean up after the mouseup has happened so that we don't click the thing being dragged { Ext.Function.defer(me.reorderer.afterBoxReflow, 1, me); } if (me.animate) { delete layout.animatePolicy; } me.reorderer.fireEvent('drop', me, me.container, me.dragCmp, me.startIndex, me.curIndex); } }, /** * @private * Called after the boxes have been reflowed after the drop. * Re-enabled the dragged Component. */ afterBoxReflow: function() { var me = this; me.dragCmp.el.setStyle('zIndex', ''); me.dragCmp.disabled = false; me.dragCmp.resumeEvents(); }, /** * @private * Calculate drop index based upon the dragEl's position. */ getNewIndex: function(pointerPos) { var me = this, dragEl = me.getDragEl(), dragBox = Ext.fly(dragEl).getBox(), targetEl, targetBox, targetMidpoint, i = 0, it = me.container.items.items, ln = it.length, lastPos = me.lastPos; me.lastPos = dragBox[me.startAttr]; for (; i < ln; i++) { targetEl = it[i].getEl(); // Only look for a drop point if this found item is an item according to our selector if (targetEl.is(me.reorderer.itemSelector)) { targetBox = targetEl.getBox(); targetMidpoint = targetBox[me.startAttr] + (targetBox[me.dim] >> 1); if (i < me.curIndex) { if ((dragBox[me.startAttr] < lastPos) && (dragBox[me.startAttr] < (targetMidpoint - 5))) { return i; } } else if (i > me.curIndex) { if ((dragBox[me.startAttr] > lastPos) && (dragBox[me.endAttr] > (targetMidpoint + 5))) { return i; } } } } } }); /** * This plugin can enable a cell to cell drag and drop operation within the same grid view. * * Note that the plugin must be added to the grid view, not to the grid panel. For example, using {@link Ext.panel.Table viewConfig}: * * viewConfig: { * plugins: { * ptype: 'celldragdrop', * * // Remove text from source cell and replace with value of emptyText. * applyEmptyText: true, * * //emptyText: Ext.String.htmlEncode('<>'), * * // Will only allow drops of the same type. * enforceType: true * } * } */ Ext.define('Ext.ux.CellDragDrop', { extend: 'Ext.plugin.Abstract', alias: 'plugin.celldragdrop', uses: [ 'Ext.view.DragZone' ], /** * @cfg {Boolean} enforceType * Set to `true` to only allow drops of the same type. * * Defaults to `false`. */ enforceType: false, /** * @cfg {Boolean} applyEmptyText * If `true`, then use the value of {@link #emptyText} to replace the drag record's value after a node drop. * Note that, if dropped on a cell of a different type, it will convert the default text according to its own conversion rules. * * Defaults to `false`. */ applyEmptyText: false, /** * @cfg {Boolean} emptyText * If {@link #applyEmptyText} is `true`, then this value as the drag record's value after a node drop. * * Defaults to an empty string. */ emptyText: '', /** * @cfg {Boolean} dropBackgroundColor * The default background color for when a drop is allowed. * * Defaults to green. */ dropBackgroundColor: 'green', /** * @cfg {Boolean} noDropBackgroundColor * The default background color for when a drop is not allowed. * * Defaults to red. */ noDropBackgroundColor: 'red', // /** * @cfg {String} dragText * The text to show while dragging. * * Two placeholders can be used in the text: * * - `{0}` The number of selected items. * - `{1}` 's' when more than 1 items (only useful for English). */ dragText: '{0} selected row{1}', // /** * @cfg {String} ddGroup * A named drag drop group to which this object belongs. If a group is specified, then both the DragZones and * DropZone used by this plugin will only interact with other drag drop objects in the same group. */ ddGroup: "GridDD", /** * @cfg {Boolean} enableDrop * Set to `false` to disallow the View from accepting drop gestures. */ enableDrop: true, /** * @cfg {Boolean} enableDrag * Set to `false` to disallow dragging items from the View. */ enableDrag: true, /** * @cfg {Object/Boolean} containerScroll * True to register this container with the Scrollmanager for auto scrolling during drag operations. * A {@link Ext.dd.ScrollManager} configuration may also be passed. */ containerScroll: false, init: function(view) { var me = this; view.on('render', me.onViewRender, me, { single: true }); }, destroy: function() { var me = this; Ext.destroy(me.dragZone, me.dropZone); }, enable: function() { var me = this; if (me.dragZone) { me.dragZone.unlock(); } if (me.dropZone) { me.dropZone.unlock(); } me.callParent(); }, disable: function() { var me = this; if (me.dragZone) { me.dragZone.lock(); } if (me.dropZone) { me.dropZone.lock(); } me.callParent(); }, onViewRender: function(view) { var me = this, scrollEl; if (me.enableDrag) { if (me.containerScroll) { scrollEl = view.getEl(); } me.dragZone = new Ext.view.DragZone({ view: view, ddGroup: me.dragGroup || me.ddGroup, dragText: me.dragText, containerScroll: me.containerScroll, scrollEl: scrollEl, getDragData: function(e) { var view = this.view, item = e.getTarget(view.getItemSelector()), record = view.getRecord(item), cell = e.getTarget(view.getCellSelector()), dragEl, header; if (item) { dragEl = document.createElement('div'); dragEl.className = 'x-form-text'; dragEl.appendChild(document.createTextNode(cell.textContent || cell.innerText)); header = view.getHeaderByCell(cell); return { event: new Ext.EventObjectImpl(e), ddel: dragEl, item: e.target, columnName: header.dataIndex, record: record }; } }, onInitDrag: function(x, y) { var self = this, data = self.dragData, view = self.view, selectionModel = view.getSelectionModel(), record = data.record, el = data.ddel; // Update the selection to match what would have been selected if the user had // done a full click on the target node rather than starting a drag from it. if (!selectionModel.isSelected(record)) { selectionModel.select(record, true); } Ext.fly(self.ddel).update(el.textContent || el.innerText); self.proxy.update(self.ddel); self.onStartDrag(x, y); return true; } }); } if (me.enableDrop) { me.dropZone = new Ext.dd.DropZone(view.el, { view: view, ddGroup: me.dropGroup || me.ddGroup, containerScroll: true, getTargetFromEvent: function(e) { var self = this, view = self.view, cell = e.getTarget(view.cellSelector), row, header; // Ascertain whether the mousemove is within a grid cell. if (cell) { row = view.findItemByChild(cell); header = view.getHeaderByCell(cell); if (row && header) { return { node: cell, record: view.getRecord(row), columnName: header.dataIndex }; } } }, // On Node enter, see if it is valid for us to drop the field on that type of column. onNodeEnter: function(target, dd, e, dragData) { var self = this, destType = target.record.getField(target.columnName).type.toUpperCase(), sourceType = dragData.record.getField(dragData.columnName).type.toUpperCase(); delete self.dropOK; // Return if no target node or if over the same cell as the source of the drag. if (!target || target.node === dragData.item.parentNode) { return; } // Check whether the data type of the column being dropped on accepts the // dragged field type. If so, set dropOK flag, and highlight the target node. if (me.enforceType && destType !== sourceType) { self.dropOK = false; if (me.noDropCls) { Ext.fly(target.node).addCls(me.noDropCls); } else { Ext.fly(target.node).applyStyles({ backgroundColor: me.noDropBackgroundColor }); } return false; } self.dropOK = true; if (me.dropCls) { Ext.fly(target.node).addCls(me.dropCls); } else { Ext.fly(target.node).applyStyles({ backgroundColor: me.dropBackgroundColor }); } }, // Return the class name to add to the drag proxy. This provides a visual indication // of drop allowed or not allowed. onNodeOver: function(target, dd, e, dragData) { return this.dropOK ? this.dropAllowed : this.dropNotAllowed; }, // Highlight the target node. onNodeOut: function(target, dd, e, dragData) { var cls = this.dropOK ? me.dropCls : me.noDropCls; if (cls) { Ext.fly(target.node).removeCls(cls); } else { Ext.fly(target.node).applyStyles({ backgroundColor: '' }); } }, // Process the drop event if we have previously ascertained that a drop is OK. onNodeDrop: function(target, dd, e, dragData) { if (this.dropOK) { target.record.set(target.columnName, dragData.record.get(dragData.columnName)); if (me.applyEmptyText) { dragData.record.set(dragData.columnName, me.emptyText); } return true; } }, onCellDrop: Ext.emptyFn }); } } }); /** * @class Ext.ux.DataTip * @extends Ext.ToolTip. * This plugin implements automatic tooltip generation for an arbitrary number of child nodes *within* a Component. * * This plugin is applied to a high level Component, which contains repeating elements, and depending on the host Component type, * it automatically selects a {@link Ext.ToolTip#delegate delegate} so that it appears when the mouse enters a sub-element. * * When applied to a GridPanel, this ToolTip appears when over a row, and the Record's data is applied * using this object's {@link #tpl} template. * * When applied to a DataView, this ToolTip appears when over a view node, and the Record's data is applied * using this object's {@link #tpl} template. * * When applied to a TreePanel, this ToolTip appears when over a tree node, and the Node's {@link Ext.data.Model} record data is applied * using this object's {@link #tpl} template. * * When applied to a FormPanel, this ToolTip appears when over a Field, and the Field's `tooltip` property is used is applied * using this object's {@link #tpl} template, or if it is a string, used as HTML content. If there is no `tooltip` property, * the field itself is used as the template's data object. * * If more complex logic is needed to determine content, then the {@link #beforeshow} event may be used. * This class also publishes a **`beforeshowtip`** event through its host Component. The *host Component* fires the * **`beforeshowtip`** event. */ Ext.define('Ext.ux.DataTip', function(DataTip) { // Target the body (if the host is a Panel), or, if there is no body, the main Element. function onHostRender() { var e = this.isXType('panel') ? this.body : this.el; if (this.dataTip.renderToTarget) { this.dataTip.render(e); } this.dataTip.setTarget(e); } function updateTip(tip, data) { if (tip.rendered) { if (tip.host.fireEvent('beforeshowtip', tip.eventHost, tip, data) === false) { return false; } tip.update(data); } else { if (Ext.isString(data)) { tip.html = data; } else { tip.data = data; } } } function beforeViewTipShow(tip) { var rec = this.view.getRecord(tip.triggerElement), data; if (rec) { data = tip.initialConfig.data ? Ext.apply(tip.initialConfig.data, rec.data) : rec.data; return updateTip(tip, data); } else { return false; } } function beforeFormTipShow(tip) { var field = Ext.getCmp(tip.triggerElement.id); if (field && (field.tooltip || tip.tpl)) { return updateTip(tip, field.tooltip || field); } else { return false; } } return { extend: 'Ext.tip.ToolTip', mixins: { plugin: 'Ext.plugin.Abstract' }, alias: 'plugin.datatip', lockableScope: 'both', constructor: function(config) { var me = this; me.callParent([ config ]); me.mixins.plugin.constructor.call(me, config); }, init: function(host) { var me = this; me.mixins.plugin.init.call(me, host); host.dataTip = me; me.host = host; if (host.isXType('tablepanel')) { me.view = host.getView(); if (host.ownerLockable) { me.host = host.ownerLockable; } me.delegate = me.delegate || me.view.rowSelector; me.on('beforeshow', beforeViewTipShow); } else if (host.isXType('dataview')) { me.view = me.host; me.delegate = me.delegate || host.itemSelector; me.on('beforeshow', beforeViewTipShow); } else if (host.isXType('form')) { me.delegate = '.' + Ext.form.Labelable.prototype.formItemCls; me.on('beforeshow', beforeFormTipShow); } else if (host.isXType('combobox')) { me.view = host.getPicker(); me.delegate = me.delegate || me.view.getItemSelector(); me.on('beforeshow', beforeViewTipShow); } if (host.rendered) { onHostRender.call(host); } else { host.onRender = Ext.Function.createSequence(host.onRender, onHostRender); } } }; }); /** * @author Ed Spencer (http://sencha.com) * Transition plugin for DataViews */ Ext.define('Ext.ux.DataView.Animated', { /** * @property defaults * @type Object * Default configuration options for all DataViewTransition instances */ defaults: { duration: 750, idProperty: 'id' }, /** * Creates the plugin instance, applies defaults * @constructor * @param {Object} config Optional config object */ constructor: function(config) { Ext.apply(this, config || {}, this.defaults); }, /** * Initializes the transition plugin. Overrides the dataview's default refresh function * @param {Ext.view.View} dataview The dataview */ init: function(dataview) { var me = this, store = dataview.store, items = dataview.all, task = { interval: 20 }, duration = me.duration; /** * @property dataview * @type Ext.view.View * Reference to the DataView this instance is bound to */ me.dataview = dataview; dataview.blockRefresh = true; dataview.updateIndexes = Ext.Function.createSequence(dataview.updateIndexes, function() { this.getTargetEl().select(this.itemSelector).each(function(element, composite, index) { element.dom.id = Ext.util.Format.format("{0}-{1}", dataview.id, store.getAt(index).internalId); }, this); }, dataview); /** * @property dataviewID * @type String * The string ID of the DataView component. This is used internally when animating child objects */ me.dataviewID = dataview.id; /** * @property cachedStoreData * @type Object * A cache of existing store data, keyed by id. This is used to determine * whether any items were added or removed from the store on data change */ me.cachedStoreData = {}; //catch the store data with the snapshot immediately me.cacheStoreData(store.data || store.snapshot); dataview.on('resize', function() { var store = dataview.store; if (store.getCount() > 0) {} }, // reDraw.call(this, store); this); // Buffer listenher so that rapid calls, for example a filter followed by a sort // Only produce one redraw. dataview.store.on({ datachanged: reDraw, scope: this, buffer: 50 }); function reDraw() { var parentEl = dataview.getTargetEl(), parentElY = parentEl.getY(), parentElPaddingTop = parentEl.getPadding('t'), added = me.getAdded(store), removed = me.getRemoved(store), remaining = me.getRemaining(store), itemArray, i, id, itemFly = new Ext.dom.Fly(), rtl = me.dataview.getInherited().rtl, oldPos, newPos, styleSide = rtl ? 'right' : 'left', newStyle = {}; // Not yet rendered if (!parentEl) { return; } // Collect nodes that will be removed in the forthcoming refresh so // that we can put them back in order to fade them out Ext.iterate(removed, function(recId, item) { id = me.dataviewID + '-' + recId; // Stop any animations for removed items and ensure th. Ext.fx.Manager.stopAnimation(id); item.dom = Ext.getDom(id); if (!item.dom) { delete removed[recId]; } }); me.cacheStoreData(store); // stores the current top and left values for each element (discovered below) var oldPositions = {}, newPositions = {}; // Find current positions of elements which are to remain after the refresh. Ext.iterate(remaining, function(id, item) { if (itemFly.attach(Ext.getDom(me.dataviewID + '-' + id))) { oldPos = oldPositions[id] = { top: itemFly.getY() - parentElY - itemFly.getMargin('t') - parentElPaddingTop }; oldPos[styleSide] = me.getItemX(itemFly); } else { delete remaining[id]; } }); // The view MUST refresh, creating items in the natural flow, and collecting the items // so that its item collection is consistent. dataview.refresh(); // Replace removed nodes so that they can be faded out, THEN removed Ext.iterate(removed, function(id, item) { parentEl.dom.appendChild(item.dom); itemFly.attach(item.dom).animate({ duration: duration, opacity: 0, callback: function(anim) { var el = Ext.get(anim.target.id); if (el) { el.destroy(); } } }); delete item.dom; }); // We have taken care of any removals. // If the store is empty, we are done. if (!store.getCount()) { return; } // Collect the correct new positions after the refresh itemArray = items.slice(); // Reverse order so that moving to absolute position does not affect the position of // the next one we're looking at. for (i = itemArray.length - 1; i >= 0; i--) { id = store.getAt(i).internalId; itemFly.attach(itemArray[i]); newPositions[id] = { dom: itemFly.dom, top: itemFly.getY() - parentElY - itemFly.getMargin('t') - parentElPaddingTop }; newPositions[id][styleSide] = me.getItemX(itemFly); // We're going to absolutely position each item. // If it is a "remaining" one from last refesh, shunt it back to // its old position from where it will be animated. newPos = oldPositions[id] || newPositions[id]; // set absolute positioning on all DataView items. We need to set position, left and // top at the same time to avoid any flickering newStyle.position = 'absolute'; newStyle.top = newPos.top + "px"; newStyle[styleSide] = newPos.left + "px"; itemFly.applyStyles(newStyle); } // This is the function which moves remaining items to their new position var doAnimate = function() { var elapsed = new Date() - task.taskStartTime, fraction = elapsed / duration; if (fraction >= 1) { // At end, return all items to natural flow. newStyle.position = newStyle.top = newStyle[styleSide] = ''; for (id in newPositions) { itemFly.attach(newPositions[id].dom).applyStyles(newStyle); } Ext.TaskManager.stop(task); } else { // In frame, move each "remaining" item according to time elapsed for (id in remaining) { var oldPos = oldPositions[id], newPos = newPositions[id], oldTop = oldPos.top, newTop = newPos.top, oldLeft = oldPos[styleSide], newLeft = newPos[styleSide], diffTop = fraction * Math.abs(oldTop - newTop), diffLeft = fraction * Math.abs(oldLeft - newLeft), midTop = oldTop > newTop ? oldTop - diffTop : oldTop + diffTop, midLeft = oldLeft > newLeft ? oldLeft - diffLeft : oldLeft + diffLeft; newStyle.top = midTop + "px"; newStyle[styleSide] = midLeft + "px"; itemFly.attach(newPos.dom).applyStyles(newStyle); } } }; // Fade in new items Ext.iterate(added, function(id, item) { if (itemFly.attach(Ext.getDom(me.dataviewID + '-' + id))) { itemFly.setOpacity(0); itemFly.animate({ duration: duration, opacity: 1 }); } }); // Stop any previous animations Ext.TaskManager.stop(task); task.run = doAnimate; Ext.TaskManager.start(task); me.cacheStoreData(store); } }, getItemX: function(el) { var rtl = this.dataview.getInherited().rtl, parentEl = el.up(''); if (rtl) { return parentEl.getViewRegion().right - el.getRegion().right + el.getMargin('r'); } else { return el.getX() - parentEl.getX() - el.getMargin('l') - parentEl.getPadding('l'); } }, /** * Caches the records from a store locally for comparison later * @param {Ext.data.Store} store The store to cache data from */ cacheStoreData: function(store) { var cachedStoreData = this.cachedStoreData = {}; store.each(function(record) { cachedStoreData[record.internalId] = record; }); }, /** * Returns all records that were already in the DataView * @return {Object} All existing records */ getExisting: function() { return this.cachedStoreData; }, /** * Returns the total number of items that are currently visible in the DataView * @return {Number} The number of existing items */ getExistingCount: function() { var count = 0, items = this.getExisting(); for (var k in items) { count++; } return count; }, /** * Returns all records in the given store that were not already present * @param {Ext.data.Store} store The updated store instance * @return {Object} Object of records not already present in the dataview in format {id: record} */ getAdded: function(store) { var cachedStoreData = this.cachedStoreData, added = {}; store.each(function(record) { if (cachedStoreData[record.internalId] == null) { added[record.internalId] = record; } }); return added; }, /** * Returns all records that are present in the DataView but not the new store * @param {Ext.data.Store} store The updated store instance * @return {Array} Array of records that used to be present */ getRemoved: function(store) { var cachedStoreData = this.cachedStoreData, removed = {}, id; for (id in cachedStoreData) { if (store.findBy(function(record) { return record.internalId == id; }) == -1) { removed[id] = cachedStoreData[id]; } } return removed; }, /** * Returns all records that are already present and are still present in the new store * @param {Ext.data.Store} store The updated store instance * @return {Object} Object of records that are still present from last time in format {id: record} */ getRemaining: function(store) { var cachedStoreData = this.cachedStoreData, remaining = {}; store.each(function(record) { if (cachedStoreData[record.internalId] != null) { remaining[record.internalId] = record; } }); return remaining; } }); /** * @author Ed Spencer */ Ext.define('Ext.ux.DataView.DragSelector', { requires: [ 'Ext.dd.DragTracker', 'Ext.util.Region' ], /** * Initializes the plugin by setting up the drag tracker */ init: function(dataview) { /** * @property dataview * @type Ext.view.View * The DataView bound to this instance */ this.dataview = dataview; dataview.mon(dataview, { beforecontainerclick: this.cancelClick, scope: this, render: { fn: this.onRender, scope: this, single: true } }); }, /** * @private * Called when the attached DataView is rendered. This sets up the DragTracker instance that will be used * to created a dragged selection area */ onRender: function() { /** * @property tracker * @type Ext.dd.DragTracker * The DragTracker attached to this instance. Note that the 4 on* functions are called in the scope of the * DragTracker ('this' refers to the DragTracker inside those functions), so we pass a reference to the * DragSelector so that we can call this class's functions. */ this.tracker = Ext.create('Ext.dd.DragTracker', { dataview: this.dataview, el: this.dataview.el, dragSelector: this, onBeforeStart: this.onBeforeStart, onStart: this.onStart, onDrag: this.onDrag, onEnd: this.onEnd }); /** * @property dragRegion * @type Ext.util.Region * Represents the region currently dragged out by the user. This is used to figure out which dataview nodes are * in the selected area and to set the size of the Proxy element used to highlight the current drag area */ this.dragRegion = Ext.create('Ext.util.Region'); }, /** * @private * Listener attached to the DragTracker's onBeforeStart event. Returns false if the drag didn't start within the * DataView's el */ onBeforeStart: function(e) { return e.target == this.dataview.getEl().dom; }, /** * @private * Listener attached to the DragTracker's onStart event. Cancel's the DataView's containerclick event from firing * and sets the start co-ordinates of the Proxy element. Clears any existing DataView selection * @param {Ext.event.Event} e The click event */ onStart: function(e) { var dragSelector = this.dragSelector, dataview = this.dataview; // Flag which controls whether the cancelClick method vetoes the processing of the DataView's containerclick event. // On IE (where else), this needs to remain set for a millisecond after mouseup because even though the mouse has // moved, the mouseup will still trigger a click event. this.dragging = true; //here we reset and show the selection proxy element and cache the regions each item in the dataview take up dragSelector.fillRegions(); dragSelector.getProxy().show(); dataview.getSelectionModel().deselectAll(); }, /** * @private * Reusable handler that's used to cancel the container click event when dragging on the dataview. See onStart for * details */ cancelClick: function() { return !this.tracker.dragging; }, /** * @private * Listener attached to the DragTracker's onDrag event. Figures out how large the drag selection area should be and * updates the proxy element's size to match. Then iterates over all of the rendered items and marks them selected * if the drag region touches them * @param {Ext.event.Event} e The drag event */ onDrag: function(e) { var dragSelector = this.dragSelector, selModel = dragSelector.dataview.getSelectionModel(), dragRegion = dragSelector.dragRegion, bodyRegion = dragSelector.bodyRegion, proxy = dragSelector.getProxy(), regions = dragSelector.regions, length = regions.length, startXY = this.startXY, currentXY = this.getXY(), minX = Math.min(startXY[0], currentXY[0]), minY = Math.min(startXY[1], currentXY[1]), width = Math.abs(startXY[0] - currentXY[0]), height = Math.abs(startXY[1] - currentXY[1]), region, selected, i; Ext.apply(dragRegion, { top: minY, left: minX, right: minX + width, bottom: minY + height }); dragRegion.constrainTo(bodyRegion); proxy.setBox(dragRegion); for (i = 0; i < length; i++) { region = regions[i]; selected = dragRegion.intersect(region); if (selected) { selModel.select(i, true); } else { selModel.deselect(i); } } }, /** * @private * Listener attached to the DragTracker's onEnd event. This is a delayed function which executes 1 * millisecond after it has been called. This is because the dragging flag must remain active to cancel * the containerclick event which the mouseup event will trigger. * @param {Ext.event.Event} e The event object */ onEnd: Ext.Function.createDelayed(function(e) { var dataview = this.dataview, selModel = dataview.getSelectionModel(), dragSelector = this.dragSelector; this.dragging = false; dragSelector.getProxy().hide(); }, 1), /** * @private * Creates a Proxy element that will be used to highlight the drag selection region * @return {Ext.Element} The Proxy element */ getProxy: function() { if (!this.proxy) { this.proxy = this.dataview.getEl().createChild({ tag: 'div', cls: 'x-view-selector' }); } return this.proxy; }, /** * @private * Gets the region taken up by each rendered node in the DataView. We use these regions to figure out which nodes * to select based on the selector region the user has dragged out */ fillRegions: function() { var dataview = this.dataview, regions = this.regions = []; dataview.all.each(function(node) { regions.push(node.getRegion()); }); this.bodyRegion = dataview.getEl().getRegion(); } }); /** * @author Ed Spencer * * ## Basic DataView with Draggable mixin. * * Ext.Loader.setPath('Ext.ux', '../../../SDK/extjs/examples/ux'); * * Ext.define('My.cool.View', { * extend: 'Ext.view.View', * * mixins: { * draggable: 'Ext.ux.DataView.Draggable' * }, * * initComponent: function() { * this.mixins.draggable.init(this, { * ddConfig: { * ddGroup: 'someGroup' * } * }); * * this.callParent(arguments); * } * }); * * Ext.onReady(function () { * Ext.create('Ext.data.Store', { * storeId: 'baseball', * fields: ['team', 'established'], * data: [ * { team: 'Atlanta Braves', established: '1871' }, * { team: 'Miami Marlins', established: '1993' }, * { team: 'New York Mets', established: '1962' }, * { team: 'Philadelphia Phillies', established: '1883' }, * { team: 'Washington Nationals', established: '1969' } * ] * }); * * Ext.create('My.cool.View', { * store: Ext.StoreMgr.get('baseball'), * tpl: [ * '', * '

', * 'The {team} were founded in {established}.', * '

', * '
' * ], * itemSelector: 'p.team', * renderTo: Ext.getBody() * }); * }); */ Ext.define('Ext.ux.DataView.Draggable', { requires: 'Ext.dd.DragZone', /** * @cfg {String} ghostCls The CSS class added to the outermost element of the created ghost proxy * (defaults to 'x-dataview-draggable-ghost') */ ghostCls: 'x-dataview-draggable-ghost', /** * @cfg {Ext.XTemplate/Array} ghostTpl The template used in the ghost DataView */ ghostTpl: [ '', '{title}', '' ], /** * @cfg {Object} ddConfig Config object that is applied to the internally created DragZone */ /** * @cfg {String} ghostConfig Config object that is used to configure the internally created DataView */ init: function(dataview, config) { /** * @property dataview * @type Ext.view.View * The Ext.view.View instance that this DragZone is attached to */ this.dataview = dataview; dataview.on('render', this.onRender, this); Ext.apply(this, { itemSelector: dataview.itemSelector, ghostConfig: {} }, config || {}); Ext.applyIf(this.ghostConfig, { itemSelector: 'img', cls: this.ghostCls, tpl: this.ghostTpl }); }, /** * @private * Called when the attached DataView is rendered. Sets up the internal DragZone */ onRender: function() { var config = Ext.apply({}, this.ddConfig || {}, { dvDraggable: this, dataview: this.dataview, getDragData: this.getDragData, getTreeNode: this.getTreeNode, afterRepair: this.afterRepair, getRepairXY: this.getRepairXY }); /** * @property dragZone * @type Ext.dd.DragZone * The attached DragZone instane */ this.dragZone = Ext.create('Ext.dd.DragZone', this.dataview.getEl(), config); }, getDragData: function(e) { var draggable = this.dvDraggable, dataview = this.dataview, selModel = dataview.getSelectionModel(), target = e.getTarget(draggable.itemSelector), selected, dragData; if (target) { if (!dataview.isSelected(target)) { selModel.select(dataview.getRecord(target)); } selected = dataview.getSelectedNodes(); dragData = { copy: true, nodes: selected, records: selModel.getSelection(), item: true }; if (selected.length === 1) { dragData.single = true; dragData.ddel = target; } else { dragData.multi = true; dragData.ddel = draggable.prepareGhost(selModel.getSelection()); } return dragData; } return false; }, getTreeNode: function() {}, // console.log('test'); afterRepair: function() { this.dragging = false; var nodes = this.dragData.nodes, length = nodes.length, i; //FIXME: Ext.fly does not work here for some reason, only frames the last node for (i = 0; i < length; i++) { Ext.get(nodes[i]).frame('#8db2e3', 1); } }, /** * @private * Returns the x and y co-ordinates that the dragged item should be animated back to if it was dropped on an * invalid drop target. If we're dragging more than one item we don't animate back and just allow afterRepair * to frame each dropped item. */ getRepairXY: function(e) { if (this.dragData.multi) { return false; } else { var repairEl = Ext.get(this.dragData.ddel), repairXY = repairEl.getXY(); //take the item's margins and padding into account to make the repair animation line up perfectly repairXY[0] += repairEl.getPadding('t') + repairEl.getMargin('t'); repairXY[1] += repairEl.getPadding('l') + repairEl.getMargin('l'); return repairXY; } }, /** * Updates the internal ghost DataView by ensuring it is rendered and contains the correct records * @param {Array} records The set of records that is currently selected in the parent DataView * @return {HTMLElement} The Ghost DataView's encapsulating HtmnlElement. */ prepareGhost: function(records) { return this.createGhost(records).getEl().dom; }, /** * @private * Creates the 'ghost' DataView that follows the mouse cursor during the drag operation. This div is usually a * lighter-weight representation of just the nodes that are selected in the parent DataView. */ createGhost: function(records) { var me = this, store; if (me.ghost) { (store = me.ghost.store).loadRecords(records); } else { store = Ext.create('Ext.data.Store', { model: records[0].self }); store.loadRecords(records); me.ghost = Ext.create('Ext.view.View', Ext.apply({ renderTo: document.createElement('div'), store: store }, me.ghostConfig)); me.ghost.container.skipGarbageCollection = me.ghost.el.skipGarbageCollection = true; } store.clearData(); return me.ghost; }, destroy: function() { if (this.ghost) { this.ghost.container.destroy(); this.ghost.destroy(); } } }); /** * */ Ext.define('Ext.ux.DataView.LabelEditor', { extend: 'Ext.Editor', alignment: 'tl-tl', completeOnEnter: true, cancelOnEsc: true, shim: false, autoSize: { width: 'boundEl', height: 'field' }, labelSelector: 'x-editable', requires: [ 'Ext.form.field.Text' ], constructor: function(config) { config.field = config.field || Ext.create('Ext.form.field.Text', { allowOnlyWhitespace: false, selectOnFocus: true }); this.callParent([ config ]); }, init: function(view) { this.view = view; this.mon(view, 'afterrender', this.bindEvents, this); this.on('complete', this.onSave, this); }, // initialize events bindEvents: function() { this.mon(this.view.getEl(), { click: { fn: this.onClick, scope: this } }); }, // on mousedown show editor onClick: function(e, target) { var me = this, item, record; if (Ext.fly(target).hasCls(me.labelSelector) && !me.editing && !e.ctrlKey && !e.shiftKey) { e.stopEvent(); item = me.view.findItemByChild(target); record = me.view.store.getAt(me.view.indexOf(item)); me.startEdit(target, record.data[me.dataIndex]); me.activeRecord = record; } else if (me.editing) { me.field.blur(); e.preventDefault(); } }, // update record onSave: function(ed, value) { this.activeRecord.set(this.dataIndex, value); } }); /** * @author Ed Spencer (http://sencha.com) * Transition plugin for DataViews */ Ext.ux.DataViewTransition = Ext.extend(Object, { /** * @property defaults * @type Object * Default configuration options for all DataViewTransition instances */ defaults: { duration: 750, idProperty: 'id' }, /** * Creates the plugin instance, applies defaults * @constructor * @param {Object} config Optional config object */ constructor: function(config) { Ext.apply(this, config || {}, this.defaults); }, /** * Initializes the transition plugin. Overrides the dataview's default refresh function * @param {Ext.view.View} dataview The dataview */ init: function(dataview) { /** * @property dataview * @type Ext.view.View * Reference to the DataView this instance is bound to */ this.dataview = dataview; var idProperty = this.idProperty; dataview.blockRefresh = true; dataview.updateIndexes = Ext.Function.createSequence(dataview.updateIndexes, function() { this.getTargetEl().select(this.itemSelector).each(function(element, composite, index) { element.id = element.dom.id = Ext.util.Format.format("{0}-{1}", dataview.id, dataview.store.getAt(index).get(idProperty)); }, this); }, dataview); /** * @property dataviewID * @type String * The string ID of the DataView component. This is used internally when animating child objects */ this.dataviewID = dataview.id; /** * @property cachedStoreData * @type Object * A cache of existing store data, keyed by id. This is used to determine * whether any items were added or removed from the store on data change */ this.cachedStoreData = {}; //var store = dataview.store; //catch the store data with the snapshot immediately this.cacheStoreData(dataview.store.snapshot); dataview.store.on('datachanged', function(store) { var parentEl = dataview.getTargetEl(), calcItem = store.getAt(0), added = this.getAdded(store), removed = this.getRemoved(store), previous = this.getRemaining(store), existing = Ext.apply({}, previous, added); //hide old items Ext.each(removed, function(item) { Ext.fly(this.dataviewID + '-' + item.get(this.idProperty)).animate({ remove: false, duration: duration, opacity: 0, useDisplay: true }); }, this); //store is empty if (calcItem == undefined) { this.cacheStoreData(store); return; } var el = Ext.get(this.dataviewID + "-" + calcItem.get(this.idProperty)); //calculate the number of rows and columns we have var itemCount = store.getCount(), itemWidth = el.getMargin('lr') + el.getWidth(), itemHeight = el.getMargin('bt') + el.getHeight(), dvWidth = parentEl.getWidth(), columns = Math.floor(dvWidth / itemWidth), rows = Math.ceil(itemCount / columns), currentRows = Math.ceil(this.getExistingCount() / columns); //make sure the correct styles are applied to the parent element parentEl.applyStyles({ display: 'block', position: 'relative' }); //stores the current top and left values for each element (discovered below) var oldPositions = {}, newPositions = {}, elCache = {}; //find current positions of each element and save a reference in the elCache Ext.iterate(previous, function(id, item) { var id = item.get(this.idProperty), el = elCache[id] = Ext.get(this.dataviewID + '-' + id); oldPositions[id] = { top: el.getY() - parentEl.getY() - el.getMargin('t') - parentEl.getPadding('t'), left: el.getX() - parentEl.getX() - el.getMargin('l') - parentEl.getPadding('l') }; }, this); //set absolute positioning on all DataView items. We need to set position, left and //top at the same time to avoid any flickering Ext.iterate(previous, function(id, item) { var oldPos = oldPositions[id], el = elCache[id]; if (el.getStyle('position') != 'absolute') { elCache[id].applyStyles({ position: 'absolute', left: oldPos.left + "px", top: oldPos.top + "px", //we set the width here to make ListViews work correctly. This is not needed for DataViews width: el.getWidth(!Ext.isIE || Ext.isStrict), height: el.getHeight(!Ext.isIE || Ext.isStrict) }); } }); //get new positions var index = 0; Ext.iterate(store.data.items, function(item) { var id = item.get(idProperty), el = elCache[id]; var column = index % columns, row = Math.floor(index / columns), top = row * itemHeight, left = column * itemWidth; newPositions[id] = { top: top, left: left }; index++; }, this); //do the movements var startTime = new Date(), duration = this.duration, dataviewID = this.dataviewID; var doAnimate = function() { var elapsed = new Date() - startTime, fraction = elapsed / duration; if (fraction >= 1) { for (var id in newPositions) { Ext.fly(dataviewID + '-' + id).applyStyles({ top: newPositions[id].top + "px", left: newPositions[id].left + "px" }); } Ext.TaskManager.stop(task); } else { //move each item for (var id in newPositions) { if (!previous[id]) { continue; } var oldPos = oldPositions[id], newPos = newPositions[id], oldTop = oldPos.top, newTop = newPos.top, oldLeft = oldPos.left, newLeft = newPos.left, diffTop = fraction * Math.abs(oldTop - newTop), diffLeft = fraction * Math.abs(oldLeft - newLeft), midTop = oldTop > newTop ? oldTop - diffTop : oldTop + diffTop, midLeft = oldLeft > newLeft ? oldLeft - diffLeft : oldLeft + diffLeft; Ext.fly(dataviewID + '-' + id).applyStyles({ top: midTop + "px", left: midLeft + "px" }); } } }; var task = { run: doAnimate, interval: 20, scope: this }; Ext.TaskManager.start(task); var count = 0; for (var k in added) { count++; } if (Ext.global.console && Ext.global.console.log) { Ext.global.console.log('added:', count); } //show new items Ext.iterate(added, function(id, item) { Ext.fly(this.dataviewID + '-' + item.get(this.idProperty)).applyStyles({ top: newPositions[item.get(this.idProperty)].top + "px", left: newPositions[item.get(this.idProperty)].left + "px" }); Ext.fly(this.dataviewID + '-' + item.get(this.idProperty)).animate({ remove: false, duration: duration, opacity: 1 }); }, this); this.cacheStoreData(store); }, this); }, /** * Caches the records from a store locally for comparison later * @param {Ext.data.Store} store The store to cache data from */ cacheStoreData: function(store) { this.cachedStoreData = {}; store.each(function(record) { this.cachedStoreData[record.get(this.idProperty)] = record; }, this); }, /** * Returns all records that were already in the DataView * @return {Object} All existing records */ getExisting: function() { return this.cachedStoreData; }, /** * Returns the total number of items that are currently visible in the DataView * @return {Number} The number of existing items */ getExistingCount: function() { var count = 0, items = this.getExisting(); for (var k in items) count++; return count; }, /** * Returns all records in the given store that were not already present * @param {Ext.data.Store} store The updated store instance * @return {Object} Object of records not already present in the dataview in format {id: record} */ getAdded: function(store) { var added = {}; store.each(function(record) { if (this.cachedStoreData[record.get(this.idProperty)] == undefined) { added[record.get(this.idProperty)] = record; } }, this); return added; }, /** * Returns all records that are present in the DataView but not the new store * @param {Ext.data.Store} store The updated store instance * @return {Array} Array of records that used to be present */ getRemoved: function(store) { var removed = []; for (var id in this.cachedStoreData) { if (store.findExact(this.idProperty, Number(id)) == -1) { removed.push(this.cachedStoreData[id]); } } return removed; }, /** * Returns all records that are already present and are still present in the new store * @param {Ext.data.Store} store The updated store instance * @return {Object} Object of records that are still present from last time in format {id: record} */ getRemaining: function(store) { var remaining = {}; store.each(function(record) { if (this.cachedStoreData[record.get(this.idProperty)] != undefined) { remaining[record.get(this.idProperty)] = record; } }, this); return remaining; } }); /** * An explorer component for navigating hierarchical content. Consists of a breadcrumb bar * at the top, tree navigation on the left, and a center panel which displays the contents * of a given node. */ Ext.define('Ext.ux.Explorer', { extend: 'Ext.panel.Panel', xtype: 'explorer', requires: [ 'Ext.layout.container.Border', 'Ext.toolbar.Breadcrumb', 'Ext.tree.Panel' ], config: { /** * @cfg {Object} breadcrumb * Configuration object for the breadcrumb toolbar */ breadcrumb: { dock: 'top', xtype: 'breadcrumb', reference: 'breadcrumb' }, /** * @cfg {Object} contentView * Configuration object for the "content" data view */ contentView: { xtype: 'dataview', reference: 'contentView', region: 'center', cls: Ext.baseCSSPrefix + 'explorer-view', itemSelector: '.' + Ext.baseCSSPrefix + 'explorer-item', tpl: '' + '
' + '
' + '
' + '
' + '
{text}
' + '
' + '
' + '
' }, /** * @cfg {Ext.data.TreeStore} store * The TreeStore to use as the data source */ store: null, /** * @cfg {Object} tree * Configuration object for the tree */ tree: { xtype: 'treepanel', reference: 'tree', region: 'west', width: 200 } }, renderConfig: { /** * @cfg {Ext.data.TreeModel} selection * The selected node */ selection: null }, layout: 'border', referenceHolder: true, defaultListenerScope: true, cls: Ext.baseCSSPrefix + 'explorer', initComponent: function() { var me = this, store = me.getStore(); if (!store) { Ext.Error.raise('Ext.ux.Explorer requires a store.'); } me.dockedItems = [ me.getBreadcrumb() ]; me.items = [ me.getTree(), me.getContentView() ]; me.callParent(); }, applyBreadcrumb: function(breadcrumb) { var store = this.getStore(); breadcrumb = Ext.create(Ext.apply({ store: store, selection: store.getRoot() }, breadcrumb)); breadcrumb.on('selectionchange', '_onBreadcrumbSelectionChange', this); return breadcrumb; }, applyContentView: function(contentView) { /** * @property {Ext.data.Store} contentStore * @private * The backing store for the content view */ var contentStore = this.contentStore = new Ext.data.Store({ model: this.getStore().model }); contentView = Ext.create(Ext.apply({ store: contentStore }, contentView)); return contentView; }, applyTree: function(tree) { tree = Ext.create(Ext.apply({ store: this.getStore() }, tree)); tree.on('selectionchange', '_onTreeSelectionChange', this); return tree; }, updateSelection: function(node) { var me = this, refs = me.getReferences(), breadcrumb = refs.breadcrumb, tree = refs.tree, treeSelectionModel = tree.getSelectionModel(), contentStore = me.contentStore, parentNode, treeView; if (breadcrumb.getSelection() !== node) { breadcrumb.setSelection(node); } if (treeSelectionModel.getSelection()[0] !== node) { treeSelectionModel.select([ node ]); parentNode = node.parentNode; if (parentNode) { parentNode.expand(); } treeView = tree.getView(); treeView.scrollRowIntoView(treeView.getRow(node)); } contentStore.removeAll(); contentStore.add(node.hasChildNodes() ? node.childNodes : [ node ]); }, updateStore: function(store) { this.getBreadcrumb().setStore(store); }, privates: { /** * Handles the tree's selectionchange event * @private * @param {Ext.tree.Panel} tree * @param {Ext.data.TreeModel[]} selection */ _onTreeSelectionChange: function(tree, selection) { this.setSelection(selection[0]); }, /** * Handles the breadcrumb bar's selectionchange event */ _onBreadcrumbSelectionChange: function(breadcrumb, selection) { this.setSelection(selection); } } }); /** *

A plugin for Field Components which creates clones of the Field for as * long as the user keeps filling them. Leaving the final one blank ends the repeating series.

*

Usage:

*

    {
        xtype: 'combo',
        plugins: [ Ext.ux.FieldReplicator ],
        triggerAction: 'all',
        fieldLabel: 'Select recipient',
        store: recipientStore
    }
 * 
*/ Ext.define('Ext.ux.FieldReplicator', { alias: 'plugin.fieldreplicator', init: function(field) { // Assign the field an id grouping it with fields cloned from it. If it already // has an id that means it is itself a clone. if (!field.replicatorId) { field.replicatorId = Ext.id(); } field.on('blur', this.onBlur, this); }, onBlur: function(field) { var ownerCt = field.ownerCt, replicatorId = field.replicatorId, isEmpty = Ext.isEmpty(field.getRawValue()), siblings = ownerCt.query('[replicatorId=' + replicatorId + ']'), isLastInGroup = siblings[siblings.length - 1] === field, clone, idx; // If a field before the final one was blanked out, remove it if (isEmpty && !isLastInGroup) { Ext.Function.defer(field.destroy, 10, field); } //delay to allow tab key to move focus first // If the field is the last in the list and has a value, add a cloned field after it else if (!isEmpty && isLastInGroup) { if (field.onReplicate) { field.onReplicate(); } clone = field.cloneConfig({ replicatorId: replicatorId }); idx = ownerCt.items.indexOf(field); ownerCt.add(idx + 1, clone); } } }); /** * @author Shea Frederick * * The GMap Panel UX extends `Ext.panel.Panel` in order to display Google Maps. * * It is important to note that you must include the following Google Maps API above bootstrap.js in your * application's index.html file (or equivilant). * * * * It is important to note that due to the Google Maps loader, you cannot currently include * the above JS resource in the Cmd generated app.json file. Doing so interferes with the loading of * Ext JS and Google Maps. * * The following example creates a window containing a GMap Panel. In this case, the center * is set as geoCodeAddr, which is a string that Google translates into longitude and latitude. * * var mapwin = Ext.create('Ext.Window', { * layout: 'fit', * title: 'GMap Window', * width: 450, * height: 250, * items: { * xtype: 'gmappanel', * gmapType: 'map', * center: { * geoCodeAddr: "221B Baker Street", * marker: { * title: 'Holmes Home' * } * }, * mapOptions : { * mapTypeId: google.maps.MapTypeId.ROADMAP * } * } * }).show(); * */ Ext.define('Ext.ux.GMapPanel', { extend: 'Ext.panel.Panel', alias: 'widget.gmappanel', requires: [ 'Ext.window.MessageBox' ], initComponent: function() { Ext.applyIf(this, { plain: true, gmapType: 'map', border: false }); this.callParent(); }, onBoxReady: function() { var center = this.center; this.callParent(arguments); if (center) { if (center.geoCodeAddr) { this.lookupCode(center.geoCodeAddr, center.marker); } else { this.createMap(center); } } else { Ext.Error.raise('center is required'); } }, createMap: function(center, marker) { var options = Ext.apply({}, this.mapOptions); options = Ext.applyIf(options, { zoom: 14, center: center, mapTypeId: google.maps.MapTypeId.HYBRID }); this.gmap = new google.maps.Map(this.body.dom, options); if (marker) { this.addMarker(Ext.applyIf(marker, { position: center })); } Ext.each(this.markers, this.addMarker, this); this.fireEvent('mapready', this, this.gmap); }, addMarker: function(marker) { marker = Ext.apply({ map: this.gmap }, marker); if (!marker.position) { marker.position = new google.maps.LatLng(marker.lat, marker.lng); } var o = new google.maps.Marker(marker); Ext.Object.each(marker.listeners, function(name, fn) { google.maps.event.addListener(o, name, fn); }); return o; }, lookupCode: function(addr, marker) { this.geocoder = new google.maps.Geocoder(); this.geocoder.geocode({ address: addr }, Ext.Function.bind(this.onLookupComplete, this, [ marker ], true)); }, onLookupComplete: function(data, response, marker) { if (response != 'OK') { Ext.MessageBox.alert('Error', 'An error occured: "' + response + '"'); return; } this.createMap(data[0].geometry.location, marker); }, afterComponentLayout: function(w, h) { this.callParent(arguments); this.redraw(); }, redraw: function() { var map = this.gmap; if (map) { google.maps.event.trigger(map, 'resize'); } } }); /** * Allows GroupTab to render a table structure. */ Ext.define('Ext.ux.GroupTabRenderer', { extend: 'Ext.plugin.Abstract', alias: 'plugin.grouptabrenderer', tableTpl: new Ext.XTemplate('
', '{%', 'values.view.renderRows(values.rows, values.viewStartIndex, out);', '%}', '
', { priority: 5 }), rowTpl: new Ext.XTemplate('{%', 'Ext.Array.remove(values.itemClasses, "', Ext.baseCSSPrefix + 'grid-row");', 'var dataRowCls = values.recordIndex === -1 ? "" : " ' + Ext.baseCSSPrefix + 'grid-data-row";', '%}', '
', '' + '{%', 'parent.view.renderCell(values, parent.record, parent.recordIndex, parent.rowIndex, xindex - 1, out, parent)', '%}', '', '
', { priority: 5 }), cellTpl: new Ext.XTemplate('{%values.tdCls = values.tdCls.replace(" ' + Ext.baseCSSPrefix + 'grid-cell "," ");%}', '
', '
{value}
', '
', '
', '
', { priority: 5 }), selectors: { // Outer table bodySelector: 'div.' + Ext.baseCSSPrefix + 'grid-table-resizer', // Element which contains rows nodeContainerSelector: 'div.' + Ext.baseCSSPrefix + 'grid-table-resizer', // row itemSelector: 'div.' + Ext.baseCSSPrefix + 'grouptab-row', // row which contains cells as opposed to wrapping rows rowSelector: 'div.' + Ext.baseCSSPrefix + 'grouptab-row', // cell cellSelector: 'div.' + Ext.baseCSSPrefix + 'grouptab-cell', getCellSelector: function(header) { return header ? header.getCellSelector() : this.cellSelector; } }, init: function(grid) { var view = grid.getView(), me = this; view.addTpl(me.tableTpl); view.addRowTpl(me.rowTpl); view.addCellTpl(me.cellTpl); Ext.apply(view, me.selectors); } }); /** * @author Nicolas Ferrero * A TabPanel with grouping support. */ Ext.define('Ext.ux.GroupTabPanel', { extend: 'Ext.Container', alias: 'widget.grouptabpanel', requires: [ 'Ext.tree.Panel', 'Ext.ux.GroupTabRenderer' ], baseCls: Ext.baseCSSPrefix + 'grouptabpanel', /** * @event beforetabchange * Fires before a tab change (activated by {@link #setActiveTab}). Return false in any listener to cancel * the tabchange * @param {Ext.ux.GroupTabPanel} grouptabPanel The GroupTabPanel * @param {Ext.Component} newCard The card that is about to be activated * @param {Ext.Component} oldCard The card that is currently active */ /** * @event tabchange * Fires when a new tab has been activated (activated by {@link #setActiveTab}). * @param {Ext.ux.GroupTabPanel} grouptabPanel The GroupTabPanel * @param {Ext.Component} newCard The newly activated item * @param {Ext.Component} oldCard The previously active item */ /** * @event beforegroupchange * Fires before a group change (activated by {@link #setActiveGroup}). Return false in any listener to cancel * the groupchange * @param {Ext.ux.GroupTabPanel} grouptabPanel The GroupTabPanel * @param {Ext.Component} newGroup The root group card that is about to be activated * @param {Ext.Component} oldGroup The root group card that is currently active */ /** * @event groupchange * Fires when a new group has been activated (activated by {@link #setActiveGroup}). * @param {Ext.ux.GroupTabPanel} grouptabPanel The GroupTabPanel * @param {Ext.Component} newGroup The newly activated root group item * @param {Ext.Component} oldGroup The previously active root group item */ initComponent: function(config) { var me = this; Ext.apply(me, config); // Processes items to create the TreeStore and also set up // "this.cards" containing the actual card items. me.store = me.createTreeStore(); me.layout = { type: 'hbox', align: 'stretch' }; me.defaults = { border: false }; me.items = [ { xtype: 'treepanel', cls: 'x-tree-panel x-grouptabbar', width: 150, rootVisible: false, store: me.store, hideHeaders: true, animate: false, processEvent: Ext.emptyFn, border: false, plugins: [ { ptype: 'grouptabrenderer' } ], viewConfig: { overItemCls: '', getRowClass: me.getRowClass }, columns: [ { xtype: 'treecolumn', sortable: false, dataIndex: 'text', flex: 1, renderer: function(value, cell, node, idx1, idx2, store, tree) { var cls = ''; if (node.parentNode && node.parentNode.parentNode === null) { cls += ' x-grouptab-first'; if (node.previousSibling) { cls += ' x-grouptab-prev'; } if (!node.get('expanded') || node.firstChild == null) { cls += ' x-grouptab-last'; } } else if (node.nextSibling === null) { cls += ' x-grouptab-last'; } else { cls += ' x-grouptab-center'; } if (node.data.activeTab) { cls += ' x-active-tab'; } cell.tdCls = 'x-grouptab' + cls; return value; } } ] }, { xtype: 'container', flex: 1, layout: 'card', activeItem: me.mainItem, baseCls: Ext.baseCSSPrefix + 'grouptabcontainer', items: me.cards } ]; me.callParent(arguments); me.setActiveTab(me.activeTab); me.setActiveGroup(me.activeGroup); me.mon(me.down('treepanel').getSelectionModel(), 'select', me.onNodeSelect, me); }, getRowClass: function(node, rowIndex, rowParams, store) { var cls = ''; if (node.data.activeGroup) { cls += ' x-active-group'; } return cls; }, /** * @private * Node selection listener. */ onNodeSelect: function(selModel, node) { var me = this, currentNode = me.store.getRootNode(), parent; if (node.parentNode && node.parentNode.parentNode === null) { parent = node; } else { parent = node.parentNode; } if (me.setActiveGroup(parent.get('id')) === false || me.setActiveTab(node.get('id')) === false) { return false; } while (currentNode) { currentNode.set('activeTab', false); currentNode.set('activeGroup', false); currentNode = currentNode.firstChild || currentNode.nextSibling || currentNode.parentNode.nextSibling; } parent.set('activeGroup', true); parent.eachChild(function(child) { child.set('activeGroup', true); }); node.set('activeTab', true); selModel.view.refresh(); }, /** * Makes the given component active (makes it the visible card in the GroupTabPanel's CardLayout) * @param {Ext.Component} cmp The component to make active */ setActiveTab: function(cmp) { var me = this, newTab = cmp, oldTab; if (Ext.isString(cmp)) { newTab = Ext.getCmp(newTab); } if (newTab === me.activeTab) { return false; } oldTab = me.activeTab; if (me.fireEvent('beforetabchange', me, newTab, oldTab) !== false) { me.activeTab = newTab; if (me.rendered) { me.down('container[baseCls=' + Ext.baseCSSPrefix + 'grouptabcontainer' + ']').getLayout().setActiveItem(newTab); } me.fireEvent('tabchange', me, newTab, oldTab); } return true; }, /** * Makes the given group active * @param {Ext.Component} cmp The root component to make active. */ setActiveGroup: function(cmp) { var me = this, newGroup = cmp, oldGroup; if (Ext.isString(cmp)) { newGroup = Ext.getCmp(newGroup); } if (newGroup === me.activeGroup) { return true; } oldGroup = me.activeGroup; if (me.fireEvent('beforegroupchange', me, newGroup, oldGroup) !== false) { me.activeGroup = newGroup; me.fireEvent('groupchange', me, newGroup, oldGroup); } else { return false; } return true; }, /** * @private * Creates the TreeStore used by the GroupTabBar. */ createTreeStore: function() { var me = this, groups = me.prepareItems(me.items), data = { text: '.', children: [] }, cards = me.cards = []; me.activeGroup = me.activeGroup || 0; Ext.each(groups, function(groupItem, idx) { var leafItems = groupItem.items.items, rootItem = (leafItems[groupItem.mainItem] || leafItems[0]), groupRoot = { children: [] }; // Create the root node of the group groupRoot.id = rootItem.id; groupRoot.text = rootItem.title; groupRoot.iconCls = rootItem.iconCls; groupRoot.expanded = true; groupRoot.activeGroup = (me.activeGroup === idx); groupRoot.activeTab = groupRoot.activeGroup ? true : false; if (groupRoot.activeTab) { me.activeTab = groupRoot.id; } if (groupRoot.activeGroup) { me.mainItem = groupItem.mainItem || 0; me.activeGroup = groupRoot.id; } Ext.each(leafItems, function(leafItem) { // First node has been done if (leafItem.id !== groupRoot.id) { var child = { id: leafItem.id, leaf: true, text: leafItem.title, iconCls: leafItem.iconCls, activeGroup: groupRoot.activeGroup, activeTab: false }; groupRoot.children.push(child); } // Ensure the items do not get headers delete leafItem.title; delete leafItem.iconCls; cards.push(leafItem); }); data.children.push(groupRoot); }); return Ext.create('Ext.data.TreeStore', { fields: [ 'id', 'text', 'activeGroup', 'activeTab' ], root: { expanded: true }, proxy: { type: 'memory', data: data } }); }, /** * Returns the item that is currently active inside this GroupTabPanel. * @return {Ext.Component/Number} The currently active item */ getActiveTab: function() { return this.activeTab; }, /** * Returns the root group item that is currently active inside this GroupTabPanel. * @return {Ext.Component/Number} The currently active root group item */ getActiveGroup: function() { return this.activeGroup; } }); /** * Barebones iframe implementation. */ Ext.define('Ext.ux.IFrame', { extend: 'Ext.Component', alias: 'widget.uxiframe', loadMask: 'Loading...', src: 'about:blank', renderTpl: [ '' ], childEls: [ 'iframeEl' ], initComponent: function() { this.callParent(); this.frameName = this.frameName || this.id + '-frame'; }, initEvents: function() { var me = this; me.callParent(); me.iframeEl.on('load', me.onLoad, me); }, initRenderData: function() { return Ext.apply(this.callParent(), { src: this.src, frameName: this.frameName }); }, getBody: function() { var doc = this.getDoc(); return doc.body || doc.documentElement; }, getDoc: function() { try { return this.getWin().document; } catch (ex) { return null; } }, getWin: function() { var me = this, name = me.frameName, win = Ext.isIE ? me.iframeEl.dom.contentWindow : window.frames[name]; return win; }, getFrame: function() { var me = this; return me.iframeEl.dom; }, beforeDestroy: function() { this.cleanupListeners(true); this.callParent(); }, cleanupListeners: function(destroying) { var doc, prop; if (this.rendered) { try { doc = this.getDoc(); if (doc) { Ext.get(doc).un(this._docListeners); if (destroying) { for (prop in doc) { if (doc.hasOwnProperty && doc.hasOwnProperty(prop)) { delete doc[prop]; } } } } } catch (e) {} } }, onLoad: function() { var me = this, doc = me.getDoc(), fn = me.onRelayedEvent; if (doc) { try { // These events need to be relayed from the inner document (where they stop // bubbling) up to the outer document. This has to be done at the DOM level so // the event reaches listeners on elements like the document body. The effected // mechanisms that depend on this bubbling behavior are listed to the right // of the event. Ext.get(doc).on(me._docListeners = { mousedown: fn, // menu dismisal (MenuManager) and Window onMouseDown (toFront) mousemove: fn, // window resize drag detection mouseup: fn, // window resize termination click: fn, // not sure, but just to be safe dblclick: fn, // not sure again scope: me }); } catch (e) {} // cannot do this xss // We need to be sure we remove all our events from the iframe on unload or we're going to LEAK! Ext.get(this.getWin()).on('beforeunload', me.cleanupListeners, me); this.el.unmask(); this.fireEvent('load', this); } else if (me.src) { this.el.unmask(); this.fireEvent('error', this); } }, onRelayedEvent: function(event) { // relay event from the iframe's document to the document that owns the iframe... var iframeEl = this.iframeEl, // Get the left-based iframe position iframeXY = iframeEl.getTrueXY(), originalEventXY = event.getXY(), // Get the left-based XY position. // This is because the consumer of the injected event will // perform its own RTL normalization. eventXY = event.getTrueXY(); // the event from the inner document has XY relative to that document's origin, // so adjust it to use the origin of the iframe in the outer document: event.xy = [ iframeXY[0] + eventXY[0], iframeXY[1] + eventXY[1] ]; event.injectEvent(iframeEl); // blame the iframe for the event... event.xy = originalEventXY; }, // restore the original XY (just for safety) load: function(src) { var me = this, text = me.loadMask, frame = me.getFrame(); if (me.fireEvent('beforeload', me, src) !== false) { if (text && me.el) { me.el.mask(text); } frame.src = me.src = (src || me.src); } } }); /* * TODO items: * * Iframe should clean up any Ext.dom.Element wrappers around its window, document * documentElement and body when it is destroyed. This helps prevent "Permission Denied" * errors in IE when Ext.dom.GarbageCollector tries to access those objects on an orphaned * iframe. Permission Denied errors can occur in one of the following 2 scenarios: * * a. When an iframe is removed from the document, and all references to it have been * removed, IE will "clear" the window object. At this point the window object becomes * completely inaccessible - accessing any of its properties results in a "Permission * Denied" error. http://msdn.microsoft.com/en-us/library/ie/hh180174(v=vs.85).aspx * * b. When an iframe is unloaded (either by navigating to a new url, or via document.open/ * document.write, new html and body elements are created and the old the html and body * elements are orphaned. Accessing the html and body elements or any of their properties * results in a "Permission Denied" error. */ /** * 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.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. * * Ext.create('Ext.Panel', { * title: 'StatusBar', * // etc. * bbar: Ext.create('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 * */ Ext.define('Ext.ux.statusbar.StatusBar', { extend: 'Ext.toolbar.Toolbar', alternateClassName: 'Ext.ux.StatusBar', alias: 'widget.statusbar', requires: [ 'Ext.toolbar.TextItem' ], /** * @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 #cfg-items} config, or added via {@link #method-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: * * // Create a left-aligned status bar containing a button, * // separator and text item that will be right-aligned (default): * Ext.create('Ext.Panel', { * title: 'StatusBar', * // etc. * bbar: Ext.create('Ext.ux.statusbar.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: * Ext.create('Ext.Panel', { * title: 'StatusBar', * // etc. * bbar: Ext.create('Ext.ux.statusbar.StatusBar', { * defaultText: 'Default status text', * id: 'status-id', * statusAlign: 'right', * items: [{ * text: 'A Button' * }, '-', 'Plain Text'] * }) * }); */ /** * @cfg {String} [defaultText=''] * The default {@link #text} value. This will be used anytime the status bar is cleared with the * `useDefaults:true` option. */ /** * @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 `useDefaults:true` option. */ /** * @cfg {String} text * A string that will be initially 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 {@link #defaultText} will be used. */ /** * @cfg {String} [iconCls=''] * A CSS class that will be **initially** set as the status bar icon and is * expected to provide a background image. * * Example usage: * * // 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 = Ext.create('Ext.ux.statusbar.StatusBar', { * defaultIconCls: 'x-status-custom' * }); * * // Changing the icon: * sb.setStatus({ * text: 'New status', * iconCls: 'x-status-custom' * }); */ /** * @cfg {String} cls * The base class applied to the containing element for this component on render. */ cls: 'x-statusbar', /** * @cfg {String} busyIconCls * The default {@link #iconCls} applied when calling {@link #showBusy}. * It can be overridden at any time by passing the `iconCls` argument into {@link #showBusy}. */ busyIconCls: 'x-status-busy', /** * @cfg {String} busyText * The default {@link #text} applied when calling {@link #showBusy}. * It can be overridden at any time by passing the `text` argument into {@link #showBusy}. */ busyText: 'Loading...', /** * @cfg {Number} autoClear * The number of milliseconds to wait after setting the status via * {@link #setStatus} before automatically clearing the status text and icon. * Note that this only applies when passing the `clear` argument to {@link #setStatus} * since that is the only way to defer clearing the status. This can * be overridden by specifying a different `wait` value in {@link #setStatus}. * Calls to {@link #clearStatus} 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. If there are no other items in * the toolbar using an empty string (`''`) for this value would end up in the toolbar * height collapsing since the empty string will not maintain the toolbar height. * Use `''` 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() { var right = this.statusAlign === 'right'; this.callParent(arguments); this.currIconCls = this.iconCls || this.defaultIconCls; this.statusEl = Ext.create('Ext.toolbar.TextItem', { cls: 'x-status-text ' + (this.currIconCls || ''), text: this.text || this.defaultText || '' }); if (right) { this.cls += ' x-status-right'; this.add('->'); this.add(this.statusEl); } else { this.insert(0, this.statusEl); this.insert(1, '->'); } }, /** * Sets the status {@link #text} and/or {@link #iconCls}. Also supports automatically clearing the * status that was set after a specified interval. * * Example usage: * * // 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 * } * }); * * @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: * * @param {String} config.text The status text to display. If not specified, any current * status text will remain unchanged. * * @param {String} config.iconCls The CSS class used to customize the status icon (see * {@link #iconCls} for details). If not specified, any current iconCls will remain unchanged. * * @param {Boolean/Number/Object} config.clear 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 `true` 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: * * @param {Number} config.clear.wait The number of milliseconds to wait before clearing * (defaults to {@link #autoClear}). * @param {Boolean} config.clear.anim False to clear the status immediately once the callback * executes (defaults to true which fades the status out). * @param {Boolean} config.clear.useDefaults False to completely clear the status text and iconCls * (defaults to true which uses {@link #defaultText} and {@link #defaultIconCls}). * * @return {Ext.ux.statusbar.StatusBar} this */ setStatus: function(o) { var me = this; o = o || {}; Ext.suspendLayouts(); if (Ext.isString(o)) { o = { text: o }; } if (o.text !== undefined) { me.setText(o.text); } if (o.iconCls !== undefined) { me.setIcon(o.iconCls); } if (o.clear) { var c = o.clear, wait = me.autoClear, defaults = { useDefaults: true, anim: true }; if (Ext.isObject(c)) { c = Ext.applyIf(c, defaults); if (c.wait) { wait = c.wait; } } else if (Ext.isNumber(c)) { wait = c; c = defaults; } else if (Ext.isBoolean(c)) { c = defaults; } c.threadId = this.activeThreadId; Ext.defer(me.clearStatus, wait, me, [ c ]); } Ext.resumeLayouts(true); return me; }, /** * Clears the status {@link #text} and {@link #iconCls}. Also supports clearing via an optional fade out animation. * * @param {Object} [config] 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: * @param {Boolean} config.anim True to clear the status by fading out the status element (defaults * to false which clears immediately). * @param {Boolean} config.useDefaults 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). * * @return {Ext.ux.statusbar.StatusBar} this */ clearStatus: function(o) { o = o || {}; var me = this, statusEl = me.statusEl; if (o.threadId && o.threadId !== me.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 me; } var text = o.useDefaults ? me.defaultText : me.emptyText, iconCls = o.useDefaults ? (me.defaultIconCls ? me.defaultIconCls : '') : ''; if (o.anim) { // animate the statusEl Ext.Element statusEl.el.puff({ remove: false, useDisplay: true, callback: function() { statusEl.el.show(); me.setStatus({ text: text, iconCls: iconCls }); } }); } else { me.setStatus({ text: text, iconCls: iconCls }); } return me; }, /** * 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.StatusBar} this */ setText: function(text) { var me = this; me.activeThreadId++; me.text = text || ''; if (me.rendered) { me.statusEl.setText(me.text); } return me; }, /** * 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.StatusBar} this */ setIcon: function(cls) { var me = this; me.activeThreadId++; cls = cls || ''; if (me.rendered) { if (me.currIconCls) { me.statusEl.removeCls(me.currIconCls); me.currIconCls = null; } if (cls.length > 0) { me.statusEl.addCls(cls); me.currIconCls = cls; } } else { me.currIconCls = cls; } return me; }, /** * 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 * `text` and/or `iconCls` 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.StatusBar} this */ showBusy: function(o) { if (Ext.isString(o)) { o = { text: o }; } o = Ext.applyIf(o || {}, { text: this.busyText, iconCls: this.busyIconCls }); return this.setStatus(o); } }); /** * A GridPanel class with live search support. * @author Nicolas Ferrero */ Ext.define('Ext.ux.LiveSearchGridPanel', { extend: 'Ext.grid.Panel', requires: [ 'Ext.toolbar.TextItem', 'Ext.form.field.Checkbox', 'Ext.form.field.Text', 'Ext.ux.statusbar.StatusBar' ], /** * @private * search value initialization */ searchValue: null, /** * @private * The matched positions from the most recent search */ matches: [], /** * @private * The current index matched. */ currentIndex: null, /** * @private * The generated regular expression used for searching. */ searchRegExp: null, /** * @private * Case sensitive mode. */ caseSensitive: false, /** * @private * Regular expression mode. */ regExpMode: false, /** * @cfg {String} matchCls * The matched string css classe. */ matchCls: 'x-livesearch-match', defaultStatusText: 'Nothing Found', // Component initialization override: adds the top and bottom toolbars and setup headers renderer. initComponent: function() { var me = this; me.tbar = [ 'Search', { xtype: 'textfield', name: 'searchField', hideLabel: true, width: 200, listeners: { change: { fn: me.onTextFieldChange, scope: this, buffer: 500 } } }, { xtype: 'button', text: '<', tooltip: 'Find Previous Row', handler: me.onPreviousClick, scope: me }, { xtype: 'button', text: '>', tooltip: 'Find Next Row', handler: me.onNextClick, scope: me }, '-', { xtype: 'checkbox', hideLabel: true, margin: '0 0 0 4px', handler: me.regExpToggle, scope: me }, 'Regular expression', { xtype: 'checkbox', hideLabel: true, margin: '0 0 0 4px', handler: me.caseSensitiveToggle, scope: me }, 'Case sensitive' ]; me.bbar = new Ext.ux.StatusBar({ defaultText: me.defaultStatusText, name: 'searchStatusBar' }); me.callParent(arguments); }, // afterRender override: it adds textfield and statusbar reference and start monitoring keydown events in textfield input afterRender: function() { var me = this; me.callParent(arguments); me.textField = me.down('textfield[name=searchField]'); me.statusBar = me.down('statusbar[name=searchStatusBar]'); me.view.on('cellkeydown', me.focusTextField, me); }, focusTextField: function(view, td, cellIndex, record, tr, rowIndex, e, eOpts) { if (e.getKey() === e.S) { e.preventDefault(); this.textField.focus(); } }, // detects html tag tagsRe: /<[^>]*>/gm, // DEL ASCII code tagsProtect: '\x0f', /** * In normal mode it returns the value with protected regexp characters. * In regular expression mode it returns the raw value except if the regexp is invalid. * @return {String} The value to process or null if the textfield value is blank or invalid. * @private */ getSearchValue: function() { var me = this, value = me.textField.getValue(); if (value === '') { return null; } if (!me.regExpMode) { value = Ext.String.escapeRegex(value); } else { try { new RegExp(value); } catch (error) { me.statusBar.setStatus({ text: error.message, iconCls: 'x-status-error' }); return null; } // this is stupid if (value === '^' || value === '$') { return null; } } return value; }, /** * Finds all strings that matches the searched value in each grid cells. * @private */ onTextFieldChange: function() { var me = this, count = 0, view = me.view, cellSelector = view.cellSelector, innerSelector = view.innerSelector, columns = me.visibleColumnManager.getColumns(); view.refresh(); // reset the statusbar me.statusBar.setStatus({ text: me.defaultStatusText, iconCls: '' }); me.searchValue = me.getSearchValue(); me.matches = []; me.currentIndex = null; if (me.searchValue !== null) { me.searchRegExp = new RegExp(me.getSearchValue(), 'g' + (me.caseSensitive ? '' : 'i')); me.store.each(function(record, idx) { var node = view.getNode(record); if (node) { Ext.Array.forEach(columns, function(column) { var cell = Ext.fly(node).down(column.getCellInnerSelector(), true), matches, cellHTML, seen; if (cell) { matches = cell.innerHTML.match(me.tagsRe); cellHTML = cell.innerHTML.replace(me.tagsRe, me.tagsProtect); // populate indexes array, set currentIndex, and replace wrap matched string in a span cellHTML = cellHTML.replace(me.searchRegExp, function(m) { ++count; if (!seen) { me.matches.push({ record: record, column: column }); seen = true; } return '' + m + ''; }, me); // restore protected tags Ext.each(matches, function(match) { cellHTML = cellHTML.replace(me.tagsProtect, match); }); // update cell html cell.innerHTML = cellHTML; } }); } }, me); // results found if (count) { me.currentIndex = 0; me.gotoCurrent(); me.statusBar.setStatus({ text: Ext.String.format('{0} match{1} found.', count, count === 1 ? 'es' : ''), iconCls: 'x-status-valid' }); } } // no results found if (me.currentIndex === null) { me.getSelectionModel().deselectAll(); me.textField.focus(); } }, /** * Selects the previous row containing a match. * @private */ onPreviousClick: function() { var me = this, matches = me.matches, len = matches.length, idx = me.currentIndex; if (len) { me.currentIndex = idx === 0 ? len - 1 : idx - 1; me.gotoCurrent(); } }, /** * Selects the next row containing a match. * @private */ onNextClick: function() { var me = this, matches = me.matches, len = matches.length, idx = me.currentIndex; if (len) { me.currentIndex = idx === len - 1 ? 0 : idx + 1; me.gotoCurrent(); } }, /** * Switch to case sensitive mode. * @private */ caseSensitiveToggle: function(checkbox, checked) { this.caseSensitive = checked; this.onTextFieldChange(); }, /** * Switch to regular expression mode * @private */ regExpToggle: function(checkbox, checked) { this.regExpMode = checked; this.onTextFieldChange(); }, privates: { gotoCurrent: function() { var pos = this.matches[this.currentIndex]; this.getNavigationModel().setPosition(pos.record, pos.column); this.getSelectionModel().select(pos.record); } } }); /** * The Preview Plugin enables toggle of a configurable preview of all visible records. * * Note: This plugin does NOT assert itself against an existing RowBody feature and may conflict with * another instance of the same plugin. */ Ext.define('Ext.ux.PreviewPlugin', { extend: 'Ext.plugin.Abstract', alias: 'plugin.preview', requires: [ 'Ext.grid.feature.RowBody' ], // private, css class to use to hide the body hideBodyCls: 'x-grid-row-body-hidden', /** * @cfg {String} bodyField * Field to display in the preview. Must be a field within the Model definition * that the store is using. */ bodyField: '', /** * @cfg {Boolean} previewExpanded */ previewExpanded: true, /** * Plugin may be safely declared on either a panel.Grid or a Grid View/viewConfig * @param {Ext.grid.Panel/Ext.view.View} target */ setCmp: function(target) { this.callParent(arguments); // Resolve grid from view as necessary var me = this, grid = me.cmp = target.isXType('gridview') ? target.grid : target, bodyField = me.bodyField, hideBodyCls = me.hideBodyCls, feature = Ext.create('Ext.grid.feature.RowBody', { grid: grid, getAdditionalData: function(data, idx, model, rowValues) { var getAdditionalData = Ext.grid.feature.RowBody.prototype.getAdditionalData, additionalData = { rowBody: data[bodyField], rowBodyCls: grid.getView().previewExpanded ? '' : hideBodyCls }; if (Ext.isFunction(getAdditionalData)) { // "this" is the RowBody object hjere. Do not change to "me" Ext.apply(additionalData, getAdditionalData.apply(this, arguments)); } return additionalData; } }), initFeature = function(grid, view) { view.previewExpanded = me.previewExpanded; // By this point, existing features are already in place, so this must be initialized and added view.featuresMC.add(feature); feature.init(grid); }; // The grid has already created its view if (grid.view) { initFeature(grid, grid.view); } else // At the time a grid creates its plugins, it has not created all the things // it needs to create its view correctly. // Process the view and init the RowBody Feature as soon as the view is created. { grid.on({ viewcreated: initFeature, single: true }); } }, /** * Toggle between the preview being expanded/hidden on all rows * @param {Boolean} expanded Pass true to expand the record and false to not show the preview. */ toggleExpanded: function(expanded) { var grid = this.getCmp(), view = grid && grid.getView(), bufferedRenderer = view.bufferedRenderer, scrollManager = view.scrollManager; if (grid && view && expanded !== view.previewExpanded) { this.previewExpanded = view.previewExpanded = !!expanded; view.refreshView(); // If we are using the touch scroller, ensure that the scroller knows about // the correct scrollable range if (scrollManager) { if (bufferedRenderer) { bufferedRenderer.stretchView(view, bufferedRenderer.getScrollHeight(true)); } else { scrollManager.refresh(true); } } } } }); /** * Plugin for displaying a progressbar inside of a paging toolbar * instead of plain text. */ Ext.define('Ext.ux.ProgressBarPager', { requires: [ 'Ext.ProgressBar' ], /** * @cfg {Number} width *

The default progress bar width. Default is 225.

*/ width: 225, /** * @cfg {String} defaultText *

The text to display while the store is loading. Default is 'Loading...'

*/ defaultText: 'Loading...', /** * @cfg {Object} defaultAnimCfg *

A {@link Ext.fx.Anim Ext.fx.Anim} configuration object.

*/ defaultAnimCfg: { duration: 1000, easing: 'bounceOut' }, /** * Creates new ProgressBarPager. * @param {Object} config Configuration options */ constructor: function(config) { if (config) { Ext.apply(this, config); } }, //public init: function(parent) { var displayItem; if (parent.displayInfo) { this.parent = parent; displayItem = parent.child("#displayItem"); if (displayItem) { parent.remove(displayItem, true); } this.progressBar = Ext.create('Ext.ProgressBar', { text: this.defaultText, width: this.width, animate: this.defaultAnimCfg, style: { cursor: 'pointer' }, listeners: { el: { scope: this, click: this.handleProgressBarClick } } }); parent.displayItem = this.progressBar; parent.add(parent.displayItem); Ext.apply(parent, this.parentOverrides); } }, // private // This method handles the click for the progress bar handleProgressBarClick: function(e) { var parent = this.parent, displayItem = parent.displayItem, box = this.progressBar.getBox(), xy = e.getXY(), position = xy[0] - box.x, pages = Math.ceil(parent.store.getTotalCount() / parent.pageSize), newPage = Math.max(Math.ceil(position / (displayItem.width / pages)), 1); parent.store.loadPage(newPage); }, // private, overriddes parentOverrides: { // private // This method updates the information via the progress bar. updateInfo: function() { if (this.displayItem) { var count = this.store.getCount(), pageData = this.getPageData(), message = count === 0 ? this.emptyMsg : Ext.String.format(this.displayMsg, pageData.fromRecord, pageData.toRecord, this.store.getTotalCount()), percentage = pageData.pageCount > 0 ? (pageData.currentPage / pageData.pageCount) : 0; this.displayItem.updateProgress(percentage, message, this.animate || this.defaultAnimConfig); } } } }); /** * @deprecated * Ext.ux.RowExpander has been promoted to the core framework. Use * {@link Ext.grid.plugin.RowExpander} instead. Ext.ux.RowExpander is now just an empty * stub that extends Ext.grid.plugin.RowExpander for backward compatibility reasons. */ Ext.define('Ext.ux.RowExpander', { extend: 'Ext.grid.plugin.RowExpander' }); /** * Plugin for PagingToolbar which replaces the textfield input with a slider */ Ext.define('Ext.ux.SlidingPager', { requires: [ 'Ext.slider.Single', 'Ext.slider.Tip' ], /** * Creates new SlidingPager. * @param {Object} config Configuration options */ constructor: function(config) { if (config) { Ext.apply(this, config); } }, init: function(pbar) { var idx = pbar.items.indexOf(pbar.child("#inputItem")), slider; Ext.each(pbar.items.getRange(idx - 2, idx + 2), function(c) { c.hide(); }); slider = Ext.create('Ext.slider.Single', { width: 114, minValue: 1, maxValue: 1, hideLabel: true, tipText: function(thumb) { return Ext.String.format('Page {0} of {1}', thumb.value, thumb.slider.maxValue); }, listeners: { changecomplete: function(s, v) { pbar.store.loadPage(v); } } }); pbar.insert(idx + 1, slider); pbar.on({ change: function(pb, data) { slider.setMaxValue(data.pageCount); slider.setValue(data.currentPage); } }); } }); /** * UX used to provide a spotlight around a specified component/element. */ Ext.define('Ext.ux.Spotlight', { /** * @private * The baseCls for the spotlight elements */ baseCls: 'x-spotlight', /** * @cfg animate {Boolean} True to animate the spotlight change * (defaults to true) */ animate: true, /** * @cfg duration {Integer} The duration of the animation, in milliseconds * (defaults to 250) */ duration: 250, /** * @cfg easing {String} The type of easing for the spotlight animatation * (defaults to null) */ easing: null, /** * @private * True if the spotlight is active on the element */ active: false, constructor: function(config) { Ext.apply(this, config); }, /** * Create all the elements for the spotlight */ createElements: function() { var me = this, baseCls = me.baseCls, body = Ext.getBody(); me.right = body.createChild({ cls: baseCls }); me.left = body.createChild({ cls: baseCls }); me.top = body.createChild({ cls: baseCls }); me.bottom = body.createChild({ cls: baseCls }); me.all = Ext.create('Ext.CompositeElement', [ me.right, me.left, me.top, me.bottom ]); }, /** * Show the spotlight */ show: function(el, callback, scope) { var me = this; //get the target element me.el = Ext.get(el); //create the elements if they don't already exist if (!me.right) { me.createElements(); } if (!me.active) { //if the spotlight is not active, show it me.all.setDisplayed(''); me.active = true; Ext.on('resize', me.syncSize, me); me.applyBounds(me.animate, false); } else { //if the spotlight is currently active, just move it me.applyBounds(false, false); } }, /** * Hide the spotlight */ hide: function(callback, scope) { var me = this; Ext.un('resize', me.syncSize, me); me.applyBounds(me.animate, true); }, /** * Resizes the spotlight with the window size. */ syncSize: function() { this.applyBounds(false, false); }, /** * Resizes the spotlight depending on the arguments * @param {Boolean} animate True to animate the changing of the bounds * @param {Boolean} reverse True to reverse the animation */ applyBounds: function(animate, reverse) { var me = this, box = me.el.getBox(), //get the current view width and height viewWidth = Ext.Element.getViewportWidth(), viewHeight = Ext.Element.getViewportHeight(), i = 0, config = false, from, to, clone; //where the element should start (if animation) from = { right: { x: box.right, y: viewHeight, width: (viewWidth - box.right), height: 0 }, left: { x: 0, y: 0, width: box.x, height: 0 }, top: { x: viewWidth, y: 0, width: 0, height: box.y }, bottom: { x: 0, y: (box.y + box.height), width: 0, height: (viewHeight - (box.y + box.height)) + 'px' } }; //where the element needs to finish to = { right: { x: box.right, y: box.y, width: (viewWidth - box.right) + 'px', height: (viewHeight - box.y) + 'px' }, left: { x: 0, y: 0, width: box.x + 'px', height: (box.y + box.height) + 'px' }, top: { x: box.x, y: 0, width: (viewWidth - box.x) + 'px', height: box.y + 'px' }, bottom: { x: 0, y: (box.y + box.height), width: (box.x + box.width) + 'px', height: (viewHeight - (box.y + box.height)) + 'px' } }; //reverse the objects if (reverse) { clone = Ext.clone(from); from = to; to = clone; } if (animate) { Ext.Array.forEach([ 'right', 'left', 'top', 'bottom' ], function(side) { me[side].setBox(from[side]); me[side].animate({ duration: me.duration, easing: me.easing, to: to[side] }); }, this); } else { Ext.Array.forEach([ 'right', 'left', 'top', 'bottom' ], function(side) { me[side].setBox(Ext.apply(from[side], to[side])); me[side].repaint(); }, this); } }, /** * Removes all the elements for the spotlight */ destroy: function() { var me = this; Ext.destroy(me.right, me.left, me.top, me.bottom); delete me.el; delete me.all; } }); /** * Plugin for adding a close context menu to tabs. Note that the menu respects * the closable configuration on the tab. As such, commands like remove others * and remove all will not remove items that are not closable. */ Ext.define('Ext.ux.TabCloseMenu', { extend: 'Ext.plugin.Abstract', alias: 'plugin.tabclosemenu', mixins: { observable: 'Ext.util.Observable' }, /** * @cfg {String} closeTabText * The text for closing the current tab. */ closeTabText: 'Close Tab', /** * @cfg {Boolean} showCloseOthers * Indicates whether to show the 'Close Others' option. */ showCloseOthers: true, /** * @cfg {String} closeOthersTabsText * The text for closing all tabs except the current one. */ closeOthersTabsText: 'Close Other Tabs', /** * @cfg {Boolean} showCloseAll * Indicates whether to show the 'Close All' option. */ showCloseAll: true, /** * @cfg {String} closeAllTabsText * The text for closing all tabs. */ closeAllTabsText: 'Close All Tabs', /** * @cfg {Array} extraItemsHead * An array of additional context menu items to add to the front of the context menu. */ extraItemsHead: null, /** * @cfg {Array} extraItemsTail * An array of additional context menu items to add to the end of the context menu. */ extraItemsTail: null, //public constructor: function(config) { this.callParent([ config ]); this.mixins.observable.constructor.call(this, config); }, init: function(tabpanel) { this.tabPanel = tabpanel; this.tabBar = tabpanel.down("tabbar"); this.mon(this.tabPanel, { scope: this, afterlayout: this.onAfterLayout, single: true }); }, onAfterLayout: function() { this.mon(this.tabBar.el, { scope: this, contextmenu: this.onContextMenu, delegate: '.x-tab' }); }, destroy: function() { this.callParent(); Ext.destroy(this.menu); }, // private onContextMenu: function(event, target) { var me = this, menu = me.createMenu(), disableAll = true, disableOthers = true, tab = me.tabBar.getChildByElement(target), index = me.tabBar.items.indexOf(tab); me.item = me.tabPanel.getComponent(index); menu.child('#close').setDisabled(!me.item.closable); if (me.showCloseAll || me.showCloseOthers) { me.tabPanel.items.each(function(item) { if (item.closable) { disableAll = false; if (item !== me.item) { disableOthers = false; return false; } } return true; }); if (me.showCloseAll) { menu.child('#closeAll').setDisabled(disableAll); } if (me.showCloseOthers) { menu.child('#closeOthers').setDisabled(disableOthers); } } event.preventDefault(); me.fireEvent('beforemenu', menu, me.item, me); menu.showAt(event.getXY()); }, createMenu: function() { var me = this; if (!me.menu) { var items = [ { itemId: 'close', text: me.closeTabText, scope: me, handler: me.onClose } ]; if (me.showCloseAll || me.showCloseOthers) { items.push('-'); } if (me.showCloseOthers) { items.push({ itemId: 'closeOthers', text: me.closeOthersTabsText, scope: me, handler: me.onCloseOthers }); } if (me.showCloseAll) { items.push({ itemId: 'closeAll', text: me.closeAllTabsText, scope: me, handler: me.onCloseAll }); } if (me.extraItemsHead) { items = me.extraItemsHead.concat(items); } if (me.extraItemsTail) { items = items.concat(me.extraItemsTail); } me.menu = Ext.create('Ext.menu.Menu', { items: items, listeners: { hide: me.onHideMenu, scope: me } }); } return me.menu; }, onHideMenu: function() { var me = this; me.fireEvent('aftermenu', me.menu, me); }, onClose: function() { this.tabPanel.remove(this.item); }, onCloseOthers: function() { this.doClose(true); }, onCloseAll: function() { this.doClose(false); }, doClose: function(excludeActive) { var items = []; this.tabPanel.items.each(function(item) { if (item.closable) { if (!excludeActive || item !== this.item) { items.push(item); } } }, this); Ext.suspendLayouts(); Ext.Array.forEach(items, function(item) { this.tabPanel.remove(item); }, this); Ext.resumeLayouts(true); } }); /** * This plugin allow you to reorder tabs of a TabPanel. */ Ext.define('Ext.ux.TabReorderer', { extend: 'Ext.ux.BoxReorderer', alias: 'plugin.tabreorderer', itemSelector: '.' + Ext.baseCSSPrefix + 'tab', init: function(tabPanel) { var me = this; me.callParent([ tabPanel.getTabBar() ]); // Ensure reorderable property is copied into dynamically added tabs tabPanel.onAdd = Ext.Function.createSequence(tabPanel.onAdd, me.onAdd); }, onBoxReady: function() { var tabs, len, i = 0, tab; this.callParent(arguments); // Copy reorderable property from card into tab for (tabs = this.container.items.items , len = tabs.length; i < len; i++) { tab = tabs[i]; if (tab.card) { tab.reorderable = tab.card.reorderable; } } }, onAdd: function(card, index) { card.tab.reorderable = card.reorderable; }, afterBoxReflow: function() { var me = this; // Cannot use callParent, this is not called in the scope of this plugin, but that of its Ext.dd.DD object Ext.ux.BoxReorderer.prototype.afterBoxReflow.apply(me, arguments); // Move the associated card to match the tab order if (me.dragCmp) { me.container.tabPanel.setActiveTab(me.dragCmp.card); me.container.tabPanel.move(me.dragCmp.card, me.curIndex); } } }); Ext.ns('Ext.ux'); /** * Plugin for adding a tab menu to a TabBar is the Tabs overflow. */ Ext.define('Ext.ux.TabScrollerMenu', { alias: 'plugin.tabscrollermenu', requires: [ 'Ext.menu.Menu' ], /** * @cfg {Number} pageSize How many items to allow per submenu. */ pageSize: 10, /** * @cfg {Number} maxText How long should the title of each {@link Ext.menu.Item} be. */ maxText: 15, /** * @cfg {String} menuPrefixText Text to prefix the submenus. */ menuPrefixText: 'Items', /** * Creates new TabScrollerMenu. * @param {Object} config Configuration options */ constructor: function(config) { Ext.apply(this, config); }, //private init: function(tabPanel) { var me = this; me.tabPanel = tabPanel; tabPanel.on({ render: function() { me.tabBar = tabPanel.tabBar; me.layout = me.tabBar.layout; me.layout.overflowHandler.handleOverflow = Ext.Function.bind(me.showButton, me); me.layout.overflowHandler.clearOverflow = Ext.Function.createSequence(me.layout.overflowHandler.clearOverflow, me.hideButton, me); }, destroy: me.destroy, scope: me, single: true }); }, showButton: function() { var me = this, result = Ext.getClass(me.layout.overflowHandler).prototype.handleOverflow.apply(me.layout.overflowHandler, arguments), button = me.menuButton; if (me.tabPanel.items.getCount() > 1) { if (!button) { button = me.menuButton = me.tabBar.body.createChild({ cls: Ext.baseCSSPrefix + 'tab-tabmenu-right' }, me.tabBar.body.child('.' + Ext.baseCSSPrefix + 'box-scroller-right')); button.addClsOnOver(Ext.baseCSSPrefix + 'tab-tabmenu-over'); button.on('click', me.showTabsMenu, me); } button.setVisibilityMode(Ext.dom.Element.DISPLAY); button.show(); result.reservedSpace += button.getWidth(); } else { me.hideButton(); } return result; }, hideButton: function() { var me = this; if (me.menuButton) { me.menuButton.hide(); } }, /** * Returns an the current page size (this.pageSize); * @return {Number} this.pageSize The current page size. */ getPageSize: function() { return this.pageSize; }, /** * Sets the number of menu items per submenu "page size". * @param {Number} pageSize The page size */ setPageSize: function(pageSize) { this.pageSize = pageSize; }, /** * Returns the current maxText length; * @return {Number} this.maxText The current max text length. */ getMaxText: function() { return this.maxText; }, /** * Sets the maximum text size for each menu item. * @param {Number} t The max text per each menu item. */ setMaxText: function(t) { this.maxText = t; }, /** * Returns the current menu prefix text String.; * @return {String} this.menuPrefixText The current menu prefix text. */ getMenuPrefixText: function() { return this.menuPrefixText; }, /** * Sets the menu prefix text String. * @param {String} t The menu prefix text. */ setMenuPrefixText: function(t) { this.menuPrefixText = t; }, showTabsMenu: function(e) { var me = this; if (me.tabsMenu) { me.tabsMenu.removeAll(); } else { me.tabsMenu = new Ext.menu.Menu(); } me.generateTabMenuItems(); var target = Ext.get(e.getTarget()), xy = target.getXY(); //Y param + 24 pixels xy[1] += 24; me.tabsMenu.showAt(xy); }, // private generateTabMenuItems: function() { var me = this, tabPanel = me.tabPanel, curActive = tabPanel.getActiveTab(), allItems = tabPanel.items.getRange(), pageSize = me.getPageSize(), tabsMenu = me.tabsMenu, totalItems, numSubMenus, remainder, i, curPage, menuItems, x, item, start, index; tabsMenu.suspendLayouts(); allItems = Ext.Array.filter(allItems, function(item) { if (item.id == curActive.id) { return false; } return item.hidden ? !!item.hiddenByLayout : true; }); totalItems = allItems.length; numSubMenus = Math.floor(totalItems / pageSize); remainder = totalItems % pageSize; if (totalItems > pageSize) { // Loop through all of the items and create submenus in chunks of 10 for (i = 0; i < numSubMenus; i++) { curPage = (i + 1) * pageSize; menuItems = []; for (x = 0; x < pageSize; x++) { index = x + curPage - pageSize; item = allItems[index]; menuItems.push(me.autoGenMenuItem(item)); } tabsMenu.add({ text: me.getMenuPrefixText() + ' ' + (curPage - pageSize + 1) + ' - ' + curPage, menu: menuItems }); } // remaining items if (remainder > 0) { start = numSubMenus * pageSize; menuItems = []; for (i = start; i < totalItems; i++) { item = allItems[i]; menuItems.push(me.autoGenMenuItem(item)); } me.tabsMenu.add({ text: me.menuPrefixText + ' ' + (start + 1) + ' - ' + (start + menuItems.length), menu: menuItems }); } } else { for (i = 0; i < totalItems; ++i) { tabsMenu.add(me.autoGenMenuItem(allItems[i])); } } tabsMenu.resumeLayouts(true); }, // private autoGenMenuItem: function(item) { var maxText = this.getMaxText(), text = Ext.util.Format.ellipsis(item.title, maxText); return { text: text, handler: this.showTabFromMenu, scope: this, disabled: item.disabled, tabToShow: item, iconCls: item.iconCls }; }, // private showTabFromMenu: function(menuItem) { this.tabPanel.setActiveTab(menuItem.tabToShow); }, destroy: function() { Ext.destroy(this.tabsMenu, this.menuButton); } }); /** * Plugin which allows items to be dropped onto a toolbar and be turned into new Toolbar items. * To use the plugin, you just need to provide a createItem implementation that takes the drop * data as an argument and returns an object that can be placed onto the toolbar. Example: *
 * Ext.create('Ext.ux.ToolbarDroppable', {
 *   createItem: function(data) {
 *     return Ext.create('Ext.Button', {text: data.text});
 *   }
 * });
 * 
* The afterLayout function can also be overridden, and is called after a new item has been * created and inserted into the Toolbar. Use this for any logic that needs to be run after * the item has been created. */ Ext.define('Ext.ux.ToolbarDroppable', { /** * Creates new ToolbarDroppable. * @param {Object} config Config options. */ constructor: function(config) { Ext.apply(this, config); }, /** * Initializes the plugin and saves a reference to the toolbar * @param {Ext.toolbar.Toolbar} toolbar The toolbar instance */ init: function(toolbar) { /** * @property toolbar * @type Ext.toolbar.Toolbar * The toolbar instance that this plugin is tied to */ this.toolbar = toolbar; this.toolbar.on({ scope: this, render: this.createDropTarget }); }, /** * Creates a drop target on the toolbar */ createDropTarget: function() { /** * @property dropTarget * @type Ext.dd.DropTarget * The drop target attached to the toolbar instance */ this.dropTarget = Ext.create('Ext.dd.DropTarget', this.toolbar.getEl(), { notifyOver: Ext.Function.bind(this.notifyOver, this), notifyDrop: Ext.Function.bind(this.notifyDrop, this) }); }, /** * Adds the given DD Group to the drop target * @param {String} ddGroup The DD Group */ addDDGroup: function(ddGroup) { this.dropTarget.addToGroup(ddGroup); }, /** * Calculates the location on the toolbar to create the new sorter button based on the XY of the * drag event * @param {Ext.event.Event} e The event object * @return {Number} The index at which to insert the new button */ calculateEntryIndex: function(e) { var entryIndex = 0, toolbar = this.toolbar, items = toolbar.items.items, count = items.length, xHover = e.getXY()[0], index = 0, el, xTotal, width, midpoint; for (; index < count; index++) { el = items[index].getEl(); xTotal = el.getXY()[0]; width = el.getWidth(); midpoint = xTotal + width / 2; if (xHover < midpoint) { entryIndex = index; break; } else { entryIndex = index + 1; } } return entryIndex; }, /** * Returns true if the drop is allowed on the drop target. This function can be overridden * and defaults to simply return true * @param {Object} data Arbitrary data from the drag source * @return {Boolean} True if the drop is allowed */ canDrop: function(data) { return true; }, /** * Custom notifyOver method which will be used in the plugin's internal DropTarget * @return {String} The CSS class to add */ notifyOver: function(dragSource, event, data) { return this.canDrop.apply(this, arguments) ? this.dropTarget.dropAllowed : this.dropTarget.dropNotAllowed; }, /** * Called when the drop has been made. Creates the new toolbar item, places it at the correct location * and calls the afterLayout callback. */ notifyDrop: function(dragSource, event, data) { var canAdd = this.canDrop(dragSource, event, data), tbar = this.toolbar; if (canAdd) { var entryIndex = this.calculateEntryIndex(event); tbar.insert(entryIndex, this.createItem(data)); tbar.doLayout(); this.afterLayout(); } return canAdd; }, /** * Creates the new toolbar item based on drop data. This method must be implemented by the plugin instance * @param {Object} data Arbitrary data from the drop * @return {Mixed} An item that can be added to a toolbar */ createItem: function(data) { Ext.Error.raise("The createItem method must be implemented in the ToolbarDroppable plugin"); }, /** * Called after a new button has been created and added to the toolbar. Add any required cleanup logic here */ afterLayout: Ext.emptyFn }); /** * A Picker field that contains a tree panel on its popup, enabling selection of tree nodes. */ Ext.define('Ext.ux.TreePicker', { extend: 'Ext.form.field.Picker', xtype: 'treepicker', uses: [ 'Ext.tree.Panel' ], triggerCls: Ext.baseCSSPrefix + 'form-arrow-trigger', config: { /** * @cfg {Ext.data.TreeStore} store * A tree store that the tree picker will be bound to */ store: null, /** * @cfg {String} displayField * The field inside the model that will be used as the node's text. * Defaults to the default value of {@link Ext.tree.Panel}'s `displayField` configuration. */ displayField: null, /** * @cfg {Array} columns * An optional array of columns for multi-column trees */ columns: null, /** * @cfg {Boolean} selectOnTab * Whether the Tab key should select the currently highlighted item. Defaults to `true`. */ selectOnTab: true, /** * @cfg {Number} maxPickerHeight * The maximum height of the tree dropdown. Defaults to 300. */ maxPickerHeight: 300, /** * @cfg {Number} minPickerHeight * The minimum height of the tree dropdown. Defaults to 100. */ minPickerHeight: 100 }, editable: false, /** * @event select * Fires when a tree node is selected * @param {Ext.ux.TreePicker} picker This tree picker * @param {Ext.data.Model} record The selected record */ initComponent: function() { var me = this; me.callParent(arguments); me.mon(me.store, { scope: me, load: me.onLoad, update: me.onUpdate }); }, /** * Creates and returns the tree panel to be used as this field's picker. */ createPicker: function() { var me = this, picker = new Ext.tree.Panel({ shrinkWrapDock: 2, store: me.store, floating: true, displayField: me.displayField, columns: me.columns, minHeight: me.minPickerHeight, maxHeight: me.maxPickerHeight, manageHeight: false, shadow: false, listeners: { scope: me, itemclick: me.onItemClick }, viewConfig: { listeners: { scope: me, render: me.onViewRender } } }), view = picker.getView(); if (Ext.isIE9 && Ext.isStrict) { // In IE9 strict mode, the tree view grows by the height of the horizontal scroll bar when the items are highlighted or unhighlighted. // Also when items are collapsed or expanded the height of the view is off. Forcing a repaint fixes the problem. view.on({ scope: me, highlightitem: me.repaintPickerView, unhighlightitem: me.repaintPickerView, afteritemexpand: me.repaintPickerView, afteritemcollapse: me.repaintPickerView }); } return picker; }, onViewRender: function(view) { view.getEl().on('keypress', this.onPickerKeypress, this); }, /** * repaints the tree view */ repaintPickerView: function() { var style = this.picker.getView().getEl().dom.style; // can't use Element.repaint because it contains a setTimeout, which results in a flicker effect style.display = style.display; }, /** * Handles a click even on a tree node * @private * @param {Ext.tree.View} view * @param {Ext.data.Model} record * @param {HTMLElement} node * @param {Number} rowIndex * @param {Ext.event.Event} e */ onItemClick: function(view, record, node, rowIndex, e) { this.selectItem(record); }, /** * Handles a keypress event on the picker element * @private * @param {Ext.event.Event} e * @param {HTMLElement} el */ onPickerKeypress: function(e, el) { var key = e.getKey(); if (key === e.ENTER || (key === e.TAB && this.selectOnTab)) { this.selectItem(this.picker.getSelectionModel().getSelection()[0]); } }, /** * Changes the selection to a given record and closes the picker * @private * @param {Ext.data.Model} record */ selectItem: function(record) { var me = this; me.setValue(record.getId()); me.fireEvent('select', me, record); me.collapse(); }, /** * Runs when the picker is expanded. Selects the appropriate tree node based on the value of the input element, * and focuses the picker so that keyboard navigation will work. * @private */ onExpand: function() { var me = this, picker = me.picker, store = picker.store, value = me.value, node; if (value) { node = store.getNodeById(value); } if (!node) { node = store.getRoot(); } picker.selectPath(node.getPath()); }, /** * Sets the specified value into the field * @param {Mixed} value * @return {Ext.ux.TreePicker} this */ setValue: function(value) { var me = this, record; me.value = value; if (me.store.loading) { // Called while the Store is loading. Ensure it is processed by the onLoad method. return me; } // try to find a record in the store that matches the value record = value ? me.store.getNodeById(value) : me.store.getRoot(); if (value === undefined) { record = me.store.getRoot(); me.value = record.getId(); } else { record = me.store.getNodeById(value); } // set the raw value to the record's display field if a record was found me.setRawValue(record ? record.get(me.displayField) : ''); return me; }, getSubmitValue: function() { return this.value; }, /** * Returns the current data value of the field (the idProperty of the record) * @return {Number} */ getValue: function() { return this.value; }, /** * Handles the store's load event. * @private */ onLoad: function() { var value = this.value; if (value) { this.setValue(value); } }, onUpdate: function(store, rec, type, modifiedFieldNames) { var display = this.displayField; if (type === 'edit' && modifiedFieldNames && Ext.Array.contains(modifiedFieldNames, display) && this.value === rec.getId()) { this.setRawValue(rec.get(display)); } } }); /** * @author Don Griffin * * This is a base class for more advanced "simlets" (simulated servers). A simlet is asked * to provide a response given a {@link Ext.ux.ajax.SimXhr} instance. */ Ext.define('Ext.ux.ajax.Simlet', function() { var urlRegex = /([^?#]*)(#.*)?$/, dateRegex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/, intRegex = /^[+-]?\d+$/, floatRegex = /^[+-]?\d+\.\d+$/; function parseParamValue(value) { var m; if (Ext.isDefined(value)) { value = decodeURIComponent(value); if (intRegex.test(value)) { value = parseInt(value, 10); } else if (floatRegex.test(value)) { value = parseFloat(value); } else if (!!(m = dateRegex.test(value))) { value = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6])); } } return value; } return { alias: 'simlet.basic', isSimlet: true, responseProps: [ 'responseText', 'responseXML', 'status', 'statusText' ], /** * @cfg {Number} responseText */ /** * @cfg {Number} responseXML */ /** * @cfg {Object} responseHeaders */ /** * @cfg {Number} status */ status: 200, /** * @cfg {String} statusText */ statusText: 'OK', constructor: function(config) { Ext.apply(this, config); }, doGet: function(ctx) { var me = this, ret = {}; Ext.Array.forEach(me.responseProps, function(prop) { if (prop in me) { ret[prop] = me[prop]; } }); return ret; }, doPost: function(ctx) { var me = this, ret = {}; Ext.Array.forEach(me.responseProps, function(prop) { if (prop in me) { ret[prop] = me[prop]; } }); return ret; }, doRedirect: function(ctx) { return false; }, /** * Performs the action requested by the given XHR and returns an object to be applied * on to the XHR (containing `status`, `responseText`, etc.). For the most part, * this is delegated to `doMethod` methods on this class, such as `doGet`. * * @param {Ext.ux.ajax.SimXhr} xhr The simulated XMLHttpRequest instance. * @returns {Object} The response properties to add to the XMLHttpRequest. */ exec: function(xhr) { var me = this, ret = {}, method = 'do' + Ext.String.capitalize(xhr.method.toLowerCase()), // doGet fn = me[method]; if (fn) { ret = fn.call(me, me.getCtx(xhr.method, xhr.url, xhr)); } else { ret = { status: 405, statusText: 'Method Not Allowed' }; } return ret; }, getCtx: function(method, url, xhr) { return { method: method, params: this.parseQueryString(url), url: url, xhr: xhr }; }, openRequest: function(method, url, options, async) { var ctx = this.getCtx(method, url), redirect = this.doRedirect(ctx), xhr; if (redirect) { xhr = redirect; } else { xhr = new Ext.ux.ajax.SimXhr({ mgr: this.manager, simlet: this, options: options }); xhr.open(method, url, async); } return xhr; }, parseQueryString: function(str) { var m = urlRegex.exec(str), ret = {}, key, value, i, n; if (m && m[1]) { var pair, parts = m[1].split('&'); for (i = 0 , n = parts.length; i < n; ++i) { if ((pair = parts[i].split('='))[0]) { key = decodeURIComponent(pair.shift()); value = parseParamValue((pair.length > 1) ? pair.join('=') : pair[0]); if (!(key in ret)) { ret[key] = value; } else if (Ext.isArray(ret[key])) { ret[key].push(value); } else { ret[key] = [ ret[key], value ]; } } } } return ret; }, redirect: function(method, url, params) { switch (arguments.length) { case 2: if (typeof url == 'string') { break; }; params = url; // fall... case 1: url = method; method = 'GET'; break; } if (params) { url = Ext.urlAppend(url, Ext.Object.toQueryString(params)); } return this.manager.openRequest(method, url); } }; }()); /** * This base class is used to handle data preparation (e.g., sorting, filtering and * group summary). */ Ext.define('Ext.ux.ajax.DataSimlet', function() { function makeSortFn(def, cmp) { var order = def.direction, sign = (order && order.toUpperCase() === 'DESC') ? -1 : 1; return function(leftRec, rightRec) { var lhs = leftRec[def.property], rhs = rightRec[def.property], c = (lhs < rhs) ? -1 : ((rhs < lhs) ? 1 : 0); if (c || !cmp) { return c * sign; } return cmp(leftRec, rightRec); }; } function makeSortFns(defs, cmp) { for (var sortFn = cmp, i = defs && defs.length; i; ) { sortFn = makeSortFn(defs[--i], sortFn); } return sortFn; } return { extend: 'Ext.ux.ajax.Simlet', buildNodes: function(node, path) { var me = this, nodeData = { data: [] }, len = node.length, children, i, child, name; me.nodes[path] = nodeData; for (i = 0; i < len; ++i) { nodeData.data.push(child = node[i]); name = child.text || child.title; child.id = path ? path + '/' + name : name; children = child.children; if (!(child.leaf = !children)) { delete child.children; me.buildNodes(children, child.id); } } }, fixTree: function(ctx, tree) { var me = this, node = ctx.params.node, nodes; if (!(nodes = me.nodes)) { me.nodes = nodes = {}; me.buildNodes(tree, ''); } node = nodes[node]; if (node) { if (me.node) { me.node.sortedData = me.sortedData; me.node.currentOrder = me.currentOrder; } me.node = node; me.data = node.data; me.sortedData = node.sortedData; me.currentOrder = node.currentOrder; } else { me.data = null; } }, getData: function(ctx) { var me = this, params = ctx.params, order = (params.filter || '') + (params.group || '') + '-' + (params.sort || '') + '-' + (params.dir || ''), tree = me.tree, dynamicData, data, fields, sortFn; if (tree) { me.fixTree(ctx, tree); } data = me.data; if (typeof data === 'function') { dynamicData = true; data = data.call(this, ctx); } // If order is '--' then it means we had no order passed, due to the string concat above if (!data || order === '--') { return data || []; } if (!dynamicData && order == me.currentOrder) { return me.sortedData; } ctx.filterSpec = params.filter && Ext.decode(params.filter); ctx.groupSpec = params.group && Ext.decode(params.group); fields = params.sort; if (params.dir) { fields = [ { direction: params.dir, property: fields } ]; } else { fields = Ext.decode(params.sort); } if (ctx.filterSpec) { var filters = new Ext.util.FilterCollection(); filters.add(this.processFilters(ctx.filterSpec)); data = Ext.Array.filter(data, filters.getFilterFn()); } sortFn = makeSortFns((ctx.sortSpec = fields)); if (ctx.groupSpec) { sortFn = makeSortFns([ ctx.groupSpec ], sortFn); } // If a straight Ajax request, data may not be an array. // If an Array, preserve 'physical' order of raw data... data = Ext.isArray(data) ? data.slice(0) : data; if (sortFn) { Ext.Array.sort(data, sortFn); } me.sortedData = data; me.currentOrder = order; return data; }, processFilters: Ext.identityFn, getPage: function(ctx, data) { var ret = data, length = data.length, start = ctx.params.start || 0, end = ctx.params.limit ? Math.min(length, start + ctx.params.limit) : length; if (start || end < length) { ret = ret.slice(start, end); } return ret; }, getGroupSummary: function(groupField, rows, ctx) { return rows[0]; }, getSummary: function(ctx, data, page) { var me = this, groupField = ctx.groupSpec.property, accum, todo = {}, summary = [], fieldValue, lastFieldValue; Ext.each(page, function(rec) { fieldValue = rec[groupField]; todo[fieldValue] = true; }); function flush() { if (accum) { summary.push(me.getGroupSummary(groupField, accum, ctx)); accum = null; } } // data is ordered primarily by the groupField, so one pass can pick up all // the summaries one at a time. Ext.each(data, function(rec) { fieldValue = rec[groupField]; if (lastFieldValue !== fieldValue) { flush(); lastFieldValue = fieldValue; } if (!todo[fieldValue]) { // if we have even 1 summary, we have summarized all that we need // (again because data and page are ordered by groupField) return !summary.length; } if (accum) { accum.push(rec); } else { accum = [ rec ]; } return true; }); flush(); // make sure that last pesky summary goes... return summary; } }; }()); /** * JSON Simlet. */ Ext.define('Ext.ux.ajax.JsonSimlet', { extend: 'Ext.ux.ajax.DataSimlet', alias: 'simlet.json', doGet: function(ctx) { var me = this, data = me.getData(ctx), page = me.getPage(ctx, data), reader = ctx.xhr.options.proxy && ctx.xhr.options.proxy.getReader(), root = reader && reader.getRootProperty(), ret = me.callParent(arguments), // pick up status/statusText response = {}; if (root && Ext.isArray(page)) { response[root] = page; response[reader.getTotalProperty()] = data.length; } else { response = page; } if (ctx.groupSpec) { response.summaryData = me.getSummary(ctx, data, page); } ret.responseText = Ext.encode(response); return ret; } }); /** * @author Don Griffin * * Simulates an XMLHttpRequest object's methods and properties but is backed by a * {@link Ext.ux.ajax.Simlet} instance that provides the data. */ Ext.define('Ext.ux.ajax.SimXhr', { readyState: 0, mgr: null, simlet: null, constructor: function(config) { var me = this; Ext.apply(me, config); me.requestHeaders = {}; }, abort: function() { var me = this; if (me.timer) { clearTimeout(me.timer); me.timer = null; } me.aborted = true; }, getAllResponseHeaders: function() { var headers = []; if (Ext.isObject(this.responseHeaders)) { Ext.Object.each(this.responseHeaders, function(name, value) { headers.push(name + ': ' + value); }); } return headers.join('\r\n'); }, getResponseHeader: function(header) { var headers = this.responseHeaders; return (headers && headers[header]) || null; }, open: function(method, url, async, user, password) { var me = this; me.method = method; me.url = url; me.async = async !== false; me.user = user; me.password = password; me.setReadyState(1); }, overrideMimeType: function(mimeType) { this.mimeType = mimeType; }, schedule: function() { var me = this, delay = me.mgr.delay; if (delay) { me.timer = setTimeout(function() { me.onTick(); }, delay); } else { me.onTick(); } }, send: function(body) { var me = this; me.body = body; if (me.async) { me.schedule(); } else { me.onComplete(); } }, setReadyState: function(state) { var me = this; if (me.readyState != state) { me.readyState = state; me.onreadystatechange(); } }, setRequestHeader: function(header, value) { this.requestHeaders[header] = value; }, // handlers onreadystatechange: Ext.emptyFn, onComplete: function() { var me = this, callback; me.readyState = 4; Ext.apply(me, me.simlet.exec(me)); callback = me.jsonpCallback; if (callback) { var text = callback + '(' + me.responseText + ')'; eval(text); } }, onTick: function() { var me = this; me.timer = null; me.onComplete(); me.onreadystatechange && me.onreadystatechange(); } }); /** * @author Don Griffin * * This singleton manages simulated Ajax responses. This allows application logic to be * written unaware that its Ajax calls are being handled by simulations ("simlets"). This * is currently done by hooking {@link Ext.data.Connection} methods, so all users of that * class (and {@link Ext.Ajax} since it is a derived class) qualify for simulation. * * The requires hooks are inserted when either the {@link #init} method is called or the * first {@link Ext.ux.ajax.Simlet} is registered. For example: * * Ext.onReady(function () { * initAjaxSim(); * * // normal stuff * }); * * function initAjaxSim () { * Ext.ux.ajax.SimManager.init({ * delay: 300 * }).register({ * '/app/data/url': { * type: 'json', // use JsonSimlet (type is like xtype for components) * data: [ * { foo: 42, bar: 'abc' }, * ... * ] * } * }); * } * * As many URL's as desired can be registered and associated with a {@link Ext.ux.ajax.Simlet}. To make * non-simulated Ajax requests once this singleton is initialized, add a `nosim:true` option * to the Ajax options: * * Ext.Ajax.request({ * url: 'page.php', * nosim: true, // ignored by normal Ajax request * params: { * id: 1 * }, * success: function(response){ * var text = response.responseText; * // process server response here * } * }); */ Ext.define('Ext.ux.ajax.SimManager', { singleton: true, requires: [ 'Ext.data.Connection', 'Ext.ux.ajax.SimXhr', 'Ext.ux.ajax.Simlet', 'Ext.ux.ajax.JsonSimlet' ], /** * @cfg {Ext.ux.ajax.Simlet} defaultSimlet * The {@link Ext.ux.ajax.Simlet} instance to use for non-matching URL's. By default, this will * return 404. Set this to null to use real Ajax calls for non-matching URL's. */ /** * @cfg {String} defaultType * The default `type` to apply to generic {@link Ext.ux.ajax.Simlet} configuration objects. The * default is 'basic'. */ defaultType: 'basic', /** * @cfg {Number} delay * The number of milliseconds to delay before delivering a response to an async request. */ delay: 150, /** * @property {Boolean} ready * True once this singleton has initialized and applied its Ajax hooks. * @private */ ready: false, constructor: function() { this.simlets = []; }, getSimlet: function(url) { // Strip down to base URL (no query parameters or hash): var me = this, index = url.indexOf('?'), simlets = me.simlets, len = simlets.length, i, simlet, simUrl, match; if (index < 0) { index = url.indexOf('#'); } if (index > 0) { url = url.substring(0, index); } for (i = 0; i < len; ++i) { simlet = simlets[i]; simUrl = simlet.url; if (simUrl instanceof RegExp) { match = simUrl.test(url); } else { match = simUrl === url; } if (match) { return simlet; } } return me.defaultSimlet; }, getXhr: function(method, url, options, async) { var simlet = this.getSimlet(url); if (simlet) { return simlet.openRequest(method, url, options, async); } return null; }, /** * Initializes this singleton and applies configuration options. * @param {Object} config An optional object with configuration properties to apply. * @return {Ext.ux.ajax.SimManager} this */ init: function(config) { var me = this; Ext.apply(me, config); if (!me.ready) { me.ready = true; if (!('defaultSimlet' in me)) { me.defaultSimlet = new Ext.ux.ajax.Simlet({ status: 404, statusText: 'Not Found' }); } me._openRequest = Ext.data.Connection.prototype.openRequest; Ext.data.Connection.override({ openRequest: function(options, requestOptions, async) { var xhr = !options.nosim && me.getXhr(requestOptions.method, requestOptions.url, options, async); if (!xhr) { xhr = this.callParent(arguments); } return xhr; } }); if (Ext.data.JsonP) { Ext.data.JsonP.self.override({ createScript: function(url, params, options) { var fullUrl = Ext.urlAppend(url, Ext.Object.toQueryString(params)), script = !options.nosim && me.getXhr('GET', fullUrl, options, true); if (!script) { script = this.callParent(arguments); } return script; }, loadScript: function(request) { var script = request.script; if (script.simlet) { script.jsonpCallback = request.params[request.callbackKey]; script.send(null); // Ext.data.JsonP will attempt dom removal of a script tag, so emulate its presence request.script = document.createElement('script'); } else { this.callParent(arguments); } } }); } } return me; }, openRequest: function(method, url, async) { var opt = { method: method, url: url }; return this._openRequest.call(Ext.data.Connection.prototype, {}, opt, async); }, /** * Registeres one or more {@link Ext.ux.ajax.Simlet} instances. * @param {Array/Object} simlet Either a {@link Ext.ux.ajax.Simlet} instance or config, an Array * of such elements or an Object keyed by URL with values that are {@link Ext.ux.ajax.Simlet} * instances or configs. */ register: function(simlet) { var me = this; me.init(); function reg(one) { var simlet = one; if (!simlet.isSimlet) { simlet = Ext.create('simlet.' + (simlet.type || simlet.stype || me.defaultType), one); } me.simlets.push(simlet); simlet.manager = me; } if (Ext.isArray(simlet)) { Ext.each(simlet, reg); } else if (simlet.isSimlet || simlet.url) { reg(simlet); } else { Ext.Object.each(simlet, function(url, s) { s.url = url; reg(s); }); } return me; } }); /** * This class simulates XML-based requests. */ Ext.define('Ext.ux.ajax.XmlSimlet', { extend: 'Ext.ux.ajax.DataSimlet', alias: 'simlet.xml', /** * This template is used to populate the XML response. The configuration of the Reader * is available so that its `root` and `record` properties can be used as well as the * `fields` of the associated `model`. But beyond that, the way these pieces are put * together in the document requires the flexibility of a template. */ xmlTpl: [ '<{root}>\n', '', ' <{parent.record}>\n', '', ' <{name}>{[parent[values.name]]}\n', '', ' \n', '', '' ], doGet: function(ctx) { var me = this, data = me.getData(ctx), page = me.getPage(ctx, data), proxy = ctx.xhr.options.operation.getProxy(), reader = proxy && proxy.getReader(), model = reader && reader.getModel(), ret = me.callParent(arguments), // pick up status/statusText response = { data: page, reader: reader, fields: model && model.fields, root: reader && reader.getRootProperty(), record: reader && reader.record }, tpl, xml, doc; if (ctx.groupSpec) { response.summaryData = me.getSummary(ctx, data, page); } // If a straight Ajax request there won't be an xmlTpl. if (me.xmlTpl) { tpl = Ext.XTemplate.getTpl(me, 'xmlTpl'); xml = tpl.apply(response); } else { xml = data; } if (typeof DOMParser != 'undefined') { doc = (new DOMParser()).parseFromString(xml, "text/xml"); } else { // IE doesn't have DOMParser, but fortunately, there is an ActiveX for XML doc = new ActiveXObject("Microsoft.XMLDOM"); doc.async = false; doc.loadXML(xml); } ret.responseText = xml; ret.responseXML = doc; return ret; }, fixTree: function() { this.callParent(arguments); var buffer = []; this.buildTreeXml(this.data, buffer); this.data = buffer.join(''); }, buildTreeXml: function(nodes, buffer) { var rootProperty = this.rootProperty, recordProperty = this.recordProperty; buffer.push('<', rootProperty, '>'); Ext.Array.forEach(nodes, function(node) { buffer.push('<', recordProperty, '>'); for (var key in node) { if (key == 'children') { this.buildTreeXml(node.children, buffer); } else { buffer.push('<', key, '>', node[key], ''); } } buffer.push(''); }); buffer.push(''); } }); /** * This base class can be used by derived classes to dynamically require Google API's. */ Ext.define('Ext.ux.google.Api', { mixins: [ 'Ext.mixin.Mashup' ], requiredScripts: [ '//www.google.com/jsapi' ], statics: { loadedModules: {} }, /* * feeds: [ callback1, callback2, .... ] transitions to -> feeds : true (when complete) */ onClassExtended: function(cls, data, hooks) { var onBeforeClassCreated = hooks.onBeforeCreated, Api = this; // the Ext.ux.google.Api class hooks.onBeforeCreated = function(cls, data) { var me = this, apis = [], requiresGoogle = Ext.Array.from(data.requiresGoogle), loadedModules = Api.loadedModules, remaining = 0, callback = function() { if (!--remaining) { onBeforeClassCreated.call(me, cls, data, hooks); } Ext.env.Ready.unblock(); }, api, i, length; /* * requiresGoogle: [ * 'feeds', * { api: 'feeds', version: '1.x', * callback : fn, nocss : true } //optionals * ] */ length = requiresGoogle.length; for (i = 0; i < length; ++i) { if (Ext.isString(api = requiresGoogle[i])) { apis.push({ api: api }); } else if (Ext.isObject(api)) { apis.push(Ext.apply({}, api)); } } Ext.each(apis, function(api) { var name = api.api, version = String(api.version || '1.x'), module = loadedModules[name]; if (!module) { ++remaining; Ext.env.Ready.block(); loadedModules[name] = module = [ callback ].concat(api.callback || []); delete api.api; delete api.version; //TODO: window.google assertion? google.load(name, version, Ext.applyIf({ callback: function() { loadedModules[name] = true; for (var n = module.length; n-- > 0; ) { module[n](); } } }, //iterate callbacks in reverse api)); } else if (module !== true) { module.push(callback); } }); if (!remaining) { onBeforeClassCreated.call(me, cls, data, hooks); } }; } }); /** * This class, when required, ensures that the Google RSS Feeds API is available. */ Ext.define('Ext.ux.google.Feeds', { extend: 'Ext.ux.google.Api', requiresGoogle: { api: 'feeds', nocss: true } }); /** * This view is created by the "google-rss" `Ext.dashboard.Dashboard` part. */ Ext.define('Ext.ux.dashboard.GoogleRssView', { extend: 'Ext.Component', requires: [ 'Ext.tip.ToolTip', 'Ext.ux.google.Feeds' ], feedCls: Ext.baseCSSPrefix + 'dashboard-googlerss', previewCls: Ext.baseCSSPrefix + 'dashboard-googlerss-preview', closeDetailsCls: Ext.baseCSSPrefix + 'dashboard-googlerss-close', nextCls: Ext.baseCSSPrefix + 'dashboard-googlerss-next', prevCls: Ext.baseCSSPrefix + 'dashboard-googlerss-prev', /** * The RSS feed URL. Some example RSS Feeds: * * * http://rss.slashdot.org/Slashdot/slashdot * * http://sports.espn.go.com/espn/rss/news (ESPN Top News) * * http://news.google.com/news?ned=us&topic=t&output=rss (Sci/Tech - Google News) * * http://rss.news.yahoo.com/rss/software (Yahoo Software News) * * http://feeds.feedburner.com/extblog (Sencha Blog) * * http://sencha.com/forum/external.php?type=RSS2 (Sencha Forums) * * http://feeds.feedburner.com/ajaxian (Ajaxian) * * http://rss.cnn.com/rss/edition.rss (CNN Top Stories) */ feedUrl: null, scrollable: true, maxFeedEntries: 10, previewTips: false, mode: 'detail', //closeDetailsGlyph: '10008@', closeDetailsGlyph: '8657@', // black triangles prevGlyph: '9664@', nextGlyph: '9654@', // hollow triangles //prevGlyph: '9665@', nextGlyph: '9655@', // black pointing index //prevGlyph: '9754@', nextGlyph: '9755@', // white pointing index //prevGlyph: '9756@', nextGlyph: '9758@', // double arrows //prevGlyph: '8656@', nextGlyph: '8658@', // closed arrows //prevGlyph: '8678@', nextGlyph: '8680@', detailTpl: '' + '
' + '
' + '' + '' + '{parent.prevGlyph}' + ' ' + '' + ' {[parent.currentEntry+1]}/{parent.numEntries} ' + '' + ' style="visibility:hidden"' + '' + '> {parent.nextGlyph}' + ' ' + ' ' + '{parent.closeGlyph}' + ' ' + '
' + '
' + '{title}' + '
' + '
By {author} - {publishedDate:this.date}
' + '
' + '
' + '{content}' + '
' + '
', summaryTpl: '' + '
' + '' + '{title}' + ' ' + '
' + 'By {author} - {publishedDate:this.date}
' + '{contentSnippet}
' + '
' + '
', initComponent: function() { var me = this; me.feedMgr = new google.feeds.Feed(me.feedUrl); me.callParent(); }, afterRender: function() { var me = this; me.callParent(); if (me.feedMgr) { me.refresh(); } me.el.on({ click: me.onClick, scope: me }); if (me.previewTips) { me.tip = new Ext.tip.ToolTip({ target: me.el, delegate: '.' + me.previewCls, maxWidth: 800, showDelay: 750, autoHide: false, scrollable: true, anchor: 'top', listeners: { beforeshow: 'onBeforeShowTip', scope: me } }); } }, formatDate: function(date) { if (!date) { return ''; } date = new Date(date); var now = new Date(), d = Ext.Date.clearTime(now, true), notime = Ext.Date.clearTime(date, true).getTime(); if (notime === d.getTime()) { return 'Today ' + Ext.Date.format(date, 'g:i a'); } d = Ext.Date.add(d, 'd', -6); if (d.getTime() <= notime) { return Ext.Date.format(date, 'D g:i a'); } if (d.getYear() === now.getYear()) { return Ext.Date.format(date, 'D M d \\a\\t g:i a'); } return Ext.Date.format(date, 'D M d, Y \\a\\t g:i a'); }, getTitle: function() { var data = this.data; return data && data.title; }, onBeforeShowTip: function(tip) { if (this.mode !== 'summary') { return false; } var el = tip.triggerElement, index = parseInt(el.getAttribute('data-index'), 10); tip.maxHeight = Ext.Element.getViewportHeight() / 2; tip.update(this.data.entries[index - 1].content); }, onClick: function(e) { var me = this, entry = me.data.currentEntry, target = Ext.fly(e.getTarget()); if (target.hasCls(me.nextCls)) { me.setCurrentEntry(entry + 1); } else if (target.hasCls(me.prevCls)) { me.setCurrentEntry(entry - 1); } else if (target.hasCls(me.closeDetailsCls)) { me.setMode('summary'); } else if (target.hasCls(me.previewCls)) { me.setMode('detail', parseInt(target.getAttribute('data-index'), 10)); } }, refresh: function() { var me = this; if (!me.feedMgr) { return; } me.fireEvent('beforeload', me); //setTimeout(function () { me.feedMgr.setNumEntries(me.maxFeedEntries); me.feedMgr.load(function(result) { me.setFeedData(result.feed); me.fireEvent('load', me); }); }, //}, 2000); setCurrentEntry: function(current) { this.setMode(this.mode, current); }, setFeedData: function(feedData) { var me = this, entries = feedData.entries, count = entries && entries.length || 0, data = Ext.apply({ numEntries: count, closeGlyph: me.wrapGlyph(me.closeDetailsGlyph), prevGlyph: me.wrapGlyph(me.prevGlyph), nextGlyph: me.wrapGlyph(me.nextGlyph), currentEntry: 0 }, feedData); me.data = data; me.setMode(me.mode); }, setMode: function(mode, currentEntry) { var me = this, data = me.data, current = (currentEntry === undefined) ? data.currentEntry : currentEntry; me.tpl = me.getTpl(mode + 'Tpl'); me.tpl.date = me.formatDate; me.mode = mode; data.currentEntry = current; data.hasNext = current + 1 < data.numEntries; data.hasPrev = current > 0; me.update(data); me.el.dom.scrollTop = 0; }, wrapGlyph: function(glyph) { var glyphFontFamily = Ext._glyphFontFamily, glyphParts, html; if (typeof glyph === 'string') { glyphParts = glyph.split('@'); glyph = glyphParts[0]; glyphFontFamily = glyphParts[1]; } html = '&#' + glyph + ';'; if (glyphFontFamily) { html = '' + html + ''; } return html; }, // @private beforeDestroy: function() { Ext.destroy(this.tip); this.callParent(); } }); /** * This `part` implements a Google RSS Feed for use in a `Dashboard`. */ Ext.define('Ext.ux.dashboard.GoogleRssPart', { extend: 'Ext.dashboard.Part', alias: 'part.google-rss', requires: [ 'Ext.window.MessageBox', 'Ext.ux.dashboard.GoogleRssView' ], viewTemplate: { layout: 'fit', items: { xclass: 'Ext.ux.dashboard.GoogleRssView', feedUrl: '{feedUrl}' } }, type: 'google-rss', config: { suggestedFeed: 'http://rss.slashdot.org/Slashdot/slashdot' }, formTitleAdd: 'Add RSS Feed', formTitleEdit: 'Edit RSS Feed', formLabel: 'RSS Feed URL', displayForm: function(instance, currentConfig, callback, scope) { var me = this, suggestion = currentConfig ? currentConfig.feedUrl : me.getSuggestedFeed(), title = instance ? me.formTitleEdit : me.formTitleAdd; Ext.Msg.prompt(title, me.formLabel, function(btn, text) { if (btn === 'ok') { callback.call(scope || me, { feedUrl: text }); } }, me, false, suggestion); } }); /** * Paging Memory Proxy, allows to use paging grid with in memory dataset */ Ext.define('Ext.ux.data.PagingMemoryProxy', { extend: 'Ext.data.proxy.Memory', alias: 'proxy.pagingmemory', alternateClassName: 'Ext.data.PagingMemoryProxy', constructor: function() { Ext.log.warn('Ext.ux.data.PagingMemoryProxy functionality has been merged into Ext.data.proxy.Memory by using the enablePaging flag.'); this.callParent(arguments); }, read: function(operation, callback, scope) { var reader = this.getReader(), result = reader.read(this.data), sorters, filters, sorterFn, records; scope = scope || this; // filtering filters = operation.filters; if (filters.length > 0) { //at this point we have an array of Ext.util.Filter objects to filter with, //so here we construct a function that combines these filters by ANDing them together records = []; Ext.each(result.records, function(record) { var isMatch = true, length = filters.length, i; for (i = 0; i < length; i++) { var filter = filters[i], fn = filter.filterFn, scope = filter.scope; isMatch = isMatch && fn.call(scope, record); } if (isMatch) { records.push(record); } }, this); result.records = records; result.totalRecords = result.total = records.length; } // sorting sorters = operation.sorters; if (sorters.length > 0) { //construct an amalgamated sorter function which combines all of the Sorters passed sorterFn = function(r1, r2) { var result = sorters[0].sort(r1, r2), length = sorters.length, i; //if we have more than one sorter, OR any additional sorter functions together for (i = 1; i < length; i++) { result = result || sorters[i].sort.call(this, r1, r2); } return result; }; result.records.sort(sorterFn); } // paging (use undefined cause start can also be 0 (thus false)) if (operation.start !== undefined && operation.limit !== undefined) { result.records = result.records.slice(operation.start, operation.start + operation.limit); result.count = result.records.length; } Ext.apply(operation, { resultSet: result }); operation.setCompleted(); operation.setSuccessful(); Ext.Function.defer(function() { Ext.callback(callback, scope, [ operation ]); }, 10); } }); // A DropZone which cooperates with DragZones whose dragData contains // a "field" property representing a form Field. Fields may be dropped onto // grid data cells containing a matching data type. Ext.define('Ext.ux.dd.CellFieldDropZone', { extend: 'Ext.dd.DropZone', constructor: function(cfg) { cfg = cfg || {}; if (cfg.onCellDrop) { this.onCellDrop = cfg.onCellDrop; } if (cfg.ddGroup) { this.ddGroup = cfg.ddGroup; } }, // Call the DropZone constructor using the View's scrolling element // only after the grid has been rendered. init: function(grid) { var me = this; if (grid.rendered) { me.grid = grid; grid.getView().on({ render: function(v) { me.view = v; Ext.ux.dd.CellFieldDropZone.superclass.constructor.call(me, me.view.el); }, single: true }); } else { grid.on('render', me.init, me, { single: true }); } }, // Scroll the main configured Element when we drag close to the edge containerScroll: true, getTargetFromEvent: function(e) { var me = this, v = me.view; // Ascertain whether the mousemove is within a grid cell var cell = e.getTarget(v.getCellSelector()); if (cell) { // We *are* within a grid cell, so ask the View exactly which one, // Extract data from the Model to create a target object for // processing in subsequent onNodeXXXX methods. Note that the target does // not have to be a DOM element. It can be whatever the noNodeXXX methods are // programmed to expect. var row = v.findItemByChild(cell), columnIndex = cell.cellIndex; if (row && Ext.isDefined(columnIndex)) { return { node: cell, record: v.getRecord(row), fieldName: me.grid.getVisibleColumnManager().getColumns()[columnIndex].dataIndex }; } } }, // On Node enter, see if it is valid for us to drop the field on that type of column. onNodeEnter: function(target, dd, e, dragData) { delete this.dropOK; if (!target) { return; } // Check that a field is being dragged. var f = dragData.field; if (!f) { return; } // Check whether the data type of the column being dropped on accepts the // dragged field type. If so, set dropOK flag, and highlight the target node. var field = target.record.fieldsMap[target.fieldName]; if (field.isNumeric) { if (!f.isXType('numberfield')) { return; } } else if (field.isDateField) { if (!f.isXType('datefield')) { return; } } else if (field.isBooleanField) { if (!f.isXType('checkbox')) { return; } } this.dropOK = true; Ext.fly(target.node).addCls('x-drop-target-active'); }, // Return the class name to add to the drag proxy. This provides a visual indication // of drop allowed or not allowed. onNodeOver: function(target, dd, e, dragData) { return this.dropOK ? this.dropAllowed : this.dropNotAllowed; }, // highlight the target node. onNodeOut: function(target, dd, e, dragData) { Ext.fly(target.node).removeCls('x-drop-target-active'); }, // Process the drop event if we have previously ascertained that a drop is OK. onNodeDrop: function(target, dd, e, dragData) { if (this.dropOK) { var value = dragData.field.getValue(); target.record.set(target.fieldName, value); this.onCellDrop(target.fieldName, value); return true; } }, onCellDrop: Ext.emptyFn }); Ext.define('Ext.ux.dd.PanelFieldDragZone', { extend: 'Ext.dd.DragZone', constructor: function(cfg) { cfg = cfg || {}; if (cfg.ddGroup) { this.ddGroup = cfg.ddGroup; } }, // Call the DRagZone's constructor. The Panel must have been rendered. init: function(panel) { // Panel is an HtmlElement if (panel.nodeType) { // Called via dragzone::init Ext.ux.dd.PanelFieldDragZone.superclass.init.apply(this, arguments); } else // Panel is a Component - need the el { // Called via plugin::init if (panel.rendered) { Ext.ux.dd.PanelFieldDragZone.superclass.constructor.call(this, panel.getEl()); } else { panel.on('afterrender', this.init, this, { single: true }); } } }, scroll: false, // On mousedown, we ascertain whether it is on one of our draggable Fields. // If so, we collect data about the draggable object, and return a drag data // object which contains our own data, plus a "ddel" property which is a DOM // node which provides a "view" of the dragged data. getDragData: function(e) { var targetLabel = e.getTarget('label', null, true), text, oldMark, field, dragEl; if (targetLabel) { // Get the data we are dragging: the Field // create a ddel for the drag proxy to display field = Ext.getCmp(targetLabel.up('.' + Ext.form.Labelable.prototype.formItemCls).id); // Temporary prevent marking the field as invalid, since it causes changes // to the underlying dom element which can cause problems in IE oldMark = field.preventMark; field.preventMark = true; if (field.isValid()) { field.preventMark = oldMark; dragEl = document.createElement('div'); dragEl.className = Ext.baseCSSPrefix + 'form-text'; text = field.getRawValue(); dragEl.innerHTML = Ext.isEmpty(text) ? ' ' : text; Ext.fly(dragEl).setWidth(field.getEl().getWidth()); return { field: field, ddel: dragEl }; } else { e.stopEvent(); } field.preventMark = oldMark; } }, // The coordinates to slide the drag proxy back to on failed drop. getRepairXY: function() { return this.dragData.field.getEl().getXY(); } }); /*! * Ext JS Library * Copyright(c) 2006-2014 Sencha Inc. * licensing@sencha.com * http://www.sencha.com/license */ /** * @class Ext.ux.desktop.Desktop * @extends Ext.panel.Panel *

This class manages the wallpaper, shortcuts and taskbar.

*/ Ext.define('Ext.ux.desktop.Desktop', { extend: 'Ext.panel.Panel', alias: 'widget.desktop', uses: [ 'Ext.util.MixedCollection', 'Ext.menu.Menu', 'Ext.view.View', // dataview 'Ext.window.Window', 'Ext.ux.desktop.TaskBar', 'Ext.ux.desktop.Wallpaper' ], activeWindowCls: 'ux-desktop-active-win', inactiveWindowCls: 'ux-desktop-inactive-win', lastActiveWindow: null, border: false, html: ' ', layout: 'fit', xTickSize: 1, yTickSize: 1, app: null, /** * @cfg {Array/Ext.data.Store} shortcuts * The items to add to the DataView. This can be a {@link Ext.data.Store Store} or a * simple array. Items should minimally provide the fields in the * {@link Ext.ux.desktop.ShortcutModel Shortcut}. */ shortcuts: null, /** * @cfg {String} shortcutItemSelector * This property is passed to the DataView for the desktop to select shortcut items. * If the {@link #shortcutTpl} is modified, this will probably need to be modified as * well. */ shortcutItemSelector: 'div.ux-desktop-shortcut', /** * @cfg {String} shortcutTpl * This XTemplate is used to render items in the DataView. If this is changed, the * {@link #shortcutItemSelector} will probably also need to changed. */ shortcutTpl: [ '', '
', '
', '', '
', '{name}', '
', '
', '
' ], /** * @cfg {Object} taskbarConfig * The config object for the TaskBar. */ taskbarConfig: null, windowMenu: null, initComponent: function() { var me = this; me.windowMenu = new Ext.menu.Menu(me.createWindowMenu()); me.bbar = me.taskbar = new Ext.ux.desktop.TaskBar(me.taskbarConfig); me.taskbar.windowMenu = me.windowMenu; me.windows = new Ext.util.MixedCollection(); me.contextMenu = new Ext.menu.Menu(me.createDesktopMenu()); me.items = [ { xtype: 'wallpaper', id: me.id + '_wallpaper' }, me.createDataView() ]; me.callParent(); me.shortcutsView = me.items.getAt(1); me.shortcutsView.on('itemclick', me.onShortcutItemClick, me); var wallpaper = me.wallpaper; me.wallpaper = me.items.getAt(0); if (wallpaper) { me.setWallpaper(wallpaper, me.wallpaperStretch); } }, afterRender: function() { var me = this; me.callParent(); me.el.on('contextmenu', me.onDesktopMenu, me); }, //------------------------------------------------------ // Overrideable configuration creation methods createDataView: function() { var me = this; return { xtype: 'dataview', overItemCls: 'x-view-over', trackOver: true, itemSelector: me.shortcutItemSelector, store: me.shortcuts, style: { position: 'absolute' }, x: 0, y: 0, tpl: new Ext.XTemplate(me.shortcutTpl) }; }, createDesktopMenu: function() { var me = this, ret = { items: me.contextMenuItems || [] }; if (ret.items.length) { ret.items.push('-'); } ret.items.push({ text: 'Tile', handler: me.tileWindows, scope: me, minWindows: 1 }, { text: 'Cascade', handler: me.cascadeWindows, scope: me, minWindows: 1 }); return ret; }, createWindowMenu: function() { var me = this; return { defaultAlign: 'br-tr', items: [ { text: 'Restore', handler: me.onWindowMenuRestore, scope: me }, { text: 'Minimize', handler: me.onWindowMenuMinimize, scope: me }, { text: 'Maximize', handler: me.onWindowMenuMaximize, scope: me }, '-', { text: 'Close', handler: me.onWindowMenuClose, scope: me } ], listeners: { beforeshow: me.onWindowMenuBeforeShow, hide: me.onWindowMenuHide, scope: me } }; }, //------------------------------------------------------ // Event handler methods onDesktopMenu: function(e) { var me = this, menu = me.contextMenu; e.stopEvent(); if (!menu.rendered) { menu.on('beforeshow', me.onDesktopMenuBeforeShow, me); } menu.showAt(e.getXY()); menu.doConstrain(); }, onDesktopMenuBeforeShow: function(menu) { var me = this, count = me.windows.getCount(); menu.items.each(function(item) { var min = item.minWindows || 0; item.setDisabled(count < min); }); }, onShortcutItemClick: function(dataView, record) { var me = this, module = me.app.getModule(record.data.module), win = module && module.createWindow(); if (win) { me.restoreWindow(win); } }, onWindowClose: function(win) { var me = this; me.windows.remove(win); me.taskbar.removeTaskButton(win.taskButton); me.updateActiveWindow(); }, //------------------------------------------------------ // Window context menu handlers onWindowMenuBeforeShow: function(menu) { var items = menu.items.items, win = menu.theWin; items[0].setDisabled(win.maximized !== true && win.hidden !== true); // Restore items[1].setDisabled(win.minimized === true); // Minimize items[2].setDisabled(win.maximized === true || win.hidden === true); }, // Maximize onWindowMenuClose: function() { var me = this, win = me.windowMenu.theWin; win.close(); }, onWindowMenuHide: function(menu) { Ext.defer(function() { menu.theWin = null; }, 1); }, onWindowMenuMaximize: function() { var me = this, win = me.windowMenu.theWin; win.maximize(); win.toFront(); }, onWindowMenuMinimize: function() { var me = this, win = me.windowMenu.theWin; win.minimize(); }, onWindowMenuRestore: function() { var me = this, win = me.windowMenu.theWin; me.restoreWindow(win); }, //------------------------------------------------------ // Dynamic (re)configuration methods getWallpaper: function() { return this.wallpaper.wallpaper; }, setTickSize: function(xTickSize, yTickSize) { var me = this, xt = me.xTickSize = xTickSize, yt = me.yTickSize = (arguments.length > 1) ? yTickSize : xt; me.windows.each(function(win) { var dd = win.dd, resizer = win.resizer; dd.xTickSize = xt; dd.yTickSize = yt; resizer.widthIncrement = xt; resizer.heightIncrement = yt; }); }, setWallpaper: function(wallpaper, stretch) { this.wallpaper.setWallpaper(wallpaper, stretch); return this; }, //------------------------------------------------------ // Window management methods cascadeWindows: function() { var x = 0, y = 0, zmgr = this.getDesktopZIndexManager(); zmgr.eachBottomUp(function(win) { if (win.isWindow && win.isVisible() && !win.maximized) { win.setPosition(x, y); x += 20; y += 20; } }); }, createWindow: function(config, cls) { var me = this, win, cfg = Ext.applyIf(config || {}, { stateful: false, isWindow: true, constrainHeader: true, minimizable: true, maximizable: true }); cls = cls || Ext.window.Window; win = me.add(new cls(cfg)); me.windows.add(win); win.taskButton = me.taskbar.addTaskButton(win); win.animateTarget = win.taskButton.el; win.on({ activate: me.updateActiveWindow, beforeshow: me.updateActiveWindow, deactivate: me.updateActiveWindow, minimize: me.minimizeWindow, destroy: me.onWindowClose, scope: me }); win.on({ boxready: function() { win.dd.xTickSize = me.xTickSize; win.dd.yTickSize = me.yTickSize; if (win.resizer) { win.resizer.widthIncrement = me.xTickSize; win.resizer.heightIncrement = me.yTickSize; } }, single: true }); // replace normal window close w/fadeOut animation: win.doClose = function() { win.doClose = Ext.emptyFn; // dblclick can call again... win.el.disableShadow(); win.el.fadeOut({ listeners: { afteranimate: function() { win.destroy(); } } }); }; return win; }, getActiveWindow: function() { var win = null, zmgr = this.getDesktopZIndexManager(); if (zmgr) { // We cannot rely on activate/deactive because that fires against non-Window // components in the stack. zmgr.eachTopDown(function(comp) { if (comp.isWindow && !comp.hidden) { win = comp; return false; } return true; }); } return win; }, getDesktopZIndexManager: function() { var windows = this.windows; // TODO - there has to be a better way to get this... return (windows.getCount() && windows.getAt(0).zIndexManager) || null; }, getWindow: function(id) { return this.windows.get(id); }, minimizeWindow: function(win) { win.minimized = true; win.hide(); }, restoreWindow: function(win) { if (win.isVisible()) { win.restore(); win.toFront(); } else { win.show(); } return win; }, tileWindows: function() { var me = this, availWidth = me.body.getWidth(true); var x = me.xTickSize, y = me.yTickSize, nextY = y; me.windows.each(function(win) { if (win.isVisible() && !win.maximized) { var w = win.el.getWidth(); // Wrap to next row if we are not at the line start and this Window will // go off the end if (x > me.xTickSize && x + w > availWidth) { x = me.xTickSize; y = nextY; } win.setPosition(x, y); x += w + me.xTickSize; nextY = Math.max(nextY, y + win.el.getHeight() + me.yTickSize); } }); }, updateActiveWindow: function() { var me = this, activeWindow = me.getActiveWindow(), last = me.lastActiveWindow; if (last && last.isDestroyed) { me.lastActiveWindow = null; return; } if (activeWindow === last) { return; } if (last) { if (last.el.dom) { last.addCls(me.inactiveWindowCls); last.removeCls(me.activeWindowCls); } last.active = false; } me.lastActiveWindow = activeWindow; if (activeWindow) { activeWindow.addCls(me.activeWindowCls); activeWindow.removeCls(me.inactiveWindowCls); activeWindow.minimized = false; activeWindow.active = true; } me.taskbar.setActiveButton(activeWindow && activeWindow.taskButton); } }); /** * Ext JS Library * Copyright(c) 2006-2014 Sencha Inc. * licensing@sencha.com * http://www.sencha.com/license * @class Ext.ux.desktop.App */ Ext.define('Ext.ux.desktop.App', { mixins: { observable: 'Ext.util.Observable' }, requires: [ 'Ext.container.Viewport', 'Ext.ux.desktop.Desktop' ], isReady: false, modules: null, useQuickTips: true, constructor: function(config) { var me = this; me.mixins.observable.constructor.call(this, config); if (Ext.isReady) { Ext.Function.defer(me.init, 10, me); } else { Ext.onReady(me.init, me); } }, init: function() { var me = this, desktopCfg; if (me.useQuickTips) { Ext.QuickTips.init(); } me.modules = me.getModules(); if (me.modules) { me.initModules(me.modules); } desktopCfg = me.getDesktopConfig(); me.desktop = new Ext.ux.desktop.Desktop(desktopCfg); me.viewport = new Ext.container.Viewport({ layout: 'fit', items: [ me.desktop ] }); Ext.getWin().on('beforeunload', me.onUnload, me); me.isReady = true; me.fireEvent('ready', me); }, /** * This method returns the configuration object for the Desktop object. A derived * class can override this method, call the base version to build the config and * then modify the returned object before returning it. */ getDesktopConfig: function() { var me = this, cfg = { app: me, taskbarConfig: me.getTaskbarConfig() }; Ext.apply(cfg, me.desktopConfig); return cfg; }, getModules: Ext.emptyFn, /** * This method returns the configuration object for the Start Button. A derived * class can override this method, call the base version to build the config and * then modify the returned object before returning it. */ getStartConfig: function() { var me = this, cfg = { app: me, menu: [] }, launcher; Ext.apply(cfg, me.startConfig); Ext.each(me.modules, function(module) { launcher = module.launcher; if (launcher) { launcher.handler = launcher.handler || Ext.bind(me.createWindow, me, [ module ]); cfg.menu.push(module.launcher); } }); return cfg; }, createWindow: function(module) { var window = module.createWindow(); window.show(); }, /** * This method returns the configuration object for the TaskBar. A derived class * can override this method, call the base version to build the config and then * modify the returned object before returning it. */ getTaskbarConfig: function() { var me = this, cfg = { app: me, startConfig: me.getStartConfig() }; Ext.apply(cfg, me.taskbarConfig); return cfg; }, initModules: function(modules) { var me = this; Ext.each(modules, function(module) { module.app = me; }); }, getModule: function(name) { var ms = this.modules; for (var i = 0, len = ms.length; i < len; i++) { var m = ms[i]; if (m.id == name || m.appType == name) { return m; } } return null; }, onReady: function(fn, scope) { if (this.isReady) { fn.call(scope, this); } else { this.on({ ready: fn, scope: scope, single: true }); } }, getDesktop: function() { return this.desktop; }, onUnload: function(e) { if (this.fireEvent('beforeunload', this) === false) { e.stopEvent(); } } }); /*! * Ext JS Library * Copyright(c) 2006-2014 Sencha Inc. * licensing@sencha.com * http://www.sencha.com/license */ Ext.define('Ext.ux.desktop.Module', { mixins: { observable: 'Ext.util.Observable' }, constructor: function(config) { this.mixins.observable.constructor.call(this, config); this.init(); }, init: Ext.emptyFn }); /*! * Ext JS Library * Copyright(c) 2006-2014 Sencha Inc. * licensing@sencha.com * http://www.sencha.com/license */ /** * @class Ext.ux.desktop.ShortcutModel * @extends Ext.data.Model * This model defines the minimal set of fields for desktop shortcuts. */ Ext.define('Ext.ux.desktop.ShortcutModel', { extend: 'Ext.data.Model', fields: [ { name: 'name' }, { name: 'iconCls' }, { name: 'module' } ] }); /** * Ext JS Library * Copyright(c) 2006-2014 Sencha Inc. * licensing@sencha.com * http://www.sencha.com/license * @class Ext.ux.desktop.StartMenu */ Ext.define('Ext.ux.desktop.StartMenu', { extend: 'Ext.menu.Menu', // We want header styling like a Panel baseCls: Ext.baseCSSPrefix + 'panel', // Special styling within cls: 'x-menu ux-start-menu', bodyCls: 'ux-start-menu-body', defaultAlign: 'bl-tl', iconCls: 'user', bodyBorder: true, width: 300, initComponent: function() { var me = this; me.layout.align = 'stretch'; me.items = me.menu; me.callParent(); me.toolbar = new Ext.toolbar.Toolbar(Ext.apply({ dock: 'right', cls: 'ux-start-menu-toolbar', vertical: true, width: 100, layout: { align: 'stretch' } }, me.toolConfig)); me.addDocked(me.toolbar); delete me.toolItems; }, addMenuItem: function() { var cmp = this.menu; cmp.add.apply(cmp, arguments); }, addToolItem: function() { var cmp = this.toolbar; cmp.add.apply(cmp, arguments); } }); // StartMenu /*! * Ext JS Library * Copyright(c) 2006-2014 Sencha Inc. * licensing@sencha.com * http://www.sencha.com/license */ /** * @class Ext.ux.desktop.TaskBar * @extends Ext.toolbar.Toolbar */ Ext.define('Ext.ux.desktop.TaskBar', { // This must be a toolbar. we rely on acquired toolbar classes and inherited toolbar methods for our // child items to instantiate and render correctly. extend: 'Ext.toolbar.Toolbar', requires: [ 'Ext.button.Button', 'Ext.resizer.Splitter', 'Ext.menu.Menu', 'Ext.ux.desktop.StartMenu' ], alias: 'widget.taskbar', cls: 'ux-taskbar', /** * @cfg {String} startBtnText * The text for the Start Button. */ startBtnText: 'Start', initComponent: function() { var me = this; me.startMenu = new Ext.ux.desktop.StartMenu(me.startConfig); me.quickStart = new Ext.toolbar.Toolbar(me.getQuickStart()); me.windowBar = new Ext.toolbar.Toolbar(me.getWindowBarConfig()); me.tray = new Ext.toolbar.Toolbar(me.getTrayConfig()); me.items = [ { xtype: 'button', cls: 'ux-start-button', iconCls: 'ux-start-button-icon', menu: me.startMenu, menuAlign: 'bl-tl', text: me.startBtnText }, me.quickStart, { xtype: 'splitter', html: ' ', height: 14, width: 2, // TODO - there should be a CSS way here cls: 'x-toolbar-separator x-toolbar-separator-horizontal' }, me.windowBar, '-', me.tray ]; me.callParent(); }, afterLayout: function() { var me = this; me.callParent(); me.windowBar.el.on('contextmenu', me.onButtonContextMenu, me); }, /** * This method returns the configuration object for the Quick Start toolbar. A derived * class can override this method, call the base version to build the config and * then modify the returned object before returning it. */ getQuickStart: function() { var me = this, ret = { minWidth: 20, width: Ext.themeName === 'neptune' ? 70 : 60, items: [], enableOverflow: true }; Ext.each(this.quickStart, function(item) { ret.items.push({ tooltip: { text: item.name, align: 'bl-tl' }, //tooltip: item.name, overflowText: item.name, iconCls: item.iconCls, module: item.module, handler: me.onQuickStartClick, scope: me }); }); return ret; }, /** * This method returns the configuration object for the Tray toolbar. A derived * class can override this method, call the base version to build the config and * then modify the returned object before returning it. */ getTrayConfig: function() { var ret = { items: this.trayItems }; delete this.trayItems; return ret; }, getWindowBarConfig: function() { return { flex: 1, cls: 'ux-desktop-windowbar', items: [ ' ' ], layout: { overflowHandler: 'Scroller' } }; }, getWindowBtnFromEl: function(el) { var c = this.windowBar.getChildByElement(el); return c || null; }, onQuickStartClick: function(btn) { var module = this.app.getModule(btn.module), window; if (module) { window = module.createWindow(); window.show(); } }, onButtonContextMenu: function(e) { var me = this, t = e.getTarget(), btn = me.getWindowBtnFromEl(t); if (btn) { e.stopEvent(); me.windowMenu.theWin = btn.win; me.windowMenu.showBy(t); } }, onWindowBtnClick: function(btn) { var win = btn.win; if (win.minimized || win.hidden) { btn.disable(); win.show(null, function() { btn.enable(); }); } else if (win.active) { btn.disable(); win.on('hide', function() { btn.enable(); }, null, { single: true }); win.minimize(); } else { win.toFront(); } }, addTaskButton: function(win) { var config = { iconCls: win.iconCls, enableToggle: true, toggleGroup: 'all', width: 140, margin: '0 2 0 3', text: Ext.util.Format.ellipsis(win.title, 20), listeners: { click: this.onWindowBtnClick, scope: this }, win: win }; var cmp = this.windowBar.add(config); cmp.toggle(true); return cmp; }, removeTaskButton: function(btn) { var found, me = this; me.windowBar.items.each(function(item) { if (item === btn) { found = item; } return !found; }); if (found) { me.windowBar.remove(found); } return found; }, setActiveButton: function(btn) { if (btn) { btn.toggle(true); } else { this.windowBar.items.each(function(item) { if (item.isButton) { item.toggle(false); } }); } } }); /** * @class Ext.ux.desktop.TrayClock * @extends Ext.toolbar.TextItem * This class displays a clock on the toolbar. */ Ext.define('Ext.ux.desktop.TrayClock', { extend: 'Ext.toolbar.TextItem', alias: 'widget.trayclock', cls: 'ux-desktop-trayclock', html: ' ', timeFormat: 'g:i A', tpl: '{time}', initComponent: function() { var me = this; me.callParent(); if (typeof (me.tpl) == 'string') { me.tpl = new Ext.XTemplate(me.tpl); } }, afterRender: function() { var me = this; Ext.Function.defer(me.updateTime, 100, me); me.callParent(); }, onDestroy: function() { var me = this; if (me.timer) { window.clearTimeout(me.timer); me.timer = null; } me.callParent(); }, updateTime: function() { var me = this, time = Ext.Date.format(new Date(), me.timeFormat), text = me.tpl.apply({ time: time }); if (me.lastText != text) { me.setText(text); me.lastText = text; } me.timer = Ext.Function.defer(me.updateTime, 10000, me); } }); /*! * Ext JS Library * Copyright(c) 2006-2014 Sencha Inc. * licensing@sencha.com * http://www.sencha.com/license */ // From code originally written by David Davis (http://www.sencha.com/blog/html5-video-canvas-and-ext-js/) /* -NOTICE- * For HTML5 video to work, your server must * send the right content type, for more info see: * http://developer.mozilla.org/En/HTML/Element/Video */ Ext.define('Ext.ux.desktop.Video', { extend: 'Ext.panel.Panel', alias: 'widget.video', layout: 'fit', autoplay: false, controls: true, bodyStyle: 'background-color:#000;color:#fff', html: '', tpl: [ '' ], initComponent: function() { var me = this, fallback, size, cfg, el; if (me.fallbackHTML) { fallback = me.fallbackHTML; } else { fallback = "Your browser does not support HTML5 Video. "; if (Ext.isChrome) { fallback += 'Upgrade Chrome.'; } else if (Ext.isGecko) { fallback += 'Upgrade to Firefox 3.5 or newer.'; } else { var chrome = 'Chrome'; fallback += 'Please try Firefox'; if (Ext.isIE) { fallback += ', ' + chrome + ' or Safari.'; } else { fallback += ' or ' + chrome + '.'; } } } me.fallbackHTML = fallback; cfg = me.data = Ext.copyTo({ tag: 'video', html: fallback }, me, 'id,poster,start,loopstart,loopend,playcount,autobuffer,loop'); // just having the params exist enables them if (me.autoplay) { cfg.autoplay = 1; } if (me.controls) { cfg.controls = 1; } // handle multiple sources if (Ext.isArray(me.src)) { cfg.src = me.src; } else { cfg.src = [ { src: me.src } ]; } me.callParent(); }, afterRender: function() { var me = this; me.callParent(); me.video = me.body.getById(me.id + '-video'); el = me.video.dom; me.supported = (el && el.tagName.toLowerCase() == 'video'); if (me.supported) { me.video.on('error', me.onVideoError, me); } }, getFallback: function() { return '

' + this.fallbackHTML + '

'; }, onVideoError: function() { var me = this; me.video.remove(); me.supported = false; me.body.createChild(me.getFallback()); }, onDestroy: function() { var me = this; var video = me.video; if (me.supported && video) { var videoDom = video.dom; if (videoDom && videoDom.pause) { videoDom.pause(); } video.remove(); me.video = null; } me.callParent(); } }); /*! * Ext JS Library * Copyright(c) 2006-2014 Sencha Inc. * licensing@sencha.com * http://www.sencha.com/license */ /** * @class Ext.ux.desktop.Wallpaper * @extends Ext.Component *

This component renders an image that stretches to fill the component.

*/ Ext.define('Ext.ux.desktop.Wallpaper', { extend: 'Ext.Component', alias: 'widget.wallpaper', cls: 'ux-wallpaper', html: '', stretch: false, wallpaper: null, stateful: true, stateId: 'desk-wallpaper', afterRender: function() { var me = this; me.callParent(); me.setWallpaper(me.wallpaper, me.stretch); }, applyState: function() { var me = this, old = me.wallpaper; me.callParent(arguments); if (old != me.wallpaper) { me.setWallpaper(me.wallpaper); } }, getState: function() { return this.wallpaper && { wallpaper: this.wallpaper }; }, setWallpaper: function(wallpaper, stretch) { var me = this, imgEl, bkgnd; me.stretch = (stretch !== false); me.wallpaper = wallpaper; if (me.rendered) { imgEl = me.el.dom.firstChild; if (!wallpaper || wallpaper == Ext.BLANK_IMAGE_URL) { Ext.fly(imgEl).hide(); } else if (me.stretch) { imgEl.src = wallpaper; me.el.removeCls('ux-wallpaper-tiled'); Ext.fly(imgEl).setStyle({ width: '100%', height: '100%' }).show(); } else { Ext.fly(imgEl).hide(); bkgnd = 'url(' + wallpaper + ')'; me.el.addCls('ux-wallpaper-tiled'); } me.el.setStyle({ backgroundImage: bkgnd || '' }); if (me.stateful) { me.saveState(); } } return me; } }); /** * This is the base class for {@link Ext.ux.event.Recorder} and {@link Ext.ux.event.Player}. */ Ext.define('Ext.ux.event.Driver', { extend: 'Ext.util.Observable', active: null, specialKeysByName: { PGUP: 33, PGDN: 34, END: 35, HOME: 36, LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40 }, specialKeysByCode: {}, /** * @event start * Fires when this object is started. * @param {Ext.ux.event.Driver} this */ /** * @event stop * Fires when this object is stopped. * @param {Ext.ux.event.Driver} this */ getTextSelection: function(el) { // See https://code.google.com/p/rangyinputs/source/browse/trunk/rangyinputs_jquery.js var doc = el.ownerDocument, range, range2, start, end; if (typeof el.selectionStart === "number") { start = el.selectionStart; end = el.selectionEnd; } else if (doc.selection) { range = doc.selection.createRange(); range2 = el.createTextRange(); range2.setEndPoint('EndToStart', range); start = range2.text.length; end = start + range.text.length; } return [ start, end ]; }, getTime: function() { return new Date().getTime(); }, /** * Returns the number of milliseconds since start was called. */ getTimestamp: function() { var d = this.getTime(); return d - this.startTime; }, onStart: function() {}, onStop: function() {}, /** * Starts this object. If this object is already started, nothing happens. */ start: function() { var me = this; if (!me.active) { me.active = new Date(); me.startTime = me.getTime(); me.onStart(); me.fireEvent('start', me); } }, /** * Stops this object. If this object is not started, nothing happens. */ stop: function() { var me = this; if (me.active) { me.active = null; me.onStop(); me.fireEvent('stop', me); } } }, function() { var proto = this.prototype; Ext.Object.each(proto.specialKeysByName, function(name, value) { proto.specialKeysByCode[value] = name; }); }); /** * Event maker. */ Ext.define('Ext.ux.event.Maker', { eventQueue: [], startAfter: 500, timerIncrement: 500, currentTiming: 0, constructor: function(config) { var me = this; me.currentTiming = me.startAfter; if (!Ext.isArray(config)) { config = [ config ]; } Ext.Array.each(config, function(item) { item.el = item.el || 'el'; Ext.Array.each(Ext.ComponentQuery.query(item.cmpQuery), function(cmp) { var event = {}, x, y, el; if (!item.domQuery) { el = cmp[item.el]; } else { el = cmp.el.down(item.domQuery); } event.target = '#' + el.dom.id; event.type = item.type; event.button = config.button || 0; x = el.getX() + (el.getWidth() / 2); y = el.getY() + (el.getHeight() / 2); event.xy = [ x, y ]; event.ts = me.currentTiming; me.currentTiming += me.timerIncrement; me.eventQueue.push(event); }); if (item.screenshot) { me.eventQueue[me.eventQueue.length - 1].screenshot = true; } }); return me.eventQueue; } }); /** * @extends Ext.ux.event.Driver * This class manages the playback of an array of "event descriptors". For details on the * contents of an "event descriptor", see {@link Ext.ux.event.Recorder}. The events recorded by the * {@link Ext.ux.event.Recorder} class are designed to serve as input for this class. * * The simplest use of this class is to instantiate it with an {@link #eventQueue} and call * {@link #method-start}. Like so: * * var player = Ext.create('Ext.ux.event.Player', { * eventQueue: [ ... ], * speed: 2, // play at 2x speed * listeners: { * stop: function () { * player = null; // all done * } * } * }); * * player.start(); * * A more complex use would be to incorporate keyframe generation after playing certain * events. * * var player = Ext.create('Ext.ux.event.Player', { * eventQueue: [ ... ], * keyFrameEvents: { * click: true * }, * listeners: { * stop: function () { * // play has completed... probably time for another keyframe... * player = null; * }, * keyframe: onKeyFrame * } * }); * * player.start(); * * If a keyframe can be handled immediately (synchronously), the listener would be: * * function onKeyFrame () { * handleKeyFrame(); * } * * If the keyframe event is always handled asynchronously, then the event listener is only * a bit more: * * function onKeyFrame (p, eventDescriptor) { * eventDescriptor.defer(); // pause event playback... * * handleKeyFrame(function () { * eventDescriptor.finish(); // ...resume event playback * }); * } * * Finally, if the keyframe could be either handled synchronously or asynchronously (perhaps * differently by browser), a slightly more complex listener is required. * * function onKeyFrame (p, eventDescriptor) { * var async; * * handleKeyFrame(function () { * // either this callback is being called immediately by handleKeyFrame (in * // which case async is undefined) or it is being called later (in which case * // async will be true). * * if (async) { * eventDescriptor.finish(); * } else { * async = false; * } * }); * * // either the callback was called (and async is now false) or it was not * // called (and async remains undefined). * * if (async !== false) { * eventDescriptor.defer(); * async = true; // let the callback know that we have gone async * } * } */ Ext.define('Ext.ux.event.Player', function(Player) { var defaults = {}, mouseEvents = {}, keyEvents = {}, doc, //HTML events supported uiEvents = {}, //events that bubble by default bubbleEvents = { //scroll: 1, resize: 1, reset: 1, submit: 1, change: 1, select: 1, error: 1, abort: 1 }; Ext.each([ 'click', 'dblclick', 'mouseover', 'mouseout', 'mousedown', 'mouseup', 'mousemove' ], function(type) { bubbleEvents[type] = defaults[type] = mouseEvents[type] = { bubbles: true, cancelable: (type != "mousemove"), // mousemove cannot be cancelled detail: 1, screenX: 0, screenY: 0, clientX: 0, clientY: 0, ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, button: 0 }; }); Ext.each([ 'keydown', 'keyup', 'keypress' ], function(type) { bubbleEvents[type] = defaults[type] = keyEvents[type] = { bubbles: true, cancelable: true, ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, keyCode: 0, charCode: 0 }; }); Ext.each([ 'blur', 'change', 'focus', 'resize', 'scroll', 'select' ], function(type) { defaults[type] = uiEvents[type] = { bubbles: (type in bubbleEvents), cancelable: false, detail: 1 }; }); var inputSpecialKeys = { 8: function(target, start, end) { // backspace: 8, if (start < end) { target.value = target.value.substring(0, start) + target.value.substring(end); } else if (start > 0) { target.value = target.value.substring(0, --start) + target.value.substring(end); } this.setTextSelection(target, start, start); }, 46: function(target, start, end) { // delete: 46 if (start < end) { target.value = target.value.substring(0, start) + target.value.substring(end); } else if (start < target.value.length - 1) { target.value = target.value.substring(0, start) + target.value.substring(start + 1); } this.setTextSelection(target, start, start); } }; return { extend: 'Ext.ux.event.Driver', /** * @cfg {Array} eventQueue The event queue to playback. This must be provided before * the {@link #method-start} method is called. */ /** * @cfg {Object} keyFrameEvents An object that describes the events that should generate * keyframe events. For example, `{ click: true }` would generate keyframe events after * each `click` event. */ keyFrameEvents: { click: true }, /** * @cfg {Boolean} pauseForAnimations True to pause event playback during animations, false * to ignore animations. Default is true. */ pauseForAnimations: true, /** * @cfg {Number} speed The playback speed multiplier. Default is 1.0 (to playback at the * recorded speed). A value of 2 would playback at 2x speed. */ speed: 1, stallTime: 0, _inputSpecialKeys: { INPUT: inputSpecialKeys, TEXTAREA: Ext.apply({}, //13: function (target, start, end) { // enter: 8, //TODO ? //} inputSpecialKeys) }, tagPathRegEx: /(\w+)(?:\[(\d+)\])?/, /** * @event beforeplay * Fires before an event is played. * @param {Ext.ux.event.Player} this * @param {Object} eventDescriptor The event descriptor about to be played. */ /** * @event keyframe * Fires when this player reaches a keyframe. Typically, this is after events * like `click` are injected and any resulting animations have been completed. * @param {Ext.ux.event.Player} this * @param {Object} eventDescriptor The keyframe event descriptor. */ constructor: function(config) { var me = this; me.callParent(arguments); me.timerFn = function() { me.onTick(); }; me.attachTo = me.attachTo || window; doc = me.attachTo.document; }, /** * Returns the element given is XPath-like description. * @param {String} xpath The XPath-like description of the element. * @return {HTMLElement} */ getElementFromXPath: function(xpath) { var me = this, parts = xpath.split('/'), regex = me.tagPathRegEx, i, n, m, count, tag, child, el = me.attachTo.document; el = (parts[0] == '~') ? el.body : el.getElementById(parts[0].substring(1)); // remove '#' for (i = 1 , n = parts.length; el && i < n; ++i) { m = regex.exec(parts[i]); count = m[2] ? parseInt(m[2], 10) : 1; tag = m[1].toUpperCase(); for (child = el.firstChild; child; child = child.nextSibling) { if (child.tagName == tag) { if (count == 1) { break; } --count; } } el = child; } return el; }, // Moving across a line break only counts as moving one character in a TextRange, whereas a line break in // the textarea value is two characters. This function corrects for that by converting a text offset into a // range character offset by subtracting one character for every line break in the textarea prior to the // offset offsetToRangeCharacterMove: function(el, offset) { return offset - (el.value.slice(0, offset).split("\r\n").length - 1); }, setTextSelection: function(el, startOffset, endOffset) { // See https://code.google.com/p/rangyinputs/source/browse/trunk/rangyinputs_jquery.js if (startOffset < 0) { startOffset += el.value.length; } if (endOffset == null) { endOffset = startOffset; } if (endOffset < 0) { endOffset += el.value.length; } if (typeof el.selectionStart === "number") { el.selectionStart = startOffset; el.selectionEnd = endOffset; } else { var range = el.createTextRange(); var startCharMove = this.offsetToRangeCharacterMove(el, startOffset); range.collapse(true); if (startOffset == endOffset) { range.move("character", startCharMove); } else { range.moveEnd("character", this.offsetToRangeCharacterMove(el, endOffset)); range.moveStart("character", startCharMove); } range.select(); } }, getTimeIndex: function() { var t = this.getTimestamp() - this.stallTime; return t * this.speed; }, makeToken: function(eventDescriptor, signal) { var me = this, t0; eventDescriptor[signal] = true; eventDescriptor.defer = function() { eventDescriptor[signal] = false; t0 = me.getTime(); }; eventDescriptor.finish = function() { eventDescriptor[signal] = true; me.stallTime += me.getTime() - t0; me.schedule(); }; }, /** * This method is called after an event has been played to prepare for the next event. * @param {Object} eventDescriptor The descriptor of the event just played. */ nextEvent: function(eventDescriptor) { var me = this, index = ++me.queueIndex; // keyframe events are inserted after a keyFrameEvent is played. if (me.keyFrameEvents[eventDescriptor.type]) { Ext.Array.insert(me.eventQueue, index, [ { keyframe: true, ts: eventDescriptor.ts } ]); } }, /** * This method returns the event descriptor at the front of the queue. This does not * dequeue the event. Repeated calls return the same object (until {@link #nextEvent} * is called). */ peekEvent: function() { return this.eventQueue[this.queueIndex] || null; }, /** * Replaces an event in the queue with an array of events. This is often used to roll * up a multi-step pseudo-event and expand it just-in-time to be played. The process * for doing this in a derived class would be this: * * Ext.define('My.Player', { * extend: 'Ext.ux.event.Player', * * peekEvent: function () { * var event = this.callParent(); * * if (event.multiStepSpecial) { * this.replaceEvent(null, [ * ... expand to actual events * ]); * * event = this.callParent(); // get the new next event * } * * return event; * } * }); * * This method ensures that the `beforeplay` hook (if any) from the replaced event is * placed on the first new event and the `afterplay` hook (if any) is placed on the * last new event. * * @param {Number} index The queue index to replace. Pass `null` to replace the event * at the current `queueIndex`. * @param {Event[]} events The array of events with which to replace the specified * event. */ replaceEvent: function(index, events) { for (var t, i = 0, n = events.length; i < n; ++i) { if (i) { t = events[i - 1]; delete t.afterplay; delete t.screenshot; delete events[i].beforeplay; } } Ext.Array.replace(this.eventQueue, (index == null) ? this.queueIndex : index, 1, events); }, /** * This method dequeues and injects events until it has arrived at the time index. If * no events are ready (based on the time index), this method does nothing. * @return {Boolean} True if there is more to do; false if not (at least for now). */ processEvents: function() { var me = this, animations = me.pauseForAnimations && me.attachTo.Ext.fx.Manager.items, eventDescriptor; while ((eventDescriptor = me.peekEvent()) !== null) { if (animations && animations.getCount()) { return true; } if (eventDescriptor.keyframe) { if (!me.processKeyFrame(eventDescriptor)) { return false; } me.nextEvent(eventDescriptor); } else if (eventDescriptor.ts <= me.getTimeIndex() && me.fireEvent('beforeplay', me, eventDescriptor) !== false && me.playEvent(eventDescriptor)) { me.nextEvent(eventDescriptor); } else { return true; } } me.stop(); return false; }, /** * This method is called when a keyframe is reached. This will fire the keyframe event. * If the keyframe has been handled, true is returned. Otherwise, false is returned. * @param {Object} eventDescriptor The event descriptor of the keyframe. * @return {Boolean} True if the keyframe was handled, false if not. */ processKeyFrame: function(eventDescriptor) { var me = this; // only fire keyframe event (and setup the eventDescriptor) once... if (!eventDescriptor.defer) { me.makeToken(eventDescriptor, 'done'); me.fireEvent('keyframe', me, eventDescriptor); } return eventDescriptor.done; }, /** * Called to inject the given event on the specified target. * @param {HTMLElement} target The target of the event. * @param {Object} event The event to inject. The properties of this object should be * those of standard DOM events but vary based on the `type` property. For details on * event types and their properties, see the class documentation. */ injectEvent: function(target, event) { var me = this, type = event.type, options = Ext.apply({}, event, defaults[type]), handler; if (type === 'type') { handler = me._inputSpecialKeys[target.tagName]; if (handler) { return me.injectTypeInputEvent(target, event, handler); } return me.injectTypeEvent(target, event); } if (type === 'focus' && target.focus) { target.focus(); return true; } if (type === 'blur' && target.blur) { target.blur(); return true; } if (type === 'scroll') { target.scrollLeft = event.pos[0]; target.scrollTop = event.pos[1]; return true; } if (type === 'mduclick') { return me.injectEvent(target, Ext.applyIf({ type: 'mousedown' }, event)) && me.injectEvent(target, Ext.applyIf({ type: 'mouseup' }, event)) && me.injectEvent(target, Ext.applyIf({ type: 'click' }, event)); } if (mouseEvents[type]) { return Player.injectMouseEvent(target, options, me.attachTo); } if (keyEvents[type]) { return Player.injectKeyEvent(target, options, me.attachTo); } if (uiEvents[type]) { return Player.injectUIEvent(target, type, options.bubbles, options.cancelable, options.view || me.attachTo, options.detail); } return false; }, injectTypeEvent: function(target, event) { var me = this, text = event.text, xlat = [], ch, chUp, i, n, sel, upper, isInput; if (text) { delete event.text; upper = text.toUpperCase(); for (i = 0 , n = text.length; i < n; ++i) { ch = text.charCodeAt(i); chUp = upper.charCodeAt(i); xlat.push(Ext.applyIf({ type: 'keydown', charCode: chUp, keyCode: chUp }, event), Ext.applyIf({ type: 'keypress', charCode: ch, keyCode: ch }, event), Ext.applyIf({ type: 'keyup', charCode: chUp, keyCode: chUp }, event)); } } else { xlat.push(Ext.applyIf({ type: 'keydown', charCode: event.keyCode }, event), Ext.applyIf({ type: 'keyup', charCode: event.keyCode }, event)); } for (i = 0 , n = xlat.length; i < n; ++i) { me.injectEvent(target, xlat[i]); } return true; }, injectTypeInputEvent: function(target, event, handler) { var me = this, text = event.text, sel, n; if (handler) { sel = me.getTextSelection(target); if (text) { n = sel[0]; target.value = target.value.substring(0, n) + text + target.value.substring(sel[1]); n += text.length; me.setTextSelection(target, n, n); } else { if (!(handler = handler[event.keyCode])) { // no handler for the special key for this element if ('caret' in event) { me.setTextSelection(target, event.caret, event.caret); } else if (event.selection) { me.setTextSelection(target, event.selection[0], event.selection[1]); } return me.injectTypeEvent(target, event); } handler.call(this, target, sel[0], sel[1]); return true; } } return true; }, playEvent: function(eventDescriptor) { var me = this, target = me.getElementFromXPath(eventDescriptor.target), event; if (!target) { // not present (yet)... wait for element present... // TODO - need a timeout here return false; } if (!me.playEventHook(eventDescriptor, 'beforeplay')) { return false; } if (!eventDescriptor.injected) { eventDescriptor.injected = true; event = me.translateEvent(eventDescriptor, target); me.injectEvent(target, event); } return me.playEventHook(eventDescriptor, 'afterplay'); }, playEventHook: function(eventDescriptor, hookName) { var me = this, doneName = hookName + '.done', firedName = hookName + '.fired', hook = eventDescriptor[hookName]; if (hook && !eventDescriptor[doneName]) { if (!eventDescriptor[firedName]) { eventDescriptor[firedName] = true; me.makeToken(eventDescriptor, doneName); if (me.eventScope && Ext.isString(hook)) { hook = me.eventScope[hook]; } if (hook) { hook.call(me.eventScope || me, eventDescriptor); } } return false; } return true; }, schedule: function() { var me = this; if (!me.timer) { me.timer = setTimeout(me.timerFn, 10); } }, _translateAcross: [ 'type', 'button', 'charCode', 'keyCode', 'caret', 'pos', 'text', 'selection' ], translateEvent: function(eventDescriptor, target) { var me = this, event = {}, modKeys = eventDescriptor.modKeys || '', names = me._translateAcross, i = names.length, name, xy; while (i--) { name = names[i]; if (name in eventDescriptor) { event[name] = eventDescriptor[name]; } } event.altKey = modKeys.indexOf('A') > 0; event.ctrlKey = modKeys.indexOf('C') > 0; event.metaKey = modKeys.indexOf('M') > 0; event.shiftKey = modKeys.indexOf('S') > 0; if (target && 'x' in eventDescriptor) { xy = Ext.fly(target).getXY(); xy[0] += eventDescriptor.x; xy[1] += eventDescriptor.y; } else if ('x' in eventDescriptor) { xy = [ eventDescriptor.x, eventDescriptor.y ]; } else if ('px' in eventDescriptor) { xy = [ eventDescriptor.px, eventDescriptor.py ]; } if (xy) { event.clientX = event.screenX = xy[0]; event.clientY = event.screenY = xy[1]; } if (eventDescriptor.key) { event.keyCode = me.specialKeysByName[eventDescriptor.key]; } if (eventDescriptor.type === 'wheel') { if ('onwheel' in me.attachTo.document) { event.wheelX = eventDescriptor.dx; event.wheelY = eventDescriptor.dy; } else { event.type = 'mousewheel'; event.wheelDeltaX = -40 * eventDescriptor.dx; event.wheelDeltaY = event.wheelDelta = -40 * eventDescriptor.dy; } } return event; }, //--------------------------------- // Driver overrides onStart: function() { var me = this; me.queueIndex = 0; me.schedule(); }, onStop: function() { var me = this; if (me.timer) { clearTimeout(me.timer); me.timer = null; } }, //--------------------------------- onTick: function() { var me = this; me.timer = null; if (me.processEvents()) { me.schedule(); } }, statics: { ieButtonCodeMap: { 0: 1, 1: 4, 2: 2 }, /* * Injects a key event using the given event information to populate the event * object. * * **Note:** `keydown` causes Safari 2.x to crash. * * @param {HTMLElement} target The target of the given event. * @param {Object} options Object object containing all of the event injection * options. * @param {String} options.type The type of event to fire. This can be any one of * the following: `keyup`, `keydown` and `keypress`. * @param {Boolean} [options.bubbles=true] `tru` if the event can be bubbled up. * DOM Level 3 specifies that all key events bubble by default. * @param {Boolean} [options.cancelable=true] `true` if the event can be canceled * using `preventDefault`. DOM Level 3 specifies that all key events can be * cancelled. * @param {Boolean} [options.ctrlKey=false] `true` if one of the CTRL keys is * pressed while the event is firing. * @param {Boolean} [options.altKey=false] `true` if one of the ALT keys is * pressed while the event is firing. * @param {Boolean} [options.shiftKey=false] `true` if one of the SHIFT keys is * pressed while the event is firing. * @param {Boolean} [options.metaKey=false] `true` if one of the META keys is * pressed while the event is firing. * @param {int} [options.keyCode=0] The code for the key that is in use. * @param {int} [options.charCode=0] The Unicode code for the character associated * with the key being used. * @param {Window} [view=window] The view containing the target. This is typically * the window object. * @private */ injectKeyEvent: function(target, options, view) { var type = options.type, customEvent = null; if (type === 'textevent') { type = 'keypress'; } view = view || window; //check for DOM-compliant browsers first if (doc.createEvent) { try { customEvent = doc.createEvent("KeyEvents"); // Interesting problem: Firefox implemented a non-standard // version of initKeyEvent() based on DOM Level 2 specs. // Key event was removed from DOM Level 2 and re-introduced // in DOM Level 3 with a different interface. Firefox is the // only browser with any implementation of Key Events, so for // now, assume it's Firefox if the above line doesn't error. // @TODO: Decipher between Firefox's implementation and a correct one. customEvent.initKeyEvent(type, options.bubbles, options.cancelable, view, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.keyCode, options.charCode); } catch (ex) { // If it got here, that means key events aren't officially supported. // Safari/WebKit is a real problem now. WebKit 522 won't let you // set keyCode, charCode, or other properties if you use a // UIEvent, so we first must try to create a generic event. The // fun part is that this will throw an error on Safari 2.x. The // end result is that we need another try...catch statement just to // deal with this mess. try { //try to create generic event - will fail in Safari 2.x customEvent = doc.createEvent("Events"); } catch (uierror) { //the above failed, so create a UIEvent for Safari 2.x customEvent = doc.createEvent("UIEvents"); } finally { customEvent.initEvent(type, options.bubbles, options.cancelable); customEvent.view = view; customEvent.altKey = options.altKey; customEvent.ctrlKey = options.ctrlKey; customEvent.shiftKey = options.shiftKey; customEvent.metaKey = options.metaKey; customEvent.keyCode = options.keyCode; customEvent.charCode = options.charCode; } } target.dispatchEvent(customEvent); } else if (doc.createEventObject) { //IE customEvent = doc.createEventObject(); customEvent.bubbles = options.bubbles; customEvent.cancelable = options.cancelable; customEvent.view = view; customEvent.ctrlKey = options.ctrlKey; customEvent.altKey = options.altKey; customEvent.shiftKey = options.shiftKey; customEvent.metaKey = options.metaKey; // IE doesn't support charCode explicitly. CharCode should // take precedence over any keyCode value for accurate // representation. customEvent.keyCode = (options.charCode > 0) ? options.charCode : options.keyCode; target.fireEvent("on" + type, customEvent); } else { return false; } return true; }, /* * Injects a mouse event using the given event information to populate the event * object. * * @param {HTMLElement} target The target of the given event. * @param {Object} options Object object containing all of the event injection * options. * @param {String} options.type The type of event to fire. This can be any one of * the following: `click`, `dblclick`, `mousedown`, `mouseup`, `mouseout`, * `mouseover` and `mousemove`. * @param {Boolean} [options.bubbles=true] `tru` if the event can be bubbled up. * DOM Level 2 specifies that all mouse events bubble by default. * @param {Boolean} [options.cancelable=true] `true` if the event can be canceled * using `preventDefault`. DOM Level 2 specifies that all mouse events except * `mousemove` can be cancelled. This defaults to `false` for `mousemove`. * @param {Boolean} [options.ctrlKey=false] `true` if one of the CTRL keys is * pressed while the event is firing. * @param {Boolean} [options.altKey=false] `true` if one of the ALT keys is * pressed while the event is firing. * @param {Boolean} [options.shiftKey=false] `true` if one of the SHIFT keys is * pressed while the event is firing. * @param {Boolean} [options.metaKey=false] `true` if one of the META keys is * pressed while the event is firing. * @param {int} [options.detail=1] The number of times the mouse button has been * used. * @param {int} [options.screenX=0] The x-coordinate on the screen at which point * the event occurred. * @param {int} [options.screenY=0] The y-coordinate on the screen at which point * the event occurred. * @param {int} [options.clientX=0] The x-coordinate on the client at which point * the event occurred. * @param {int} [options.clientY=0] The y-coordinate on the client at which point * the event occurred. * @param {int} [options.button=0] The button being pressed while the event is * executing. The value should be 0 for the primary mouse button (typically the * left button), 1 for the tertiary mouse button (typically the middle button), * and 2 for the secondary mouse button (typically the right button). * @param {HTMLElement} [options.relatedTarget=null] For `mouseout` events, this * is the element that the mouse has moved to. For `mouseover` events, this is * the element that the mouse has moved from. This argument is ignored for all * other events. * @param {Window} [view=window] The view containing the target. This is typically * the window object. * @private */ injectMouseEvent: function(target, options, view) { var type = options.type, customEvent = null; view = view || window; //check for DOM-compliant browsers first if (doc.createEvent) { customEvent = doc.createEvent("MouseEvents"); //Safari 2.x (WebKit 418) still doesn't implement initMouseEvent() if (customEvent.initMouseEvent) { customEvent.initMouseEvent(type, options.bubbles, options.cancelable, view, options.detail, options.screenX, options.screenY, options.clientX, options.clientY, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, options.relatedTarget); } else { //Safari //the closest thing available in Safari 2.x is UIEvents customEvent = doc.createEvent("UIEvents"); customEvent.initEvent(type, options.bubbles, options.cancelable); customEvent.view = view; customEvent.detail = options.detail; customEvent.screenX = options.screenX; customEvent.screenY = options.screenY; customEvent.clientX = options.clientX; customEvent.clientY = options.clientY; customEvent.ctrlKey = options.ctrlKey; customEvent.altKey = options.altKey; customEvent.metaKey = options.metaKey; customEvent.shiftKey = options.shiftKey; customEvent.button = options.button; customEvent.relatedTarget = options.relatedTarget; } /* * Check to see if relatedTarget has been assigned. Firefox * versions less than 2.0 don't allow it to be assigned via * initMouseEvent() and the property is readonly after event * creation, so in order to keep YAHOO.util.getRelatedTarget() * working, assign to the IE proprietary toElement property * for mouseout event and fromElement property for mouseover * event. */ if (options.relatedTarget && !customEvent.relatedTarget) { if (type == "mouseout") { customEvent.toElement = options.relatedTarget; } else if (type == "mouseover") { customEvent.fromElement = options.relatedTarget; } } target.dispatchEvent(customEvent); } else if (doc.createEventObject) { //IE customEvent = doc.createEventObject(); customEvent.bubbles = options.bubbles; customEvent.cancelable = options.cancelable; customEvent.view = view; customEvent.detail = options.detail; customEvent.screenX = options.screenX; customEvent.screenY = options.screenY; customEvent.clientX = options.clientX; customEvent.clientY = options.clientY; customEvent.ctrlKey = options.ctrlKey; customEvent.altKey = options.altKey; customEvent.metaKey = options.metaKey; customEvent.shiftKey = options.shiftKey; customEvent.button = Player.ieButtonCodeMap[options.button] || 0; /* * Have to use relatedTarget because IE won't allow assignment * to toElement or fromElement on generic events. This keeps * YAHOO.util.customEvent.getRelatedTarget() functional. */ customEvent.relatedTarget = options.relatedTarget; target.fireEvent('on' + type, customEvent); } else { return false; } return true; }, /* * Injects a UI event using the given event information to populate the event * object. * * @param {HTMLElement} target The target of the given event. * @param {String} options.type The type of event to fire. This can be any one of * the following: `click`, `dblclick`, `mousedown`, `mouseup`, `mouseout`, * `mouseover` and `mousemove`. * @param {Boolean} [options.bubbles=true] `tru` if the event can be bubbled up. * DOM Level 2 specifies that all mouse events bubble by default. * @param {Boolean} [options.cancelable=true] `true` if the event can be canceled * using `preventDefault`. DOM Level 2 specifies that all mouse events except * `mousemove` can be canceled. This defaults to `false` for `mousemove`. * @param {int} [options.detail=1] The number of times the mouse button has been * used. * @param {Window} [view=window] The view containing the target. This is typically * the window object. * @private */ injectUIEvent: function(target, options, view) { var customEvent = null; view = view || window; //check for DOM-compliant browsers first if (doc.createEvent) { //just a generic UI Event object is needed customEvent = doc.createEvent("UIEvents"); customEvent.initUIEvent(options.type, options.bubbles, options.cancelable, view, options.detail); target.dispatchEvent(customEvent); } else if (doc.createEventObject) { //IE customEvent = doc.createEventObject(); customEvent.bubbles = options.bubbles; customEvent.cancelable = options.cancelable; customEvent.view = view; customEvent.detail = options.detail; target.fireEvent("on" + options.type, customEvent); } else { return false; } return true; } } }; }); // statics /** * @extends Ext.ux.event.Driver * Event recorder. */ Ext.define('Ext.ux.event.Recorder', function(Recorder) { function apply() { var a = arguments, n = a.length, obj = { kind: 'other' }, i; for (i = 0; i < n; ++i) { Ext.apply(obj, arguments[i]); } if (obj.alt && !obj.event) { obj.event = obj.alt; } return obj; } function key(extra) { return apply({ kind: 'keyboard', modKeys: true, key: true }, extra); } function mouse(extra) { return apply({ kind: 'mouse', button: true, modKeys: true, xy: true }, extra); } var eventsToRecord = { keydown: key(), keypress: key(), keyup: key(), dragmove: mouse({ alt: 'mousemove', pageCoords: true, whileDrag: true }), mousemove: mouse({ pageCoords: true }), mouseover: mouse(), mouseout: mouse(), click: mouse(), wheel: mouse({ wheel: true }), mousedown: mouse({ press: true }), mouseup: mouse({ release: true }), scroll: apply({ listen: false }), focus: apply(), blur: apply() }; for (var key in eventsToRecord) { if (!eventsToRecord[key].event) { eventsToRecord[key].event = key; } } eventsToRecord.wheel.event = null; // must detect later return { extend: 'Ext.ux.event.Driver', /** * @event add * Fires when an event is added to the recording. * @param {Ext.ux.event.Recorder} this * @param {Object} eventDescriptor The event descriptor. */ /** * @event coalesce * Fires when an event is coalesced. This edits the tail of the recorded * event list. * @param {Ext.ux.event.Recorder} this * @param {Object} eventDescriptor The event descriptor that was coalesced. */ eventsToRecord: eventsToRecord, ignoreIdRegEx: /ext-gen(?:\d+)/, inputRe: /^(input|textarea)$/i, constructor: function(config) { var me = this, events = config && config.eventsToRecord; if (events) { me.eventsToRecord = Ext.apply(Ext.apply({}, me.eventsToRecord), // duplicate events); // and merge delete config.eventsToRecord; } // don't smash me.callParent(arguments); me.clear(); me.modKeys = []; me.attachTo = me.attachTo || window; }, clear: function() { this.eventsRecorded = []; }, listenToEvent: function(event) { var me = this, el = me.attachTo.document.body, fn = function() { return me.onEvent.apply(me, arguments); }, cleaner = {}; if (el.attachEvent && el.ownerDocument.documentMode < 10) { event = 'on' + event; el.attachEvent(event, fn); cleaner.destroy = function() { if (fn) { el.detachEvent(event, fn); fn = null; } }; } else { el.addEventListener(event, fn, true); cleaner.destroy = function() { if (fn) { el.removeEventListener(event, fn, true); fn = null; } }; } return cleaner; }, coalesce: function(rec, ev) { var me = this, events = me.eventsRecorded, length = events.length, tail = length && events[length - 1], tail2 = (length > 1) && events[length - 2], tail3 = (length > 2) && events[length - 3]; if (!tail) { return false; } if (rec.type === 'mousemove') { if (tail.type === 'mousemove' && rec.ts - tail.ts < 200) { rec.ts = tail.ts; events[length - 1] = rec; return true; } } else if (rec.type === 'click') { if (tail2 && tail.type === 'mouseup' && tail2.type === 'mousedown') { if (rec.button == tail.button && rec.button == tail2.button && rec.target == tail.target && rec.target == tail2.target && me.samePt(rec, tail) && me.samePt(rec, tail2)) { events.pop(); // remove mouseup tail2.type = 'mduclick'; return true; } } } else if (rec.type === 'keyup') { // tail3 = { type: "type", text: "..." }, // tail2 = { type: "keydown", charCode: 65, keyCode: 65 }, // tail = { type: "keypress", charCode: 97, keyCode: 97 }, // rec = { type: "keyup", charCode: 65, keyCode: 65 }, if (tail2 && tail.type === 'keypress' && tail2.type === 'keydown') { if (rec.target === tail.target && rec.target === tail2.target) { events.pop(); // remove keypress tail2.type = 'type'; tail2.text = String.fromCharCode(tail.charCode); delete tail2.charCode; delete tail2.keyCode; if (tail3 && tail3.type === 'type') { if (tail3.text && tail3.target === tail2.target) { tail3.text += tail2.text; events.pop(); } } return true; } } // tail = { type: "keydown", charCode: 40, keyCode: 40 }, // rec = { type: "keyup", charCode: 40, keyCode: 40 }, else if (me.completeKeyStroke(tail, rec)) { tail.type = 'type'; me.completeSpecialKeyStroke(ev.target, tail, rec); return true; } // tail2 = { type: "keydown", charCode: 40, keyCode: 40 }, // tail = { type: "scroll", ... }, // rec = { type: "keyup", charCode: 40, keyCode: 40 }, else if (tail.type === 'scroll' && me.completeKeyStroke(tail2, rec)) { tail2.type = 'type'; me.completeSpecialKeyStroke(ev.target, tail2, rec); // swap the order of type and scroll events events.pop(); events.pop(); events.push(tail, tail2); return true; } } return false; }, completeKeyStroke: function(down, up) { if (down && down.type === 'keydown' && down.keyCode === up.keyCode) { delete down.charCode; return true; } return false; }, completeSpecialKeyStroke: function(target, down, up) { var key = this.specialKeysByCode[up.keyCode]; if (key && this.inputRe.test(target.tagName)) { // home,end,arrow keys + shift get crazy, so encode selection/caret delete down.keyCode; down.key = key; down.selection = this.getTextSelection(target); if (down.selection[0] === down.selection[1]) { down.caret = down.selection[0]; delete down.selection; } return true; } return false; }, getElementXPath: function(el) { var me = this, good = false, xpath = [], count, sibling, t, tag; for (t = el; t; t = t.parentNode) { if (t == me.attachTo.document.body) { xpath.unshift('~'); good = true; break; } if (t.id && !me.ignoreIdRegEx.test(t.id)) { xpath.unshift('#' + t.id); good = true; break; } for (count = 1 , sibling = t; !!(sibling = sibling.previousSibling); ) { if (sibling.tagName == t.tagName) { ++count; } } tag = t.tagName.toLowerCase(); if (count < 2) { xpath.unshift(tag); } else { xpath.unshift(tag + '[' + count + ']'); } } return good ? xpath.join('/') : null; }, getRecordedEvents: function() { return this.eventsRecorded; }, onEvent: function(ev) { var me = this, e = new Ext.event.Event(ev), info = me.eventsToRecord[e.type], root, modKeys, elXY, rec = { type: e.type, ts: me.getTimestamp(), target: me.getElementXPath(e.target) }, xy; if (!info || !rec.target) { return; } root = e.target.ownerDocument; root = root.defaultView || root.parentWindow; // Standards || IE if (root !== me.attachTo) { return; } if (me.eventsToRecord.scroll) { me.syncScroll(e.target); } if (info.xy) { xy = e.getXY(); if (info.pageCoords || !rec.target) { rec.px = xy[0]; rec.py = xy[1]; } else { elXY = Ext.fly(e.getTarget()).getXY(); xy[0] -= elXY[0]; xy[1] -= elXY[1]; rec.x = xy[0]; rec.y = xy[1]; } } if (info.button) { if ('buttons' in ev) { rec.button = ev.buttons; } else // LEFT=1, RIGHT=2, MIDDLE=4, etc. { rec.button = ev.button; } if (!rec.button && info.whileDrag) { return; } } if (info.wheel) { rec.type = 'wheel'; if (info.event === 'wheel') { // Current FireFox (technically IE9+ if we use addEventListener but // checking document.onwheel does not detect this) rec.dx = ev.deltaX; rec.dy = ev.deltaY; } else if (typeof ev.wheelDeltaX === 'number') { // new WebKit has both X & Y rec.dx = -1 / 40 * ev.wheelDeltaX; rec.dy = -1 / 40 * ev.wheelDeltaY; } else if (ev.wheelDelta) { // old WebKit and IE rec.dy = -1 / 40 * ev.wheelDelta; } else if (ev.detail) { // Old Gecko rec.dy = ev.detail; } } if (info.modKeys) { me.modKeys[0] = e.altKey ? 'A' : ''; me.modKeys[1] = e.ctrlKey ? 'C' : ''; me.modKeys[2] = e.metaKey ? 'M' : ''; me.modKeys[3] = e.shiftKey ? 'S' : ''; modKeys = me.modKeys.join(''); if (modKeys) { rec.modKeys = modKeys; } } if (info.key) { rec.charCode = e.getCharCode(); rec.keyCode = e.getKey(); } if (me.coalesce(rec, e)) { me.fireEvent('coalesce', me, rec); } else { me.eventsRecorded.push(rec); me.fireEvent('add', me, rec); } }, onStart: function() { var me = this, ddm = me.attachTo.Ext.dd.DragDropManager, evproto = me.attachTo.Ext.EventObjectImpl.prototype, special = []; // FireFox does not support the 'mousewheel' event but does support the // 'wheel' event instead. Recorder.prototype.eventsToRecord.wheel.event = ('onwheel' in me.attachTo.document) ? 'wheel' : 'mousewheel'; me.listeners = []; Ext.Object.each(me.eventsToRecord, function(name, value) { if (value && value.listen !== false) { if (!value.event) { value.event = name; } if (value.alt && value.alt !== name) { // The 'drag' event is just mousemove while buttons are pressed, // so if there is a mousemove entry as well, ignore the drag if (!me.eventsToRecord[value.alt]) { special.push(value); } } else { me.listeners.push(me.listenToEvent(value.event)); } } }); Ext.each(special, function(info) { me.eventsToRecord[info.alt] = info; me.listeners.push(me.listenToEvent(info.alt)); }); me.ddmStopEvent = ddm.stopEvent; ddm.stopEvent = Ext.Function.createSequence(ddm.stopEvent, function(e) { me.onEvent(e); }); me.evStopEvent = evproto.stopEvent; evproto.stopEvent = Ext.Function.createSequence(evproto.stopEvent, function() { me.onEvent(this); }); }, onStop: function() { var me = this; Ext.destroy(me.listeners); me.listeners = null; me.attachTo.Ext.dd.DragDropManager.stopEvent = me.ddmStopEvent; me.attachTo.Ext.EventObjectImpl.prototype.stopEvent = me.evStopEvent; }, samePt: function(pt1, pt2) { return pt1.x == pt2.x && pt1.y == pt2.y; }, syncScroll: function(el) { var me = this, ts = me.getTimestamp(), oldX, oldY, x, y, scrolled, rec; for (var p = el; p; p = p.parentNode) { oldX = p.$lastScrollLeft; oldY = p.$lastScrollTop; x = p.scrollLeft; y = p.scrollTop; scrolled = false; if (oldX !== x) { if (x) { scrolled = true; } p.$lastScrollLeft = x; } if (oldY !== y) { if (y) { scrolled = true; } p.$lastScrollTop = y; } if (scrolled) { //console.log('scroll x:' + x + ' y:' + y, p); me.eventsRecorded.push(rec = { type: 'scroll', target: me.getElementXPath(p), ts: ts, pos: [ x, y ] }); me.fireEvent('add', me, rec); } if (p.tagName === 'BODY') { break; } } } }; }); /** * Recorder manager. * Used as a bookmarklet: * * javascript:void(window.open("../ux/event/RecorderManager.html","recmgr")) */ Ext.define('Ext.ux.event.RecorderManager', { extend: 'Ext.panel.Panel', alias: 'widget.eventrecordermanager', uses: [ 'Ext.ux.event.Recorder', 'Ext.ux.event.Player' ], layout: 'fit', buttonAlign: 'left', eventsToIgnore: { mousemove: 1, mouseover: 1, mouseout: 1 }, bodyBorder: false, playSpeed: 1, initComponent: function() { var me = this; me.recorder = new Ext.ux.event.Recorder({ attachTo: me.attachTo, listeners: { add: me.updateEvents, coalesce: me.updateEvents, buffer: 200, scope: me } }); me.recorder.eventsToRecord = Ext.apply({}, me.recorder.eventsToRecord); function speed(text, value) { return { text: text, speed: value, group: 'speed', checked: value == me.playSpeed, handler: me.onPlaySpeed, scope: me }; } me.tbar = [ { text: 'Record', xtype: 'splitbutton', whenIdle: true, handler: me.onRecord, scope: me, menu: me.makeRecordButtonMenu() }, { text: 'Play', xtype: 'splitbutton', whenIdle: true, handler: me.onPlay, scope: me, menu: [ speed('Qarter Speed (0.25x)', 0.25), speed('Half Speed (0.5x)', 0.5), speed('3/4 Speed (0.75x)', 0.75), '-', speed('Recorded Speed (1x)', 1), speed('Double Speed (2x)', 2), speed('Quad Speed (4x)', 4), '-', speed('Full Speed', 1000) ] }, { text: 'Clear', whenIdle: true, handler: me.onClear, scope: me }, '->', { text: 'Stop', whenActive: true, disabled: true, handler: me.onStop, scope: me } ]; var events = me.attachTo && me.attachTo.testEvents; me.items = [ { xtype: 'textarea', itemId: 'eventView', fieldStyle: 'font-family: monospace', selectOnFocus: true, emptyText: 'Events go here!', value: events ? me.stringifyEvents(events) : '', scrollToBottom: function() { var inputEl = this.inputEl.dom; inputEl.scrollTop = inputEl.scrollHeight; } } ]; me.fbar = [ { xtype: 'tbtext', text: 'Attached To: ' + (me.attachTo && me.attachTo.location.href) } ]; me.callParent(); }, makeRecordButtonMenu: function() { var ret = [], subs = {}, eventsToRec = this.recorder.eventsToRecord, ignoredEvents = this.eventsToIgnore; Ext.Object.each(eventsToRec, function(name, value) { var sub = subs[value.kind]; if (!sub) { subs[value.kind] = sub = []; ret.push({ text: value.kind, menu: sub }); } sub.push({ text: name, checked: true, handler: function(menuItem) { if (menuItem.checked) { eventsToRec[name] = value; } else { delete eventsToRec[name]; } } }); if (ignoredEvents[name]) { sub[sub.length - 1].checked = false; Ext.Function.defer(function() { delete eventsToRec[name]; }, 1); } }); function less(lhs, rhs) { return (lhs.text < rhs.text) ? -1 : ((rhs.text < lhs.text) ? 1 : 0); } ret.sort(less); Ext.Array.each(ret, function(sub) { sub.menu.sort(less); }); return ret; }, getEventView: function() { return this.down('#eventView'); }, onClear: function() { var view = this.getEventView(); view.setValue(''); }, onPlay: function() { var me = this, view = me.getEventView(), events = view.getValue(); if (events) { events = Ext.decode(events); if (events.length) { me.player = Ext.create('Ext.ux.event.Player', { attachTo: window.opener, eventQueue: events, speed: me.playSpeed, listeners: { stop: me.onPlayStop, scope: me } }); me.player.start(); me.syncBtnUI(); } } }, onPlayStop: function() { this.player = null; this.syncBtnUI(); }, onPlaySpeed: function(menuitem) { this.playSpeed = menuitem.speed; }, onRecord: function() { this.recorder.start(); this.syncBtnUI(); }, onStop: function() { var me = this; if (me.player) { me.player.stop(); me.player = null; } else { me.recorder.stop(); } me.syncBtnUI(); me.updateEvents(); }, syncBtnUI: function() { var me = this, idle = !me.player && !me.recorder.active; Ext.each(me.query('[whenIdle]'), function(btn) { btn.setDisabled(!idle); }); Ext.each(me.query('[whenActive]'), function(btn) { btn.setDisabled(idle); }); var view = me.getEventView(); view.setReadOnly(!idle); }, stringifyEvents: function(events) { var line, lines = []; Ext.each(events, function(ev) { line = []; Ext.Object.each(ev, function(name, value) { if (line.length) { line.push(', '); } else { line.push(' { '); } line.push(name, ': '); line.push(Ext.encode(value)); }); line.push(' }'); lines.push(line.join('')); }); return '[\n' + lines.join(',\n') + '\n]'; }, updateEvents: function() { var me = this, text = me.stringifyEvents(me.recorder.getRecordedEvents()), view = me.getEventView(); view.setValue(text); view.scrollToBottom(); } }); /** * A control that allows selection of multiple items in a list. */ Ext.define('Ext.ux.form.MultiSelect', { extend: 'Ext.form.FieldContainer', mixins: [ 'Ext.util.StoreHolder', 'Ext.form.field.Field' ], alternateClassName: 'Ext.ux.Multiselect', alias: [ 'widget.multiselectfield', 'widget.multiselect' ], requires: [ 'Ext.panel.Panel', 'Ext.view.BoundList', 'Ext.layout.container.Fit' ], uses: [ 'Ext.view.DragZone', 'Ext.view.DropZone' ], layout: 'anchor', /** * @cfg {String} [dragGroup=""] The ddgroup name for the MultiSelect DragZone. */ /** * @cfg {String} [dropGroup=""] The ddgroup name for the MultiSelect DropZone. */ /** * @cfg {String} [title=""] A title for the underlying panel. */ /** * @cfg {Boolean} [ddReorder=false] Whether the items in the MultiSelect list are drag/drop reorderable. */ ddReorder: false, /** * @cfg {Object/Array} tbar An optional toolbar to be inserted at the top of the control's selection list. * This can be a {@link Ext.toolbar.Toolbar} object, a toolbar config, or an array of buttons/button configs * to be added to the toolbar. See {@link Ext.panel.Panel#tbar}. */ /** * @cfg {String} [appendOnly=false] `true` if the list should only allow append drops when drag/drop is enabled. * This is useful for lists which are sorted. */ appendOnly: false, /** * @cfg {String} [displayField="text"] Name of the desired display field in the dataset. */ displayField: 'text', /** * @cfg {String} [valueField="text"] Name of the desired value field in the dataset. */ /** * @cfg {Boolean} [allowBlank=true] `false` to require at least one item in the list to be selected, `true` to allow no * selection. */ allowBlank: true, /** * @cfg {Number} [minSelections=0] Minimum number of selections allowed. */ minSelections: 0, /** * @cfg {Number} [maxSelections=Number.MAX_VALUE] Maximum number of selections allowed. */ maxSelections: Number.MAX_VALUE, /** * @cfg {String} [blankText="This field is required"] Default text displayed when the control contains no items. */ blankText: 'This field is required', /** * @cfg {String} [minSelectionsText="Minimum {0}item(s) required"] * Validation message displayed when {@link #minSelections} is not met. * The {0} token will be replaced by the value of {@link #minSelections}. */ minSelectionsText: 'Minimum {0} item(s) required', /** * @cfg {String} [maxSelectionsText="Maximum {0}item(s) allowed"] * Validation message displayed when {@link #maxSelections} is not met * The {0} token will be replaced by the value of {@link #maxSelections}. */ maxSelectionsText: 'Maximum {0} item(s) required', /** * @cfg {String} [delimiter=","] The string used to delimit the selected values when {@link #getSubmitValue submitting} * the field as part of a form. If you wish to have the selected values submitted as separate * parameters rather than a single delimited parameter, set this to `null`. */ delimiter: ',', /** * @cfg {String} [dragText="{0} Item{1}"] The text to show while dragging items. * {0} will be replaced by the number of items. {1} will be replaced by the plural * form if there is more than 1 item. */ dragText: '{0} Item{1}', /** * @cfg {Ext.data.Store/Array} store The data source to which this MultiSelect is bound (defaults to `undefined`). * Acceptable values for this property are: *
*/ ignoreSelectChange: 0, /** * @cfg {Object} listConfig * An optional set of configuration properties that will be passed to the {@link Ext.view.BoundList}'s constructor. * Any configuration that is valid for BoundList can be included. */ initComponent: function() { var me = this; me.items = me.setupItems(); me.bindStore(me.store, true); if (me.store.autoCreated) { me.valueField = me.displayField = 'field1'; if (!me.store.expanded) { me.displayField = 'field2'; } } if (!Ext.isDefined(me.valueField)) { me.valueField = me.displayField; } me.callParent(); me.initField(); }, setupItems: function() { var me = this; me.boundList = Ext.create('Ext.view.BoundList', Ext.apply({ anchor: 'none 100%', border: 1, multiSelect: true, store: me.store, displayField: me.displayField, disabled: me.disabled }, me.listConfig)); me.boundList.getSelectionModel().on('selectionchange', me.onSelectChange, me); // Boundlist expects a reference to its pickerField for when an item is selected (see Boundlist#onItemClick). me.boundList.pickerField = me; // Only need to wrap the BoundList in a Panel if we have a title. if (!me.title) { return me.boundList; } // Wrap to add a title me.boundList.border = false; return { border: true, anchor: 'none 100%', layout: 'anchor', title: me.title, tbar: me.tbar, items: me.boundList }; }, onSelectChange: function(selModel, selections) { if (!this.ignoreSelectChange) { this.setValue(selections); } }, getSelected: function() { return this.boundList.getSelectionModel().getSelection(); }, // compare array values isEqual: function(v1, v2) { var fromArray = Ext.Array.from, i = 0, len; v1 = fromArray(v1); v2 = fromArray(v2); len = v1.length; if (len !== v2.length) { return false; } for (; i < len; i++) { if (v2[i] !== v1[i]) { return false; } } return true; }, afterRender: function() { var me = this, records; me.callParent(); if (me.selectOnRender) { records = me.getRecordsForValue(me.value); if (records.length) { ++me.ignoreSelectChange; me.boundList.getSelectionModel().select(records); --me.ignoreSelectChange; } delete me.toSelect; } if (me.ddReorder && !me.dragGroup && !me.dropGroup) { me.dragGroup = me.dropGroup = 'MultiselectDD-' + Ext.id(); } if (me.draggable || me.dragGroup) { me.dragZone = Ext.create('Ext.view.DragZone', { view: me.boundList, ddGroup: me.dragGroup, dragText: me.dragText }); } if (me.droppable || me.dropGroup) { me.dropZone = Ext.create('Ext.view.DropZone', { view: me.boundList, ddGroup: me.dropGroup, handleNodeDrop: function(data, dropRecord, position) { var view = this.view, store = view.getStore(), records = data.records, index; // remove the Models from the source Store data.view.store.remove(records); index = store.indexOf(dropRecord); if (position === 'after') { index++; } store.insert(index, records); view.getSelectionModel().select(records); me.fireEvent('drop', me, records); } }); } }, isValid: function() { var me = this, disabled = me.disabled, validate = me.forceValidation || !disabled; return validate ? me.validateValue(me.value) : disabled; }, validateValue: function(value) { var me = this, errors = me.getErrors(value), isValid = Ext.isEmpty(errors); if (!me.preventMark) { if (isValid) { me.clearInvalid(); } else { me.markInvalid(errors); } } return isValid; }, markInvalid: function(errors) { // Save the message and fire the 'invalid' event var me = this, oldMsg = me.getActiveError(); me.setActiveErrors(Ext.Array.from(errors)); if (oldMsg !== me.getActiveError()) { me.updateLayout(); } }, /** * Clear any invalid styles/messages for this field. * * __Note:__ this method does not cause the Field's {@link #validate} or {@link #isValid} methods to return `true` * if the value does not _pass_ validation. So simply clearing a field's errors will not necessarily allow * submission of forms submitted with the {@link Ext.form.action.Submit#clientValidation} option set. */ clearInvalid: function() { // Clear the message and fire the 'valid' event var me = this, hadError = me.hasActiveError(); me.unsetActiveError(); if (hadError) { me.updateLayout(); } }, getSubmitData: function() { var me = this, data = null, val; if (!me.disabled && me.submitValue && !me.isFileUpload()) { val = me.getSubmitValue(); if (val !== null) { data = {}; data[me.getName()] = val; } } return data; }, /** * Returns the value that would be included in a standard form submit for this field. * * @return {String} The value to be submitted, or `null`. */ getSubmitValue: function() { var me = this, delimiter = me.delimiter, val = me.getValue(); return Ext.isString(delimiter) ? val.join(delimiter) : val; }, getValue: function() { return this.value || []; }, getRecordsForValue: function(value) { var me = this, records = [], all = me.store.getRange(), valueField = me.valueField, i = 0, allLen = all.length, rec, j, valueLen; for (valueLen = value.length; i < valueLen; ++i) { for (j = 0; j < allLen; ++j) { rec = all[j]; if (rec.get(valueField) == value[i]) { records.push(rec); } } } return records; }, setupValue: function(value) { var delimiter = this.delimiter, valueField = this.valueField, i = 0, out, len, item; if (Ext.isDefined(value)) { if (delimiter && Ext.isString(value)) { value = value.split(delimiter); } else if (!Ext.isArray(value)) { value = [ value ]; } for (len = value.length; i < len; ++i) { item = value[i]; if (item && item.isModel) { value[i] = item.get(valueField); } } out = Ext.Array.unique(value); } else { out = []; } return out; }, setValue: function(value) { var me = this, selModel = me.boundList.getSelectionModel(), store = me.store; // Store not loaded yet - we cannot set the value if (!store.getCount()) { store.on({ load: Ext.Function.bind(me.setValue, me, [ value ]), single: true }); return; } value = me.setupValue(value); me.mixins.field.setValue.call(me, value); if (me.rendered) { ++me.ignoreSelectChange; selModel.deselectAll(); if (value.length) { selModel.select(me.getRecordsForValue(value)); } --me.ignoreSelectChange; } else { me.selectOnRender = true; } }, clearValue: function() { this.setValue([]); }, onEnable: function() { var list = this.boundList; this.callParent(); if (list) { list.enable(); } }, onDisable: function() { var list = this.boundList; this.callParent(); if (list) { list.disable(); } }, getErrors: function(value) { var me = this, format = Ext.String.format, errors = [], numSelected; value = Ext.Array.from(value || me.getValue()); numSelected = value.length; if (!me.allowBlank && numSelected < 1) { errors.push(me.blankText); } if (numSelected < me.minSelections) { errors.push(format(me.minSelectionsText, me.minSelections)); } if (numSelected > me.maxSelections) { errors.push(format(me.maxSelectionsText, me.maxSelections)); } return errors; }, onDestroy: function() { var me = this; me.bindStore(null); Ext.destroy(me.dragZone, me.dropZone); me.callParent(); }, onBindStore: function(store) { var boundList = this.boundList; if (boundList) { boundList.bindStore(store); } } }); /** */ Ext.define('Ext.aria.ux.form.MultiSelect', { override: 'Ext.ux.form.MultiSelect', requires: [ 'Ext.view.BoundListKeyNav' ], /** * @cfg {Number} [pageSize=10] The number of items to advance on pageUp and pageDown */ pageSize: 10, afterRender: function() { var me = this, boundList = me.boundList; me.callParent(); if (boundList) { boundList.pageSize = me.pageSize; me.keyNav = new Ext.view.BoundListKeyNav(boundList.el, { boundList: boundList, // The View takes care of these up: Ext.emptyFn, down: Ext.emptyFn, pageUp: function() { var me = this, boundList = me.boundList, store = boundList.getStore(), selModel = boundList.getSelectionModel(), pageSize = boundList.pageSize, selection, oldItemIdx, newItemIdx; selection = selModel.getSelection()[0]; oldItemIdx = selection ? store.indexOf(selection) : -1; newItemIdx = oldItemIdx < 0 ? 0 : oldItemIdx - pageSize; selModel.select(newItemIdx < 0 ? 0 : newItemIdx); }, pageDown: function() { var me = this, boundList = me.boundList, pageSize = boundList.pageSize, store = boundList.store, selModel = boundList.getSelectionModel(), selection, oldItemIdx, newItemIdx, lastIdx; selection = selModel.getSelection()[0]; lastIdx = store.getCount() - 1; oldItemIdx = selection ? store.indexOf(selection) : -1; newItemIdx = oldItemIdx < 0 ? pageSize : oldItemIdx + pageSize; selModel.select(newItemIdx > lastIdx ? lastIdx : newItemIdx); }, home: function() { this.boundList.getSelectionModel().select(0); }, end: function() { var boundList = this.boundList; boundList.getSelectionModel().select(boundList.store.getCount() - 1); } }); } }, destroy: function() { var me = this, keyNav = me.keyNav; if (keyNav) { keyNav.destroy(); } me.callParent(); } }); /* * Note that this control will most likely remain as an example, and not as a core Ext form * control. However, the API will be changing in a future release and so should not yet be * treated as a final, stable API at this time. */ /** * A control that allows selection of between two Ext.ux.form.MultiSelect controls. */ Ext.define('Ext.ux.form.ItemSelector', { extend: 'Ext.ux.form.MultiSelect', alias: [ 'widget.itemselectorfield', 'widget.itemselector' ], alternateClassName: [ 'Ext.ux.ItemSelector' ], requires: [ 'Ext.button.Button', 'Ext.ux.form.MultiSelect' ], /** * @cfg {Boolean} [hideNavIcons=false] True to hide the navigation icons */ hideNavIcons: false, /** * @cfg {Array} buttons Defines the set of buttons that should be displayed in between the ItemSelector * fields. Defaults to ['top', 'up', 'add', 'remove', 'down', 'bottom']. These names are used * to build the button CSS class names, and to look up the button text labels in {@link #buttonsText}. * This can be overridden with a custom Array to change which buttons are displayed or their order. */ buttons: [ 'top', 'up', 'add', 'remove', 'down', 'bottom' ], /** * @cfg {Object} buttonsText The tooltips for the {@link #buttons}. * Labels for buttons. */ buttonsText: { top: "Move to Top", up: "Move Up", add: "Add to Selected", remove: "Remove from Selected", down: "Move Down", bottom: "Move to Bottom" }, layout: { type: 'hbox', align: 'stretch' }, initComponent: function() { var me = this; me.ddGroup = me.id + '-dd'; me.callParent(); // bindStore must be called after the fromField has been created because // it copies records from our configured Store into the fromField's Store me.bindStore(me.store); }, createList: function(title) { var me = this; return Ext.create('Ext.ux.form.MultiSelect', { // We don't want the multiselects themselves to act like fields, // so override these methods to prevent them from including // any of their values submitValue: false, getSubmitData: function() { return null; }, getModelData: function() { return null; }, flex: 1, dragGroup: me.ddGroup, dropGroup: me.ddGroup, title: title, store: { model: me.store.model, data: [] }, displayField: me.displayField, valueField: me.valueField, disabled: me.disabled, listeners: { boundList: { scope: me, itemdblclick: me.onItemDblClick, drop: me.syncValue } } }); }, setupItems: function() { var me = this; me.fromField = me.createList(me.fromTitle); me.toField = me.createList(me.toTitle); return [ me.fromField, { xtype: 'container', margin: '0 4', layout: { type: 'vbox', pack: 'center' }, items: me.createButtons() }, me.toField ]; }, createButtons: function() { var me = this, buttons = []; if (!me.hideNavIcons) { Ext.Array.forEach(me.buttons, function(name) { buttons.push({ xtype: 'button', tooltip: me.buttonsText[name], handler: me['on' + Ext.String.capitalize(name) + 'BtnClick'], cls: Ext.baseCSSPrefix + 'form-itemselector-btn', iconCls: Ext.baseCSSPrefix + 'form-itemselector-' + name, navBtn: true, scope: me, margin: '4 0 0 0' }); }); } return buttons; }, /** * Get the selected records from the specified list. * * Records will be returned *in store order*, not in order of selection. * @param {Ext.view.BoundList} list The list to read selections from. * @return {Ext.data.Model[]} The selected records in store order. * */ getSelections: function(list) { var store = list.getStore(); return Ext.Array.sort(list.getSelectionModel().getSelection(), function(a, b) { a = store.indexOf(a); b = store.indexOf(b); if (a < b) { return -1; } else if (a > b) { return 1; } return 0; }); }, onTopBtnClick: function() { var list = this.toField.boundList, store = list.getStore(), selected = this.getSelections(list); store.suspendEvents(); store.remove(selected, true); store.insert(0, selected); store.resumeEvents(); list.refresh(); this.syncValue(); list.getSelectionModel().select(selected); }, onBottomBtnClick: function() { var list = this.toField.boundList, store = list.getStore(), selected = this.getSelections(list); store.suspendEvents(); store.remove(selected, true); store.add(selected); store.resumeEvents(); list.refresh(); this.syncValue(); list.getSelectionModel().select(selected); }, onUpBtnClick: function() { var list = this.toField.boundList, store = list.getStore(), selected = this.getSelections(list), rec, i = 0, len = selected.length, index = 0; // Move each selection up by one place if possible store.suspendEvents(); for (; i < len; ++i , index++) { rec = selected[i]; index = Math.max(index, store.indexOf(rec) - 1); store.remove(rec, true); store.insert(index, rec); } store.resumeEvents(); list.refresh(); this.syncValue(); list.getSelectionModel().select(selected); }, onDownBtnClick: function() { var list = this.toField.boundList, store = list.getStore(), selected = this.getSelections(list), rec, i = selected.length - 1, index = store.getCount() - 1; // Move each selection down by one place if possible store.suspendEvents(); for (; i > -1; --i , index--) { rec = selected[i]; index = Math.min(index, store.indexOf(rec) + 1); store.remove(rec, true); store.insert(index, rec); } store.resumeEvents(); list.refresh(); this.syncValue(); list.getSelectionModel().select(selected); }, onAddBtnClick: function() { var me = this, selected = me.getSelections(me.fromField.boundList); me.moveRec(true, selected); me.toField.boundList.getSelectionModel().select(selected); }, onRemoveBtnClick: function() { var me = this, selected = me.getSelections(me.toField.boundList); me.moveRec(false, selected); me.fromField.boundList.getSelectionModel().select(selected); }, moveRec: function(add, recs) { var me = this, fromField = me.fromField, toField = me.toField, fromStore = add ? fromField.store : toField.store, toStore = add ? toField.store : fromField.store; fromStore.suspendEvents(); toStore.suspendEvents(); fromStore.remove(recs); toStore.add(recs); fromStore.resumeEvents(); toStore.resumeEvents(); fromField.boundList.refresh(); toField.boundList.refresh(); me.syncValue(); }, // Synchronizes the submit value with the current state of the toStore syncValue: function() { var me = this; me.mixins.field.setValue.call(me, me.setupValue(me.toField.store.getRange())); }, onItemDblClick: function(view, rec) { this.moveRec(view === this.fromField.boundList, rec); }, setValue: function(value) { var me = this, fromField = me.fromField, toField = me.toField, fromStore = fromField.store, toStore = toField.store, selected; // Wait for from store to be loaded if (!me.fromStorePopulated) { me.fromField.store.on({ load: Ext.Function.bind(me.setValue, me, [ value ]), single: true }); return; } value = me.setupValue(value); me.mixins.field.setValue.call(me, value); selected = me.getRecordsForValue(value); // Clear both left and right Stores. // Both stores must not fire events during this process. fromStore.suspendEvents(); toStore.suspendEvents(); fromStore.removeAll(); toStore.removeAll(); // Reset fromStore me.populateFromStore(me.store); // Copy selection across to toStore Ext.Array.forEach(selected, function(rec) { // In the from store, move it over if (fromStore.indexOf(rec) > -1) { fromStore.remove(rec); } toStore.add(rec); }); // Stores may now fire events fromStore.resumeEvents(); toStore.resumeEvents(); // Refresh both sides and then update the app layout Ext.suspendLayouts(); fromField.boundList.refresh(); toField.boundList.refresh(); Ext.resumeLayouts(true); }, onBindStore: function(store, initial) { var me = this; if (me.fromField) { me.fromField.store.removeAll(); me.toField.store.removeAll(); // Add everything to the from field as soon as the Store is loaded if (store.getCount()) { me.populateFromStore(store); } else { me.store.on('load', me.populateFromStore, me); } } }, populateFromStore: function(store) { var fromStore = this.fromField.store; // Flag set when the fromStore has been loaded this.fromStorePopulated = true; fromStore.add(store.getRange()); // setValue waits for the from Store to be loaded fromStore.fireEvent('load', fromStore); }, onEnable: function() { var me = this; me.callParent(); me.fromField.enable(); me.toField.enable(); Ext.Array.forEach(me.query('[navBtn]'), function(btn) { btn.enable(); }); }, onDisable: function() { var me = this; me.callParent(); me.fromField.disable(); me.toField.disable(); Ext.Array.forEach(me.query('[navBtn]'), function(btn) { btn.disable(); }); }, onDestroy: function() { this.bindStore(null); this.callParent(); } }); Ext.define('Ext.ux.form.SearchField', { extend: 'Ext.form.field.Text', alias: 'widget.searchfield', triggers: { clear: { weight: 0, cls: Ext.baseCSSPrefix + 'form-clear-trigger', hidden: true, handler: 'onClearClick', scope: 'this' }, search: { weight: 1, cls: Ext.baseCSSPrefix + 'form-search-trigger', handler: 'onSearchClick', scope: 'this' } }, hasSearch: false, paramName: 'query', initComponent: function() { var me = this, store = me.store, proxy; me.callParent(arguments); me.on('specialkey', function(f, e) { if (e.getKey() == e.ENTER) { me.onSearchClick(); } }); if (!store || !store.isStore) { store = me.store = Ext.data.StoreManager.lookup(store); } // We're going to use filtering store.setRemoteFilter(true); // Set up the proxy to encode the filter in the simplest way as a name/value pair proxy = me.store.getProxy(); proxy.setFilterParam(me.paramName); proxy.encodeFilters = function(filters) { return filters[0].getValue(); }; }, onClearClick: function() { var me = this, activeFilter = me.activeFilter; if (activeFilter) { me.setValue(''); me.store.getFilters().remove(activeFilter); me.activeFilter = null; me.getTrigger('clear').hide(); me.updateLayout(); } }, onSearchClick: function() { var me = this, value = me.getValue(); if (value.length > 0) { // Param name is ignored here since we use custom encoding in the proxy. // id is used by the Store to replace any previous filter me.activeFilter = new Ext.util.Filter({ property: me.paramName, value: value }); me.store.getFilters().add(me.activeFilter); me.getTrigger('clear').show(); me.updateLayout(); } } }); /** * A small grid nested within a parent grid's row. * * See the [Kitchen Sink](http://dev.sencha.com/extjs/5.0.1/examples/kitchensink/#customer-grid) for example usage. */ Ext.define('Ext.ux.grid.SubTable', { extend: 'Ext.grid.plugin.RowExpander', alias: 'plugin.subtable', rowBodyTpl: [ '', '{%', 'this.owner.renderTable(out, values);', '%}', '
' ], init: function(grid) { var me = this, columns = me.columns, len, i, columnCfg; me.callParent(arguments); me.columns = []; if (columns) { for (i = 0 , len = columns.length; i < len; ++i) { // Don't register with the component manager, we create them to use // their rendering smarts, but don't want to treat them as real components columnCfg = Ext.apply({ preventRegister: true }, columns[i]); columnCfg.xtype = columnCfg.xtype || 'gridcolumn'; me.columns.push(Ext.widget(columnCfg)); } } }, destroy: function() { var columns = this.columns, len, i; if (columns) { for (i = 0 , len = columns.length; i < len; ++i) { columns[i].destroy(); } } this.columns = null; this.callParent(); }, getRowBodyFeatureData: function(record, idx, rowValues) { this.callParent(arguments); rowValues.rowBodyCls += ' ' + Ext.baseCSSPrefix + 'grid-subtable-row'; }, renderTable: function(out, rowValues) { var me = this, columns = me.columns, numColumns = columns.length, associatedRecords = me.getAssociatedRecords(rowValues.record), recCount = associatedRecords.length, rec, column, i, j, value; out.push(''); for (j = 0; j < numColumns; j++) { out.push('', columns[j].text, ''); } out.push(''); for (i = 0; i < recCount; i++) { rec = associatedRecords[i]; out.push(''); for (j = 0; j < numColumns; j++) { column = columns[j]; value = rec.get(column.dataIndex); if (column.renderer && column.renderer.call) { value = column.renderer.call(column.scope || me, value, {}, rec); } out.push('
', value, '
'); } out.push(''); } }, getRowBodyContentsFn: function(rowBodyTpl) { var me = this; return function(rowValues) { rowBodyTpl.owner = me; return rowBodyTpl.applyTemplate(rowValues); }; }, getAssociatedRecords: function(record) { return record[this.association]().getRange(); } }); /** * A Grid which creates itself from an existing HTML table element. */ Ext.define('Ext.ux.grid.TransformGrid', { extend: 'Ext.grid.Panel', /** * Creates the grid from HTML table element. * @param {String/HTMLElement/Ext.Element} table The table element from which this grid will be created - * The table MUST have some type of size defined for the grid to fill. The container will be * automatically set to position relative if it isn't already. * @param {Object} [config] A config object that sets properties on this grid and has two additional (optional) * properties: fields and columns which allow for customizing data fields and columns for this grid. */ constructor: function(table, config) { config = Ext.apply({}, config); table = this.table = Ext.get(table); var configFields = config.fields || [], configColumns = config.columns || [], fields = [], cols = [], headers = table.query("thead th"), i = 0, len = headers.length, data = table.dom, width, height, store, col, text, name; for (; i < len; ++i) { col = headers[i]; text = col.innerHTML; name = 'tcol-' + i; fields.push(Ext.applyIf(configFields[i] || {}, { name: name, mapping: 'td:nth(' + (i + 1) + ')/@innerHTML' })); cols.push(Ext.applyIf(configColumns[i] || {}, { text: text, dataIndex: name, width: col.offsetWidth, tooltip: col.title, sortable: true })); } if (config.width) { width = config.width; } else { width = table.getWidth() + 1; } if (config.height) { height = config.height; } Ext.applyIf(config, { store: { data: data, fields: fields, proxy: { type: 'memory', reader: { record: 'tbody tr', type: 'xml' } } }, columns: cols, width: width, height: height }); this.callParent([ config ]); if (config.remove !== false) { // Don't use table.remove() as that destroys the row/cell data in the table in // IE6-7 so it cannot be read by the data reader. data.parentNode.removeChild(data); } }, onDestroy: function() { this.callParent(); this.table.remove(); delete this.table; } }); /** * A {@link Ext.ux.statusbar.StatusBar} plugin that provides automatic error * notification when the associated form contains validation errors. */ Ext.define('Ext.ux.statusbar.ValidationStatus', { extend: 'Ext.Component', requires: [ 'Ext.util.MixedCollection' ], /** * @cfg {String} errorIconCls * The {@link Ext.ux.statusbar.StatusBar#iconCls iconCls} value to be applied * to the status message when there is a validation error. */ errorIconCls: 'x-status-error', /** * @cfg {String} errorListCls * The css class to be used for the error list when there are validation errors. */ errorListCls: 'x-status-error-list', /** * @cfg {String} validIconCls * The {@link Ext.ux.statusbar.StatusBar#iconCls iconCls} value to be applied * to the status message when the form validates. */ validIconCls: 'x-status-valid', /** * @cfg {String} showText * The {@link Ext.ux.statusbar.StatusBar#text text} value to be applied when * there is a form validation error. */ showText: 'The form has errors (click for details...)', /** * @cfg {String} hideText * The {@link Ext.ux.statusbar.StatusBar#text text} value to display when * the error list is displayed. */ hideText: 'Click again to hide the error list', /** * @cfg {String} submitText * The {@link Ext.ux.statusbar.StatusBar#text text} value to be applied when * the form is being submitted. */ submitText: 'Saving...', // private init: function(sb) { var me = this; me.statusBar = sb; sb.on({ single: true, scope: me, render: me.onStatusbarRender, beforedestroy: me.destroy }); sb.on({ click: { element: 'el', fn: me.onStatusClick, scope: me, buffer: 200 } }); }, onStatusbarRender: function(sb) { var me = this, startMonitor = function() { me.monitor = true; }; me.monitor = true; me.errors = Ext.create('Ext.util.MixedCollection'); me.listAlign = (sb.statusAlign === 'right' ? 'br-tr?' : 'bl-tl?'); if (me.form) { me.formPanel = Ext.getCmp(me.form); me.basicForm = me.formPanel.getForm(); me.startMonitoring(); me.basicForm.on('beforeaction', function(f, action) { if (action.type === 'submit') { // Ignore monitoring while submitting otherwise the field validation // events cause the status message to reset too early me.monitor = false; } }); me.basicForm.on('actioncomplete', startMonitor); me.basicForm.on('actionfailed', startMonitor); } }, // private startMonitoring: function() { this.basicForm.getFields().each(function(f) { f.on('validitychange', this.onFieldValidation, this); }, this); }, // private stopMonitoring: function() { this.basicForm.getFields().each(function(f) { f.un('validitychange', this.onFieldValidation, this); }, this); }, // private onDestroy: function() { this.stopMonitoring(); this.statusBar.statusEl.un('click', this.onStatusClick, this); this.callParent(arguments); }, // private onFieldValidation: function(f, isValid) { var me = this, msg; if (!me.monitor) { return false; } msg = f.getErrors()[0]; if (msg) { me.errors.add(f.id, { field: f, msg: msg }); } else { me.errors.removeAtKey(f.id); } this.updateErrorList(); if (me.errors.getCount() > 0) { if (me.statusBar.getText() !== me.showText) { me.statusBar.setStatus({ text: me.showText, iconCls: me.errorIconCls }); } } else { me.statusBar.clearStatus().setIcon(me.validIconCls); } }, // private updateErrorList: function() { var me = this, msg, msgEl = me.getMsgEl(); if (me.errors.getCount() > 0) { msg = [ ''); msgEl.update(msg.join('')); } else { msgEl.update(''); } // reset msgEl size msgEl.setSize('auto', 'auto'); }, // private getMsgEl: function() { var me = this, msgEl = me.msgEl, t; if (!msgEl) { msgEl = me.msgEl = Ext.DomHelper.append(Ext.getBody(), { cls: me.errorListCls }, true); msgEl.hide(); msgEl.on('click', function(e) { t = e.getTarget('li', 10, true); if (t) { Ext.getCmp(t.id.split('x-err-')[1]).focus(); me.hideErrors(); } }, null, { stopEvent: true }); } // prevent anchor click navigation return msgEl; }, // private showErrors: function() { var me = this; me.updateErrorList(); me.getMsgEl().alignTo(me.statusBar.getEl(), me.listAlign).slideIn('b', { duration: 300, easing: 'easeOut' }); me.statusBar.setText(me.hideText); me.formPanel.body.on('click', me.hideErrors, me, { single: true }); }, // hide if the user clicks directly into the form // private hideErrors: function() { var el = this.getMsgEl(); if (el.isVisible()) { el.slideOut('b', { duration: 300, easing: 'easeIn' }); this.statusBar.setText(this.showText); } this.formPanel.body.un('click', this.hideErrors, this); }, // private onStatusClick: function() { if (this.getMsgEl().isVisible()) { this.hideErrors(); } else if (this.errors.getCount() > 0) { this.showErrors(); } } });