• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5'use strict';
6
7/**
8 * Item element of the progress center.
9 * @param {HTMLDocument} document Document which the new item belongs to.
10 * @constructor
11 */
12function ProgressCenterItemElement(document) {
13  var label = document.createElement('label');
14  label.className = 'label';
15
16  var progressBarIndicator = document.createElement('div');
17  progressBarIndicator.className = 'progress-track';
18
19  var progressBar = document.createElement('div');
20  progressBar.className = 'progress-bar';
21  progressBar.appendChild(progressBarIndicator);
22
23  var progressFrame = document.createElement('div');
24  progressFrame.className = 'progress-frame';
25  progressFrame.appendChild(label);
26  progressFrame.appendChild(progressBar);
27
28  var cancelButton = document.createElement('button');
29  cancelButton.className = 'cancel';
30  cancelButton.setAttribute('tabindex', '-1');
31
32  var buttonFrame = document.createElement('div');
33  buttonFrame.className = 'button-frame';
34  buttonFrame.appendChild(cancelButton);
35
36  var itemElement = document.createElement('li');
37  itemElement.appendChild(progressFrame);
38  itemElement.appendChild(buttonFrame);
39
40  return ProgressCenterItemElement.decorate(itemElement);
41}
42
43/**
44 * Ensures the animation triggers.
45 *
46 * @param {function()} callback Function to set the transition end properties.
47 * @return {function()} Function to cancel the request.
48 * @private
49 */
50ProgressCenterItemElement.safelySetAnimation_ = function(callback) {
51  var requestId = requestAnimationFrame(function() {
52    // The transition start properties currently set are rendered at this frame.
53    // And the transition end properties set by the callback is rendered at the
54    // next frame.
55    requestId = requestAnimationFrame(callback);
56  });
57  return function() {
58    cancelAnimationFrame(requestId);
59  };
60};
61
62/**
63 * Event triggered when the item should be dismissed.
64 * @type {string}
65 * @const
66 */
67ProgressCenterItemElement.PROGRESS_ANIMATION_END_EVENT = 'progressAnimationEnd';
68
69/**
70 * Decorates the given element as a progress item.
71 * @param {HTMLElement} element Item to be decorated.
72 * @return {ProgressCenterItemElement} Decorated item.
73 */
74ProgressCenterItemElement.decorate = function(element) {
75  element.__proto__ = ProgressCenterItemElement.prototype;
76  element.state_ = ProgressItemState.PROGRESSING;
77  element.track_ = element.querySelector('.progress-track');
78  element.track_.addEventListener('webkitTransitionEnd',
79                                  element.onTransitionEnd_.bind(element));
80  element.cancelTransition_ = null;
81  return element;
82};
83
84ProgressCenterItemElement.prototype = {
85  __proto__: HTMLDivElement.prototype,
86  get quiet() {
87    return this.classList.contains('quiet');
88  }
89};
90
91/**
92 * Updates the element view according to the item.
93 * @param {ProgressCenterItem} item Item to be referred for the update.
94 * @param {boolean} animated Whether the progress width is applied as animated
95 *     or not.
96 */
97ProgressCenterItemElement.prototype.update = function(item, animated) {
98  // Set element attributes.
99  this.state_ = item.state;
100  this.setAttribute('data-progress-id', item.id);
101  this.classList.toggle('error', item.state === ProgressItemState.ERROR);
102  this.classList.toggle('cancelable', item.cancelable);
103  this.classList.toggle('single', item.single);
104  this.classList.toggle('quiet', item.quiet);
105
106  // Set label.
107  if (this.state_ === ProgressItemState.PROGRESSING ||
108      this.state_ === ProgressItemState.ERROR) {
109    this.querySelector('label').textContent = item.message;
110  } else if (this.state_ === ProgressItemState.CANCELED) {
111    this.querySelector('label').textContent = '';
112  }
113
114  // Cancel the previous property set.
115  if (this.cancelTransition_) {
116    this.cancelTransition_();
117    this.cancelTransition_ = null;
118  }
119
120  // Set track width.
121  var setWidth = function(nextWidthFrame) {
122    var currentWidthRate = parseInt(this.track_.style.width);
123    // Prevent assigning the same width to avoid stopping the animation.
124    // animated == false may be intended to cancel the animation, so in that
125    // case, the assignment should be done.
126    if (currentWidthRate === nextWidthFrame && animated)
127      return;
128    this.track_.hidden = false;
129    this.track_.style.width = nextWidthFrame + '%';
130    this.track_.classList.toggle('animated', animated);
131  }.bind(this, item.progressRateInPercent);
132
133  if (animated) {
134    this.cancelTransition_ =
135        ProgressCenterItemElement.safelySetAnimation_(setWidth);
136  } else {
137    // For animated === false, we should call setWidth immediately to cancel the
138    // animation, otherwise the animation may complete before canceling it.
139    setWidth();
140  }
141};
142
143/**
144 * Resets the item.
145 */
146ProgressCenterItemElement.prototype.reset = function() {
147  this.track_.hidden = true;
148  this.track_.width = '';
149  this.state_ = ProgressItemState.PROGRESSING;
150};
151
152/**
153 * Handles transition end events.
154 * @param {Event} event Transition end event.
155 * @private
156 */
157ProgressCenterItemElement.prototype.onTransitionEnd_ = function(event) {
158  if (event.propertyName !== 'width')
159    return;
160  this.track_.classList.remove('animated');
161  this.dispatchEvent(new Event(
162      ProgressCenterItemElement.PROGRESS_ANIMATION_END_EVENT,
163      {bubbles: true}));
164};
165
166/**
167 * Progress center panel.
168 *
169 * @param {HTMLElement} element DOM Element of the process center panel.
170 * @constructor
171 */
172function ProgressCenterPanel(element) {
173  /**
174   * Root element of the progress center.
175   * @type {HTMLElement}
176   * @private
177   */
178  this.element_ = element;
179
180  /**
181   * Open view containing multiple progress items.
182   * @type {HTMLElement}
183   * @private
184   */
185  this.openView_ = this.element_.querySelector('#progress-center-open-view');
186
187  /**
188   * Close view that is a summarized progress item.
189   * @type {HTMLElement}
190   * @private
191   */
192  this.closeView_ = ProgressCenterItemElement.decorate(
193      this.element_.querySelector('#progress-center-close-view'));
194
195  /**
196   * Toggle animation rule of the progress center.
197   * @type {CSSKeyFrameRule}
198   * @private
199   */
200  this.toggleAnimation_ = ProgressCenterPanel.getToggleAnimation_(
201      element.ownerDocument);
202
203  /**
204   * Item group for normal priority items.
205   * @type {ProgressCenterItemGroup}
206   * @private
207   */
208  this.normalItemGroup_ = new ProgressCenterItemGroup('normal', false);
209
210  /**
211   * Item group for low priority items.
212   * @type {ProgressCenterItemGroup}
213   * @private
214   */
215  this.quietItemGroup_ = new ProgressCenterItemGroup('quiet', true);
216
217  /**
218   * Queries to obtains items for each group.
219   * @type {Object.<string, string>}
220   * @private
221   */
222  this.itemQuery_ = Object.seal({
223    normal: 'li:not(.quiet)',
224    quiet: 'li.quiet'
225  });
226
227  /**
228   * Timeout IDs of the inactive state of each group.
229   * @type {Object.<string, number?>}
230   * @private
231   */
232  this.timeoutId_ = Object.seal({
233    normal: null,
234    quiet: null
235  });
236
237  /**
238   * Callback to becalled with the ID of the progress item when the cancel
239   * button is clicked.
240   */
241  this.cancelCallback = null;
242
243  Object.seal(this);
244
245  // Register event handlers.
246  element.addEventListener('click', this.onClick_.bind(this));
247  element.addEventListener(
248      'webkitAnimationEnd', this.onToggleAnimationEnd_.bind(this));
249  element.addEventListener(
250      ProgressCenterItemElement.PROGRESS_ANIMATION_END_EVENT,
251      this.onItemAnimationEnd_.bind(this));
252}
253
254/**
255 * Obtains the toggle animation keyframes rule from the document.
256 * @param {HTMLDocument} document Document containing the rule.
257 * @return {CSSKeyFrameRules} Animation rule.
258 * @private
259 */
260ProgressCenterPanel.getToggleAnimation_ = function(document) {
261  for (var i = 0; i < document.styleSheets.length; i++) {
262    var styleSheet = document.styleSheets[i];
263    for (var j = 0; j < styleSheet.cssRules.length; j++) {
264      var rule = styleSheet.cssRules[j];
265      if (rule.type === CSSRule.WEBKIT_KEYFRAMES_RULE &&
266          rule.name === 'progress-center-toggle') {
267        return rule;
268      }
269    }
270  }
271  throw new Error('The progress-center-toggle rules is not found.');
272};
273
274/**
275 * The default amount of milliseconds time, before a progress item will reset
276 * after the last complete.
277 * @type {number}
278 * @private
279 * @const
280 */
281ProgressCenterPanel.RESET_DELAY_TIME_MS_ = 5000;
282
283/**
284 * Updates an item to the progress center panel.
285 * @param {!ProgressCenterItem} item Item including new contents.
286 */
287ProgressCenterPanel.prototype.updateItem = function(item) {
288  var targetGroup = this.getGroupForItem_(item);
289
290  // Update the item.
291  var oldState = targetGroup.state;
292  targetGroup.update(item);
293  this.handleGroupStateChange_(targetGroup, oldState, targetGroup.state);
294
295  // Update an open view item.
296  var newItem = targetGroup.getItem(item.id);
297  var itemElement = this.getItemElement_(item.id);
298  if (newItem) {
299    if (!itemElement) {
300      itemElement = new ProgressCenterItemElement(this.element_.ownerDocument);
301      this.openView_.insertBefore(itemElement, this.openView_.firstNode);
302    }
303    itemElement.update(newItem, targetGroup.isAnimated(item.id));
304  } else {
305    if (itemElement)
306      itemElement.parentNode.removeChild(itemElement);
307  }
308
309  // Update the close view.
310  this.updateCloseView_();
311};
312
313/**
314 * Handles the item animation end.
315 * @param {Event} event Item animation end event.
316 * @private
317 */
318ProgressCenterPanel.prototype.onItemAnimationEnd_ = function(event) {
319  var targetGroup = event.target.classList.contains('quiet') ?
320      this.quietItemGroup_ : this.normalItemGroup_;
321  var oldState = targetGroup.state;
322  if (event.target === this.closeView_) {
323    targetGroup.completeSummarizedItemAnimation();
324  } else {
325    var itemId = event.target.getAttribute('data-progress-id');
326    targetGroup.completeItemAnimation(itemId);
327    var newItem = targetGroup.getItem(itemId);
328    var itemElement = this.getItemElement_(itemId);
329    if (!newItem && itemElement)
330      itemElement.parentNode.removeChild(itemElement);
331  }
332  this.handleGroupStateChange_(targetGroup, oldState, targetGroup.state);
333  this.updateCloseView_();
334};
335
336/**
337 * Handles the state change of group.
338 * @param {ProgressCenterItemGroup} group Item group.
339 * @param {ProgressCenterItemGroup.State} oldState Old state of the group.
340 * @param {ProgressCenterItemGroup.State} newState New state of the group.
341 * @private
342 */
343ProgressCenterPanel.prototype.handleGroupStateChange_ =
344    function(group, oldState, newState) {
345  if (oldState === ProgressCenterItemGroup.State.INACTIVE) {
346    clearTimeout(this.timeoutId_[group.name]);
347    this.timeoutId_[group.name] = null;
348    var elements =
349        this.openView_.querySelectorAll(this.itemQuery_[group.name]);
350    for (var i = 0; i < elements.length; i++) {
351      elements[i].parentNode.removeChild(elements[i]);
352    }
353  }
354  if (newState === ProgressCenterItemGroup.State.INACTIVE) {
355    this.timeoutId_[group.name] = setTimeout(function() {
356      var inOldState = group.state;
357      group.endInactive();
358      this.handleGroupStateChange_(group, inOldState, group.state);
359      this.updateCloseView_();
360    }.bind(this), ProgressCenterPanel.RESET_DELAY_TIME_MS_);
361  }
362};
363
364/**
365 * Updates the close view.
366 * @private
367 */
368ProgressCenterPanel.prototype.updateCloseView_ = function() {
369  // Try to use the normal summarized item.
370  var normalSummarizedItem =
371      this.normalItemGroup_.getSummarizedItem(this.quietItemGroup_.numErrors);
372  if (normalSummarizedItem) {
373    // If the quiet animation is overrided by normal summarized item, discard
374    // the quiet animation.
375    if (this.quietItemGroup_.isSummarizedAnimated()) {
376      var oldState = this.quietItemGroup_.state;
377      this.quietItemGroup_.completeSummarizedItemAnimation();
378      this.handleGroupStateChange_(this.quietItemGroup_,
379                                   oldState,
380                                   this.quietItemGroup_.state);
381    }
382
383    // Update the view state.
384    this.closeView_.update(normalSummarizedItem,
385                           this.normalItemGroup_.isSummarizedAnimated());
386    this.element_.hidden = false;
387    return;
388  }
389
390  // Try to use the quiet summarized item.
391  var quietSummarizedItem =
392      this.quietItemGroup_.getSummarizedItem(this.normalItemGroup_.numErrors);
393  if (quietSummarizedItem) {
394    this.closeView_.update(quietSummarizedItem,
395                           this.quietItemGroup_.isSummarizedAnimated());
396    this.element_.hidden = false;
397    return;
398  }
399
400  // Try to use the error summarized item.
401  var errorSummarizedItem = ProgressCenterItemGroup.getSummarizedErrorItem(
402      this.normalItemGroup_, this.quietItemGroup_);
403  if (errorSummarizedItem) {
404    this.closeView_.update(errorSummarizedItem, false);
405    this.element_.hidden = false;
406    return;
407  }
408
409  // Hide the progress center because there is no items to show.
410  this.closeView_.reset();
411  this.element_.hidden = true;
412  this.element_.classList.remove('opened');
413};
414
415/**
416 * Gets an item element having the specified ID.
417 * @param {string} id progress item ID.
418 * @return {HTMLElement} Item element having the ID.
419 * @private
420 */
421ProgressCenterPanel.prototype.getItemElement_ = function(id) {
422  var query = 'li[data-progress-id="' + id + '"]';
423  return this.openView_.querySelector(query);
424};
425
426/**
427 * Obtains the group for the item.
428 * @param {ProgressCenterItem} item Progress item.
429 * @return {ProgressCenterItemGroup} Item group that should contain the item.
430 * @private
431 */
432ProgressCenterPanel.prototype.getGroupForItem_ = function(item) {
433  return item.quiet ? this.quietItemGroup_ : this.normalItemGroup_;
434};
435
436/**
437 * Handles the animation end event of the progress center.
438 * @param {Event} event Animation end event.
439 * @private
440 */
441ProgressCenterPanel.prototype.onToggleAnimationEnd_ = function(event) {
442  // Transition end of the root element's height.
443  if (event.target === this.element_ &&
444      event.animationName === 'progress-center-toggle') {
445    this.element_.classList.remove('animated');
446    return;
447  }
448};
449
450/**
451 * Handles the click event.
452 * @param {Event} event Click event.
453 * @private
454 */
455ProgressCenterPanel.prototype.onClick_ = function(event) {
456  // Toggle button.
457  if (event.target.classList.contains('open') ||
458      event.target.classList.contains('close')) {
459    // If the progress center has already animated, just return.
460    if (this.element_.classList.contains('animated'))
461      return;
462
463    // Obtains current and target height.
464    var currentHeight;
465    var targetHeight;
466    if (this.element_.classList.contains('opened')) {
467      currentHeight = this.openView_.getBoundingClientRect().height;
468      targetHeight = this.closeView_.getBoundingClientRect().height;
469    } else {
470      currentHeight = this.closeView_.getBoundingClientRect().height;
471      targetHeight = this.openView_.getBoundingClientRect().height;
472    }
473
474    // Set styles for animation.
475    this.toggleAnimation_.cssRules[0].style.height = currentHeight + 'px';
476    this.toggleAnimation_.cssRules[1].style.height = targetHeight + 'px';
477    this.element_.classList.add('animated');
478    this.element_.classList.toggle('opened');
479    return;
480  }
481
482  // Cancel button.
483  if (event.target.classList.contains('cancel')) {
484    var itemElement = event.target.parentNode.parentNode;
485    if (this.cancelCallback) {
486      var id = itemElement.getAttribute('data-progress-id');
487      this.cancelCallback(id);
488    }
489  }
490};
491