facebook-workplaceoutlookemailmicrosoft-teamsdiscordmessengercustom-servicesmacoslinuxwindowsinboxwhatsappicloudtweetdeckhipchattelegramhangoutsslackgmailskype
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
11926 lines
404 KiB
11926 lines
404 KiB
9 years ago
|
/**
|
||
|
* 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;
|
||
|
//<debugger>
|
||
|
if (!me.alt) {
|
||
|
Ext.log.warn('For ARIA compliance, IMG elements SHOULD have an alt attribute');
|
||
|
}
|
||
|
//</debugger>
|
||
|
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',
|
||
|
//<locale>
|
||
|
closeText: 'closable',
|
||
|
//</locale>
|
||
|
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('<<foo>>'),
|
||
|
*
|
||
|
* // 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',
|
||
|
//<locale>
|
||
|
/**
|
||
|
* @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}',
|
||
|
//</locale>
|
||
|
/**
|
||
|
* @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: [
|
||
|
* '<tpl for=".">',
|
||
|
* '<p class="team">',
|
||
|
* 'The {team} were founded in {established}.',
|
||
|
* '</p>',
|
||
|
* '</tpl>'
|
||
|
* ],
|
||
|
* 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: [
|
||
|
'<tpl for=".">',
|
||
|
'{title}',
|
||
|
'</tpl>'
|
||
|
],
|
||
|
/**
|
||
|
* @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: '<tpl for=".">' + '<div class="' + Ext.baseCSSPrefix + 'explorer-item">' + '<div class="{iconCls}">' + '<div class="' + Ext.baseCSSPrefix + 'explorer-node-icon' + '{[values.leaf ? " ' + Ext.baseCSSPrefix + 'explorer-leaf-icon' + '" : ""]}' + '">' + '</div>' + '<div class="' + Ext.baseCSSPrefix + 'explorer-item-text">{text}</div>' + '</div>' + '</div>' + '</tpl>'
|
||
|
},
|
||
|
/**
|
||
|
* @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);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
/**
|
||
|
* <p>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.</p>
|
||
|
* <p>Usage:</p>
|
||
|
* <pre><code>
|
||
|
{
|
||
|
xtype: 'combo',
|
||
|
plugins: [ Ext.ux.FieldReplicator ],
|
||
|
triggerAction: 'all',
|
||
|
fieldLabel: 'Select recipient',
|
||
|
store: recipientStore
|
||
|
}
|
||
|
* </code></pre>
|
||
|
*/
|
||
|
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).
|
||
|
*
|
||
|
* <script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?v=3&sensor=false"></script>
|
||
|
*
|
||
|
* 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('<div id="{view.id}-body" class="' + Ext.baseCSSPrefix + '{view.id}-table ' + Ext.baseCSSPrefix + 'grid-table-resizer" style="{tableStyle}">', '{%', 'values.view.renderRows(values.rows, values.viewStartIndex, out);', '%}', '</div>', {
|
||
|
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";', '%}', '<div {[values.rowId ? ("id=\\"" + values.rowId + "\\"") : ""]} ', 'data-boundView="{view.id}" ', 'data-recordId="{record.internalId}" ', 'data-recordIndex="{recordIndex}" ', 'class="' + Ext.baseCSSPrefix + 'grouptab-row {[values.itemClasses.join(" ")]} {[values.rowClasses.join(" ")]}{[dataRowCls]}" ', '{rowAttr:attributes}>', '<tpl for="columns">' + '{%', 'parent.view.renderCell(values, parent.record, parent.recordIndex, parent.rowIndex, xindex - 1, out, parent)', '%}', '</tpl>', '</div>', {
|
||
|
priority: 5
|
||
|
}),
|
||
|
cellTpl: new Ext.XTemplate('{%values.tdCls = values.tdCls.replace(" ' + Ext.baseCSSPrefix + 'grid-cell "," ");%}', '<div class="' + Ext.baseCSSPrefix + 'grouptab-cell {tdCls}" {tdAttr}>', '<div {unselectableAttr} class="' + Ext.baseCSSPrefix + 'grid-cell-inner" style="text-align: {align}; {style};">{value}</div>', '<div class="x-grouptabs-corner x-grouptabs-corner-top-left"></div>', '<div class="x-grouptabs-corner x-grouptabs-corner-bottom-left"></div>', '</div>', {
|
||
|
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: [
|
||
|
'<iframe src="{src}" id="{id}-iframeEl" data-ref="iframeEl" name="{frameName}" width="100%" height="100%" frameborder="0"></iframe>'
|
||
|
],
|
||
|
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 <b>initially</b> set as the status message. This string
|
||
|
* will be set as innerHTML (html tags are accepted) for the toolbar item.
|
||
|
* If not specified, the value set for {@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 '<span class="' + me.matchCls + '">' + m + '</span>';
|
||
|
}, 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
|
||
|
* <p>The default progress bar width. Default is 225.</p>
|
||
|
*/
|
||
|
width: 225,
|
||
|
/**
|
||
|
* @cfg {String} defaultText
|
||
|
* <p>The text to display while the store is loading. Default is 'Loading...'</p>
|
||
|
*/
|
||
|
defaultText: 'Loading...',
|
||
|
/**
|
||
|
* @cfg {Object} defaultAnimCfg
|
||
|
* <p>A {@link Ext.fx.Anim Ext.fx.Anim} configuration object.</p>
|
||
|
*/
|
||
|
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 <b>{0}</b> of <b>{1}</b>', 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:
|
||
|
* <pre>
|
||
|
* Ext.create('Ext.ux.ToolbarDroppable', {
|
||
|
* createItem: function(data) {
|
||
|
* return Ext.create('Ext.Button', {text: data.text});
|
||
|
* }
|
||
|
* });
|
||
|
* </pre>
|
||
|
* 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',
|
||
|
'<tpl for="data">',
|
||
|
' <{parent.record}>\n',
|
||
|
'<tpl for="parent.fields">',
|
||
|
' <{name}>{[parent[values.name]]}</{name}>\n',
|
||
|
'</tpl>',
|
||
|
' </{parent.record}>\n',
|
||
|
'</tpl>',
|
||
|
'</{root}>'
|
||
|
],
|
||
|
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], '</', key, '>');
|
||
|
}
|
||
|
}
|
||
|
buffer.push('</', recordProperty, '>');
|
||
|
});
|
||
|
buffer.push('</', rootProperty, '>');
|
||
|
}
|
||
|
});
|
||
|
|
||
|
/**
|
||
|
* 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: '<tpl for="entries[currentEntry]">' + '<div class="' + Ext.baseCSSPrefix + 'dashboard-googlerss-detail-header">' + '<div class="' + Ext.baseCSSPrefix + 'dashboard-googlerss-detail-nav">' + '<tpl if="parent.hasPrev">' + '<span class="' + Ext.baseCSSPrefix + 'dashboard-googlerss-prev ' + Ext.baseCSSPrefix + 'dashboard-googlerss-glyph">' + '{parent.prevGlyph}' + '</span> ' + '</tpl>' + ' {[parent.currentEntry+1]}/{parent.numEntries} ' + '<span class="' + Ext.baseCSSPrefix + 'dashboard-googlerss-next ' + Ext.baseCSSPrefix + 'dashboard-googlerss-glyph"' + '<tpl if="!parent.hasNext">' + ' style="visibility:hidden"' + '</tpl>' + '> {parent.nextGlyph}' + '</span> ' + '<span class="' + Ext.baseCSSPrefix + 'dashboard-googlerss-close ' + Ext.baseCSSPrefix + 'dashboard-googlerss-glyph"> ' + '{parent.closeGlyph}' + '</span> ' + '</div>' + '<div class="' + Ext.baseCSSPrefix + 'dashboard-googlerss-title">' + '<a href="{link}" target=_blank>{title}</a>' + '</div>' + '<div class="' + Ext.baseCSSPrefix + 'dashboard-googlerss-author">By {author} - {publishedDate:this.date}</div>' + '</div>' + '<div class="' + Ext.baseCSSPrefix + 'dashboard-googlerss-detail">' + '{content}' + '</div>' + '</tpl>',
|
||
|
summaryTpl: '<tpl for="entries">' + '<div class="' + Ext.baseCSSPrefix + 'dashboard-googlerss">' + '<span class="' + Ext.baseCSSPrefix + 'dashboard-googlerss-title">' + '<a href="{link}" target=_blank>{title}</a>' + '</span> ' + '<img src="' + Ext.BLANK_IMAGE_URL + '" data-index="{#}" class="' + Ext.baseCSSPrefix + 'dashboard-googlerss-preview"><br>' + '<span class="' + Ext.baseCSSPrefix + 'dashboard-googlerss-author">By {author} - {publishedDate:this.date}</span><br>' + '<span class="' + Ext.baseCSSPrefix + 'dashboard-googlerss-snippet">{contentSnippet}</span><br>' + '</div>' + '</tpl>',
|
||
|
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 = '<span style="font-family:' + glyphFontFamily + '">' + html + '</span>';
|
||
|
}
|
||
|
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
|
||
|
* <p>This class manages the wallpaper, shortcuts and taskbar.</p>
|
||
|
*/
|
||
|
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: [
|
||
|
'<tpl for=".">',
|
||
|
'<div class="ux-desktop-shortcut" id="{name}-shortcut">',
|
||
|
'<div class="ux-desktop-shortcut-icon {iconCls}">',
|
||
|
'<img src="',
|
||
|
Ext.BLANK_IMAGE_URL,
|
||
|
'" title="{name}">',
|
||
|
'</div>',
|
||
|
'<span class="ux-desktop-shortcut-text">{name}</span>',
|
||
|
'</div>',
|
||
|
'</tpl>',
|
||
|
'<div class="x-clear"></div>'
|
||
|
],
|
||
|
/**
|
||
|
* @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: [
|
||
|
'<video id="{id}-video" autoPlay="{autoplay}" controls="{controls}" poster="{poster}" start="{start}" loopstart="{loopstart}" loopend="{loopend}" autobuffer="{autobuffer}" loop="{loop}" style="width:100%;height:100%">',
|
||
|
'<tpl for="src">',
|
||
|
'<source src="{src}" type="{type}"/>',
|
||
|
'</tpl>',
|
||
|
'{html}',
|
||
|
'</video>'
|
||
|
],
|
||
|
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 = '<a href="http://www.google.com/chrome">Chrome</a>';
|
||
|
fallback += 'Please try <a href="http://www.mozilla.com">Firefox</a>';
|
||
|
if (Ext.isIE) {
|
||
|
fallback += ', ' + chrome + ' or <a href="http://www.apple.com/safari/">Safari</a>.';
|
||
|
} 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 '<h1 style="background-color:#ff4f4f;padding: 10px;">' + this.fallbackHTML + '</h1>';
|
||
|
},
|
||
|
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
|
||
|
* <p>This component renders an image that stretches to fill the component.</p>
|
||
|
*/
|
||
|
Ext.define('Ext.ux.desktop.Wallpaper', {
|
||
|
extend: 'Ext.Component',
|
||
|
alias: 'widget.wallpaper',
|
||
|
cls: 'ux-wallpaper',
|
||
|
html: '<img src="' + Ext.BLANK_IMAGE_URL + '">',
|
||
|
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:
|
||
|
* <div class="mdetail-params"><ul>
|
||
|
* <li><b>any {@link Ext.data.Store Store} subclass</b></li>
|
||
|
* <li><b>an Array</b> : Arrays will be converted to a {@link Ext.data.ArrayStore} internally.
|
||
|
* <div class="mdetail-params"><ul>
|
||
|
* <li><b>1-dimensional array</b> : (e.g., <tt>['Foo','Bar']</tt>)<div class="sub-desc">
|
||
|
* A 1-dimensional array will automatically be expanded (each array item will be the combo
|
||
|
* {@link #valueField value} and {@link #displayField text})</div></li>
|
||
|
* <li><b>2-dimensional array</b> : (e.g., <tt>[['f','Foo'],['b','Bar']]</tt>)<div class="sub-desc">
|
||
|
* For a multi-dimensional array, the value in index 0 of each item will be assumed to be the combo
|
||
|
* {@link #valueField value}, while the value at index 1 is assumed to be the combo {@link #displayField text}.
|
||
|
* </div></li></ul></div></li></ul></div>
|
||
|
*/
|
||
|
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 <tt>['top', 'up', 'add', 'remove', 'down', 'bottom']</tt>. 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: [
|
||
|
'<table class="' + Ext.baseCSSPrefix + 'grid-subtable"><tbody>',
|
||
|
'{%',
|
||
|
'this.owner.renderTable(out, values);',
|
||
|
'%}',
|
||
|
'</tbody></table>'
|
||
|
],
|
||
|
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('<thead>');
|
||
|
for (j = 0; j < numColumns; j++) {
|
||
|
out.push('<th class="' + Ext.baseCSSPrefix + 'grid-subtable-header">', columns[j].text, '</th>');
|
||
|
}
|
||
|
out.push('</thead>');
|
||
|
for (i = 0; i < recCount; i++) {
|
||
|
rec = associatedRecords[i];
|
||
|
out.push('<tr>');
|
||
|
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('<td class="' + Ext.baseCSSPrefix + 'grid-subtable-cell"');
|
||
|
if (column.width != null) {
|
||
|
out.push(' style="width:' + column.width + 'px"');
|
||
|
}
|
||
|
out.push('><div class="' + Ext.baseCSSPrefix + 'grid-cell-inner">', value, '</div></td>');
|
||
|
}
|
||
|
out.push('</tr>');
|
||
|
}
|
||
|
},
|
||
|
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 = [
|
||
|
'<ul>'
|
||
|
];
|
||
|
this.errors.each(function(err) {
|
||
|
msg.push('<li id="x-err-', err.field.id, '"><a href="#">', err.msg, '</a></li>');
|
||
|
});
|
||
|
msg.push('</ul>');
|
||
|
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();
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|