whatsappicloudtweetdeckhipchattelegramhangoutsslackgmailskypefacebook-workplaceoutlookemailmicrosoft-teamsdiscordmessengercustom-servicesmacoslinuxwindowsinbox
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.
11925 lines
404 KiB
11925 lines
404 KiB
/** |
|
* 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(); |
|
} |
|
} |
|
}); |
|
|
|
|