blob: 929a933b67e8123dd98c78c5085ec146babb7cc1 [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* Item element of the progress center.
* @param {HTMLDocument} document Document which the new item belongs to.
* @constructor
*/
function ProgressCenterItemElement(document) {
var label = document.createElement('label');
label.className = 'label';
var progressBarIndicator = document.createElement('div');
progressBarIndicator.className = 'progress-track';
var progressBar = document.createElement('div');
progressBar.className = 'progress-bar';
progressBar.appendChild(progressBarIndicator);
var progressFrame = document.createElement('div');
progressFrame.className = 'progress-frame';
progressFrame.appendChild(label);
progressFrame.appendChild(progressBar);
var cancelButton = document.createElement('button');
cancelButton.className = 'cancel';
cancelButton.setAttribute('tabindex', '-1');
var buttonFrame = document.createElement('div');
buttonFrame.className = 'button-frame';
buttonFrame.appendChild(cancelButton);
var itemElement = document.createElement('li');
itemElement.appendChild(progressFrame);
itemElement.appendChild(buttonFrame);
return ProgressCenterItemElement.decorate(itemElement);
}
/**
* Ensures the animation triggers.
*
* @param {function()} callback Function to set the transition end properties.
* @return {function()} Function to cancel the request.
* @private
*/
ProgressCenterItemElement.safelySetAnimation_ = function(callback) {
var requestId = requestAnimationFrame(function() {
// The transition start properties currently set are rendered at this frame.
// And the transition end properties set by the callback is rendered at the
// next frame.
requestId = requestAnimationFrame(callback);
});
return function() {
cancelAnimationFrame(requestId);
};
};
/**
* Event triggered when the item should be dismissed.
* @type {string}
* @const
*/
ProgressCenterItemElement.PROGRESS_ANIMATION_END_EVENT = 'progressAnimationEnd';
/**
* Decorates the given element as a progress item.
* @param {HTMLElement} element Item to be decorated.
* @return {ProgressCenterItemElement} Decorated item.
*/
ProgressCenterItemElement.decorate = function(element) {
element.__proto__ = ProgressCenterItemElement.prototype;
element.state_ = ProgressItemState.PROGRESSING;
element.track_ = element.querySelector('.progress-track');
element.track_.addEventListener('webkitTransitionEnd',
element.onTransitionEnd_.bind(element));
element.cancelTransition_ = null;
return element;
};
ProgressCenterItemElement.prototype = {
__proto__: HTMLDivElement.prototype,
get quiet() {
return this.classList.contains('quiet');
}
};
/**
* Updates the element view according to the item.
* @param {ProgressCenterItem} item Item to be referred for the update.
* @param {boolean} animated Whether the progress width is applied as animated
* or not.
*/
ProgressCenterItemElement.prototype.update = function(item, animated) {
// Set element attributes.
this.state_ = item.state;
this.setAttribute('data-progress-id', item.id);
this.classList.toggle('error', item.state === ProgressItemState.ERROR);
this.classList.toggle('cancelable', item.cancelable);
this.classList.toggle('single', item.single);
this.classList.toggle('quiet', item.quiet);
// Set label.
if (this.state_ === ProgressItemState.PROGRESSING ||
this.state_ === ProgressItemState.ERROR) {
this.querySelector('label').textContent = item.message;
} else if (this.state_ === ProgressItemState.CANCELED) {
this.querySelector('label').textContent = '';
}
// Cancel the previous property set.
if (this.cancelTransition_) {
this.cancelTransition_();
this.cancelTransition_ = null;
}
// Set track width.
var setWidth = function(nextWidthFrame) {
var currentWidthRate = parseInt(this.track_.style.width);
// Prevent assigning the same width to avoid stopping the animation.
// animated == false may be intended to cancel the animation, so in that
// case, the assignment should be done.
if (currentWidthRate === nextWidthFrame && animated)
return;
this.track_.hidden = false;
this.track_.style.width = nextWidthFrame + '%';
this.track_.classList.toggle('animated', animated);
}.bind(this, item.progressRateInPercent);
if (animated) {
this.cancelTransition_ =
ProgressCenterItemElement.safelySetAnimation_(setWidth);
} else {
// For animated === false, we should call setWidth immediately to cancel the
// animation, otherwise the animation may complete before canceling it.
setWidth();
}
};
/**
* Resets the item.
*/
ProgressCenterItemElement.prototype.reset = function() {
this.track_.hidden = true;
this.track_.width = '';
this.state_ = ProgressItemState.PROGRESSING;
};
/**
* Handles transition end events.
* @param {Event} event Transition end event.
* @private
*/
ProgressCenterItemElement.prototype.onTransitionEnd_ = function(event) {
if (event.propertyName !== 'width')
return;
this.track_.classList.remove('animated');
this.dispatchEvent(new Event(
ProgressCenterItemElement.PROGRESS_ANIMATION_END_EVENT,
{bubbles: true}));
};
/**
* Progress center panel.
*
* @param {HTMLElement} element DOM Element of the process center panel.
* @constructor
*/
function ProgressCenterPanel(element) {
/**
* Root element of the progress center.
* @type {HTMLElement}
* @private
*/
this.element_ = element;
/**
* Open view containing multiple progress items.
* @type {HTMLElement}
* @private
*/
this.openView_ = this.element_.querySelector('#progress-center-open-view');
/**
* Close view that is a summarized progress item.
* @type {HTMLElement}
* @private
*/
this.closeView_ = ProgressCenterItemElement.decorate(
this.element_.querySelector('#progress-center-close-view'));
/**
* Toggle animation rule of the progress center.
* @type {CSSKeyFrameRule}
* @private
*/
this.toggleAnimation_ = ProgressCenterPanel.getToggleAnimation_(
element.ownerDocument);
/**
* Item group for normal priority items.
* @type {ProgressCenterItemGroup}
* @private
*/
this.normalItemGroup_ = new ProgressCenterItemGroup('normal', false);
/**
* Item group for low priority items.
* @type {ProgressCenterItemGroup}
* @private
*/
this.quietItemGroup_ = new ProgressCenterItemGroup('quiet', true);
/**
* Queries to obtains items for each group.
* @type {Object.<string, string>}
* @private
*/
this.itemQuery_ = Object.seal({
normal: 'li:not(.quiet)',
quiet: 'li.quiet'
});
/**
* Timeout IDs of the inactive state of each group.
* @type {Object.<string, number?>}
* @private
*/
this.timeoutId_ = Object.seal({
normal: null,
quiet: null
});
/**
* Callback to becalled with the ID of the progress item when the cancel
* button is clicked.
*/
this.cancelCallback = null;
Object.seal(this);
// Register event handlers.
element.addEventListener('click', this.onClick_.bind(this));
element.addEventListener(
'webkitAnimationEnd', this.onToggleAnimationEnd_.bind(this));
element.addEventListener(
ProgressCenterItemElement.PROGRESS_ANIMATION_END_EVENT,
this.onItemAnimationEnd_.bind(this));
}
/**
* Obtains the toggle animation keyframes rule from the document.
* @param {HTMLDocument} document Document containing the rule.
* @return {CSSKeyFrameRules} Animation rule.
* @private
*/
ProgressCenterPanel.getToggleAnimation_ = function(document) {
for (var i = 0; i < document.styleSheets.length; i++) {
var styleSheet = document.styleSheets[i];
for (var j = 0; j < styleSheet.cssRules.length; j++) {
var rule = styleSheet.cssRules[j];
if (rule.type === CSSRule.WEBKIT_KEYFRAMES_RULE &&
rule.name === 'progress-center-toggle') {
return rule;
}
}
}
throw new Error('The progress-center-toggle rules is not found.');
};
/**
* The default amount of milliseconds time, before a progress item will reset
* after the last complete.
* @type {number}
* @private
* @const
*/
ProgressCenterPanel.RESET_DELAY_TIME_MS_ = 5000;
/**
* Updates an item to the progress center panel.
* @param {!ProgressCenterItem} item Item including new contents.
*/
ProgressCenterPanel.prototype.updateItem = function(item) {
var targetGroup = this.getGroupForItem_(item);
// Update the item.
var oldState = targetGroup.state;
targetGroup.update(item);
this.handleGroupStateChange_(targetGroup, oldState, targetGroup.state);
// Update an open view item.
var newItem = targetGroup.getItem(item.id);
var itemElement = this.getItemElement_(item.id);
if (newItem) {
if (!itemElement) {
itemElement = new ProgressCenterItemElement(this.element_.ownerDocument);
this.openView_.insertBefore(itemElement, this.openView_.firstNode);
}
itemElement.update(newItem, targetGroup.isAnimated(item.id));
} else {
if (itemElement)
itemElement.parentNode.removeChild(itemElement);
}
// Update the close view.
this.updateCloseView_();
};
/**
* Handles the item animation end.
* @param {Event} event Item animation end event.
* @private
*/
ProgressCenterPanel.prototype.onItemAnimationEnd_ = function(event) {
var targetGroup = event.target.classList.contains('quiet') ?
this.quietItemGroup_ : this.normalItemGroup_;
var oldState = targetGroup.state;
if (event.target === this.closeView_) {
targetGroup.completeSummarizedItemAnimation();
} else {
var itemId = event.target.getAttribute('data-progress-id');
targetGroup.completeItemAnimation(itemId);
var newItem = targetGroup.getItem(itemId);
var itemElement = this.getItemElement_(itemId);
if (!newItem && itemElement)
itemElement.parentNode.removeChild(itemElement);
}
this.handleGroupStateChange_(targetGroup, oldState, targetGroup.state);
this.updateCloseView_();
};
/**
* Handles the state change of group.
* @param {ProgressCenterItemGroup} group Item group.
* @param {ProgressCenterItemGroup.State} oldState Old state of the group.
* @param {ProgressCenterItemGroup.State} newState New state of the group.
* @private
*/
ProgressCenterPanel.prototype.handleGroupStateChange_ =
function(group, oldState, newState) {
if (oldState === ProgressCenterItemGroup.State.INACTIVE) {
clearTimeout(this.timeoutId_[group.name]);
this.timeoutId_[group.name] = null;
var elements =
this.openView_.querySelectorAll(this.itemQuery_[group.name]);
for (var i = 0; i < elements.length; i++) {
elements[i].parentNode.removeChild(elements[i]);
}
}
if (newState === ProgressCenterItemGroup.State.INACTIVE) {
this.timeoutId_[group.name] = setTimeout(function() {
var inOldState = group.state;
group.endInactive();
this.handleGroupStateChange_(group, inOldState, group.state);
this.updateCloseView_();
}.bind(this), ProgressCenterPanel.RESET_DELAY_TIME_MS_);
}
};
/**
* Updates the close view.
* @private
*/
ProgressCenterPanel.prototype.updateCloseView_ = function() {
// Try to use the normal summarized item.
var normalSummarizedItem =
this.normalItemGroup_.getSummarizedItem(this.quietItemGroup_.numErrors);
if (normalSummarizedItem) {
// If the quiet animation is overrided by normal summarized item, discard
// the quiet animation.
if (this.quietItemGroup_.isSummarizedAnimated()) {
var oldState = this.quietItemGroup_.state;
this.quietItemGroup_.completeSummarizedItemAnimation();
this.handleGroupStateChange_(this.quietItemGroup_,
oldState,
this.quietItemGroup_.state);
}
// Update the view state.
this.closeView_.update(normalSummarizedItem,
this.normalItemGroup_.isSummarizedAnimated());
this.element_.hidden = false;
return;
}
// Try to use the quiet summarized item.
var quietSummarizedItem =
this.quietItemGroup_.getSummarizedItem(this.normalItemGroup_.numErrors);
if (quietSummarizedItem) {
this.closeView_.update(quietSummarizedItem,
this.quietItemGroup_.isSummarizedAnimated());
this.element_.hidden = false;
return;
}
// Try to use the error summarized item.
var errorSummarizedItem = ProgressCenterItemGroup.getSummarizedErrorItem(
this.normalItemGroup_, this.quietItemGroup_);
if (errorSummarizedItem) {
this.closeView_.update(errorSummarizedItem, false);
this.element_.hidden = false;
return;
}
// Hide the progress center because there is no items to show.
this.closeView_.reset();
this.element_.hidden = true;
this.element_.classList.remove('opened');
};
/**
* Gets an item element having the specified ID.
* @param {string} id progress item ID.
* @return {HTMLElement} Item element having the ID.
* @private
*/
ProgressCenterPanel.prototype.getItemElement_ = function(id) {
var query = 'li[data-progress-id="' + id + '"]';
return this.openView_.querySelector(query);
};
/**
* Obtains the group for the item.
* @param {ProgressCenterItem} item Progress item.
* @return {ProgressCenterItemGroup} Item group that should contain the item.
* @private
*/
ProgressCenterPanel.prototype.getGroupForItem_ = function(item) {
return item.quiet ? this.quietItemGroup_ : this.normalItemGroup_;
};
/**
* Handles the animation end event of the progress center.
* @param {Event} event Animation end event.
* @private
*/
ProgressCenterPanel.prototype.onToggleAnimationEnd_ = function(event) {
// Transition end of the root element's height.
if (event.target === this.element_ &&
event.animationName === 'progress-center-toggle') {
this.element_.classList.remove('animated');
return;
}
};
/**
* Handles the click event.
* @param {Event} event Click event.
* @private
*/
ProgressCenterPanel.prototype.onClick_ = function(event) {
// Toggle button.
if (event.target.classList.contains('open') ||
event.target.classList.contains('close')) {
// If the progress center has already animated, just return.
if (this.element_.classList.contains('animated'))
return;
// Obtains current and target height.
var currentHeight;
var targetHeight;
if (this.element_.classList.contains('opened')) {
currentHeight = this.openView_.getBoundingClientRect().height;
targetHeight = this.closeView_.getBoundingClientRect().height;
} else {
currentHeight = this.closeView_.getBoundingClientRect().height;
targetHeight = this.openView_.getBoundingClientRect().height;
}
// Set styles for animation.
this.toggleAnimation_.cssRules[0].style.height = currentHeight + 'px';
this.toggleAnimation_.cssRules[1].style.height = targetHeight + 'px';
this.element_.classList.add('animated');
this.element_.classList.toggle('opened');
return;
}
// Cancel button.
if (event.target.classList.contains('cancel')) {
var itemElement = event.target.parentNode.parentNode;
if (this.cancelCallback) {
var id = itemElement.getAttribute('data-progress-id');
this.cancelCallback(id);
}
}
};