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