/**
* An updateable progress bar component. The progress bar supports two different modes: manual
* and automatic.
*
* In manual mode, you are responsible for showing, updating (via {@link #updateProgress})
* and clearing the progress bar as needed from your own code. This method is most appropriate
* when you want to show progress throughout an operation that has predictable points of interest
* at which you can update the control.
*
* In automatic mode, you simply call {@link #wait} and let the progress bar run indefinitely,
* only clearing it once the operation is complete. You can optionally have the progress bar
* wait for a specific amount of time and then clear itself. Automatic mode is most appropriate
* for timed operations or asynchronous operations in which you have no need for indicating
* intermediate progress.
*
* @example
* var p = Ext.create('Ext.ProgressBar', {
* renderTo: Ext.getBody(),
* width: 300
* });
*
* // Wait for 5 seconds, then update the status el (progress bar will auto-reset)
* p.wait({
* interval: 500, //bar will move fast!
* duration: 50000,
* increment: 15,
* text: 'Updating...',
* scope: this,
* fn: function(){
* p.updateText('Done!');
* }
* });
*/
Ext.define('Ext.ProgressBar', {
extend: 'Ext.Component',
xtype: 'progressbar',
mixins: [
'Ext.ProgressBase'
],
requires: [
'Ext.Template',
'Ext.CompositeElement',
'Ext.TaskManager',
'Ext.layout.component.ProgressBar'
],
uses: ['Ext.fx.Anim'],
/**
* @cfg {String/HTMLElement/Ext.dom.Element} textEl
* The element to render the progress text to (defaults to the progress bar's internal
* text element)
*/
/**
* @cfg {String} id
* The progress bar element's id (defaults to an auto-generated id)
*/
/**
* @cfg {String} [baseCls='x-progress']
* The base CSS class to apply to the progress bar's wrapper element.
*/
baseCls: Ext.baseCSSPrefix + 'progress',
/**
* @cfg {Boolean/Object} animate
* True to animate the progress bar during transitions, or an animation configuration
* (see the {@link #method-animate} method for details).
*/
animate: false,
/**
* @cfg {String} text
* The text shown in the progress bar.
*/
text: '',
/**
* @private
*/
waitTimer: null,
childEls: [
'bar'
],
defaultBindProperty: 'value',
/* eslint-disable indent, max-len */
renderTpl: [
'<tpl if="internalText">',
'<div class="{baseCls}-text {baseCls}-text-back" role="presentation">{text}</div>',
'</tpl>',
'<div id="{id}-bar" data-ref="bar" class="{baseCls}-bar {baseCls}-bar-{ui}" role="presentation" style="width:{percentage}%">',
'<tpl if="internalText">',
'<div class="{baseCls}-text" role="presentation">',
'<div role="presentation">{text}</div>',
'</div>',
'</tpl>',
'</div>'
],
/* eslint-enable indent, max-len */
componentLayout: 'progressbar',
ariaRole: 'progressbar',
focusable: true,
tabIndex: 0,
autoEl: {
'aria-valuemin': '0',
'aria-valuenow': '0',
'aria-valuemax': '100'
},
/**
* @event update
* Fires after each update interval
* @param {Ext.ProgressBar} this
* @param {Number} value The current progress value
* @param {String} text The current progress text
*/
initRenderData: function() {
var me = this,
value = me.value || 0,
data;
data = me.callParent();
return Ext.apply(data, {
internalText: !me.hasOwnProperty('textEl'),
text: me.text || Math.round(value * 100) + '%',
percentage: value * 100
});
},
onRender: function() {
var me = this;
me.callParent(arguments);
// External text display
if (me.textEl) {
me.textEl = Ext.get(me.textEl);
me.updateText(me.text);
}
// Inline text display
else {
// This produces a composite w/2 el's (which is why we cannot use childEls or
// renderSelectors):
me.textEl = me.el.select('.' + me.baseCls + '-text');
}
},
afterRender: function() {
var me = this;
me.callParent(arguments);
if (me.text) {
me.ariaEl.dom.setAttribute('aria-valuetext', me.text);
}
},
updateValue: function(value) {
this.updateProgress(value);
},
/**
* Updates the progress bar value, and optionally its text.
*
* If the text argument is not specified, then the {@link #textTpl} will be used to generate
* the text. If there is no `textTpl`, any existing text value will be unchanged. To blank out
* existing text, pass `""`.
*
* Note that even if the progress bar value exceeds 1, it will never automatically reset --
* you are responsible for determining when the progress is complete and
* calling {@link #reset} to clear and/or hide the control.
* @param {Number} [value=0] A floating point value between 0 and 1 (e.g., .5)
* @param {String} [text=''] The string to display in the progress text element
* @param {Boolean} [animate=false] Whether to animate the transition of the progress bar.
* If this value is not specified, the default for the class is used
* @return {Ext.ProgressBar} this
*/
updateProgress: function(value, text, animate) {
var me = this,
oldValue = me.value,
textTpl = me.getTextTpl();
value = value || 0;
// Ensure value is not undefined.
me.value = value || (value = 0);
// Empty string (falsy) must blank out the text as per docs.
if (text != null) {
me.autoText = false;
me.updateText(text);
}
// Generate text using template and progress values.
else if (textTpl) {
me.autoText = false;
me.updateText(textTpl.apply({
value: value,
percent: value * 100
}));
}
else if (!me.text && me.autoText !== false) {
me.autoText = true;
me.updateText(Math.round(value * 100) + '%');
}
// Text was set the previous time but not this time. We can't
// deduce what it should be just from the value so need to
// reset aria-valuetext because it is no longer valid.
else if (me.text && me.ariaEl.dom) {
me.ariaEl.dom.removeAttribute('aria-valuetext');
}
if (me.rendered && !me.destroyed) {
if (animate === true || (animate !== false && me.animate)) {
me.bar.stopAnimation();
me.bar.animate(Ext.apply({
from: {
width: (oldValue * 100) + '%'
},
to: {
width: (value * 100) + '%'
}
}, me.animate));
}
else {
me.bar.setStyle('width', (value * 100) + '%');
}
me.ariaEl.dom.setAttribute('aria-valuenow', Math.round(value * 100));
}
me.fireEvent('update', me, value, text);
return me;
},
/**
* Updates the progress bar text. If specified, textEl will be updated, otherwise
* the progress bar itself will display the updated text.
* @param {String} [text=''] The string to display in the progress text element
* @return {Ext.ProgressBar} this
*/
updateText: function(text) {
var me = this;
if (!me.autoText) {
me.text = text;
}
if (me.rendered) {
me.textEl.setHtml(text);
if (!me.autoText) {
me.ariaEl.dom.setAttribute('aria-valuetext', text);
}
else {
me.ariaEl.dom.removeAttribute('aria-valuetext');
}
}
return me;
},
applyText: function(text) {
this.updateText(text);
},
getText: function() {
return this.text;
},
/**
* Initiates an auto-updating progress bar. A duration can be specified, in which case
* the progress bar will automatically reset after a fixed amount of time and optionally call
* a callback function if specified. If no duration is passed in, then the progress bar will run
* indefinitely and must be manually cleared by calling {@link #reset}.
*
* Example usage:
*
* var p = new Ext.ProgressBar({
* renderTo: 'my-el'
* });
*
* // Wait for 5 seconds, then update the status el (progress bar will auto-reset)
* var p = Ext.create('Ext.ProgressBar', {
* renderTo: Ext.getBody(),
* width: 300
* });
*
* // Wait for 5 seconds, then update the status el (progress bar will auto-reset)
* p.wait({
* interval: 500, // bar will move fast!
* duration: 50000,
* increment: 15,
* text: 'Updating...',
* scope: this,
* fn: function() {
* p.updateText('Done!');
* }
* });
*
* // Or update indefinitely until some async action completes, then reset manually
* p.wait();
* myAction.on('complete', function() {
* p.reset();
* p.updateText('Done!');
* });
*
* @param {Object} config (optional) Configuration options
* @param {Number} config.duration The length of time in milliseconds that the progress bar
* should run before resetting itself (defaults to undefined, in which case it will run
* indefinitely until reset is called)
* @param {Number} config.interval The length of time in milliseconds between each progress
* update (defaults to 1000 ms)
* @param {Boolean} config.animate Whether to animate the transition of the progress bar.
* If this value is not specified, the default for the class is used.
* @param {Number} config.increment The number of progress update segments to display within
* the progress bar (defaults to 10). If the bar reaches the end and is still updating, it will
* automatically wrap back to the beginning.
* @param {String} config.text Optional text to display in the progress bar element
* (defaults to '').
* @param {Function} config.fn A callback function to execute after the progress bar finishes
* auto-updating. The function will be called with no arguments. This function will be ignored
* if duration is not specified since in that case the progress bar can only be stopped
* programmatically, so any required function should be called by the same code after it resets
* the progress bar.
* @param {Object} config.scope The scope that is passed to the callback function (only applies
* when duration and fn are both passed).
* @return {Ext.ProgressBar} this
*/
wait: function(config) {
var me = this,
scope;
if (!me.waitTimer) {
scope = me;
config = config || {};
me.autoText = !config.text;
me.text = config.text;
me.updateText(config.text);
me.waitTimer = Ext.TaskManager.start({
run: function(i) {
var inc = config.increment || 10;
i -= 1;
me.updateProgress(((((i + inc) % inc) + 1) * (100 / inc)) * 0.01, null,
config.animate);
},
interval: config.interval || 1000,
duration: config.duration,
onStop: function() {
if (config.fn) {
config.fn.apply(config.scope || me);
}
me.reset();
},
scope: scope
});
}
return me;
},
/**
* Returns true if the progress bar is currently in a {@link #wait} operation
* @return {Boolean} True if waiting, else false
*/
isWaiting: function() {
return this.waitTimer !== null;
},
/**
* Resets the progress bar value to 0 and text to empty string. If hide = true,
* the progress bar will also be hidden (using the {@link #hideMode} property internally).
* @param {Boolean} [hide=false] True to hide the progress bar.
* @return {Ext.ProgressBar} this
*/
reset: function(hide) {
var me = this;
me.updateProgress(0);
me.clearTimer();
if (hide === true) {
me.hide();
}
if (me.rendered) {
me.ariaEl.dom.removeAttribute('aria-valuetext');
}
return me;
},
/**
* @private
*/
clearTimer: function() {
var me = this;
if (me.waitTimer) {
me.waitTimer.onStop = null; // prevent recursion
Ext.TaskManager.stop(me.waitTimer);
me.waitTimer = null;
}
},
doDestroy: function() {
var me = this,
bar = me.bar,
nodes, el, i, len;
me.clearTimer();
if (me.rendered) {
if (me.textEl.isComposite) {
// Clean up Element references created by the layout
nodes = me.textEl.slice();
for (i = 0, len = nodes.length; i < len; i++) {
el = Ext.get(nodes[i]);
el.destroy();
}
}
Ext.destroyMembers(me, 'textEl', 'progressBar');
if (bar && me.animate) {
bar.stopAnimation();
}
}
me.callParent();
}
});