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 * Progress center panel. 9 * 10 * @param {HTMLElement} element DOM Element of the process center panel. 11 * @constructor 12 */ 13var ProgressCenterPanel = function(element) { 14 /** 15 * Root element of the progress center. 16 * @type {!HTMLElement} 17 * @private 18 */ 19 this.element_ = element; 20 21 /** 22 * Open view containing multiple progress items. 23 * @type {!HTMLElement} 24 * @private 25 */ 26 this.openView_ = this.element_.querySelector('#progress-center-open-view'); 27 28 /** 29 * Close view that is a summarized progress item. 30 * @type {!HTMLElement} 31 * @private 32 */ 33 this.closeView_ = this.element_.querySelector('#progress-center-close-view'); 34 35 /** 36 * Toggle animation rule of the progress center. 37 * @type {!CSSKeyFrameRule} 38 * @private 39 */ 40 this.toggleAnimation_ = ProgressCenterPanel.getToggleAnimation_( 41 element.ownerDocument); 42 43 /** 44 * Reset is requested but it is pending until the transition of progress bar 45 * is complete. 46 * @type {boolean} 47 * @private 48 */ 49 this.resetRequested_ = false; 50 51 /** 52 * Callback to becalled with the ID of the progress item when the cancel 53 * button is clicked. 54 */ 55 this.cancelCallback = null; 56 57 Object.seal(this); 58 59 // Register event handlers. 60 element.addEventListener('click', this.onClick_.bind(this)); 61 element.addEventListener( 62 'webkitAnimationEnd', this.onToggleAnimationEnd_.bind(this)); 63 element.addEventListener( 64 'webkitTransitionEnd', this.onItemTransitionEnd_.bind(this)); 65}; 66 67/** 68 * Updates attributes of the item element. 69 * @param {!HTMLElement} element Element to be updated. 70 * @param {!ProgressCenterItem} item Progress center item. 71 * @private 72 */ 73ProgressCenterPanel.updateItemElement_ = function(element, item) { 74 // Sets element attributes. 75 element.setAttribute('data-progress-id', item.id); 76 element.classList.toggle('error', item.state === ProgressItemState.ERROR); 77 element.classList.toggle('cancelable', item.cancelable); 78 79 // Only when the previousWidthRate is not NaN (when style width is already 80 // set) and the progress rate increases, we use transition animation. 81 var previousWidthRate = 82 parseInt(element.querySelector('.progress-track').style.width); 83 var targetWidthRate = item.progressRateInPercent; 84 var animation = !isNaN(previousWidthRate) && 85 previousWidthRate < targetWidthRate; 86 if (item.state === ProgressItemState.COMPLETED && animation) { 87 // The attribute pre-complete means that the actual operation is already 88 // done but the UI transition of progress bar is not complete. 89 element.setAttribute('pre-complete', ''); 90 } else { 91 element.querySelector('label').textContent = item.message; 92 } 93 94 // To commit the property change and to trigger the transition even if the 95 // change is done synchronously, assign the width value asynchronously. 96 var updateTrackWidth = function() { 97 var track = element.querySelector('.progress-track'); 98 track.classList.toggle('animated', animation); 99 track.style.width = targetWidthRate + '%'; 100 track.hidden = false; 101 }; 102 if (animation) 103 setTimeout(updateTrackWidth); 104 else 105 updateTrackWidth(); 106}; 107 108/** 109 * Obtains the toggle animation keyframes rule from the document. 110 * @param {HTMLDocument} document Document containing the rule. 111 * @return {CSSKeyFrameRules} Animation rule. 112 * @private 113 */ 114ProgressCenterPanel.getToggleAnimation_ = function(document) { 115 for (var i = 0; i < document.styleSheets.length; i++) { 116 var styleSheet = document.styleSheets[i]; 117 for (var j = 0; j < styleSheet.cssRules.length; j++) { 118 var rule = styleSheet.cssRules[j]; 119 if (rule.type === CSSRule.WEBKIT_KEYFRAMES_RULE && 120 rule.name === 'progress-center-toggle') { 121 return rule; 122 } 123 } 124 } 125 throw new Error('The progress-center-toggle rules is not found.'); 126}; 127 128/** 129 * Updates an item to the progress center panel. 130 * @param {!ProgressCenterItem} item Item including new contents. 131 */ 132ProgressCenterPanel.prototype.updateItem = function(item) { 133 // If reset is requested, force to reset. 134 if (this.resetRequested_) 135 this.reset(true); 136 137 var itemElement = this.getItemElement_(item.id); 138 139 // Check whether the item should be displayed or not by referring its state. 140 switch (item.state) { 141 // Should show the item. 142 case ProgressItemState.PROGRESSING: 143 case ProgressItemState.ERROR: 144 // If the item has not been added yet, create a new element and add it. 145 if (!itemElement) { 146 itemElement = this.createNewItemElement_(); 147 this.openView_.insertBefore(itemElement, this.openView_.firstNode); 148 } 149 150 // Update the element by referring the item model. 151 ProgressCenterPanel.updateItemElement_(itemElement, item); 152 this.element_.hidden = false; 153 break; 154 155 // Should not show the item. 156 case ProgressItemState.COMPLETED: 157 case ProgressItemState.CANCELED: 158 // If itemElement is not shown, just break. 159 if (!itemElement) 160 break; 161 162 // If the item is complete state, once update it because it may turn to 163 // have the pre-complete attribute. 164 if (item.state === ProgressItemState.COMPLETED) 165 ProgressCenterPanel.updateItemElement_(itemElement, item); 166 167 // If the item has the pre-complete attribute, keep showing it. Otherwise, 168 // just remove it. 169 if (item.state !== ProgressItemState.COMPLETED || 170 !itemElement.hasAttribute('pre-complete')) { 171 this.openView_.removeChild(itemElement); 172 } 173 break; 174 } 175}; 176 177/** 178 * Updates close showing summarized item. 179 * @param {!ProgressCenterItem} summarizedItem Item to be displayed in the close 180 * view. 181 */ 182ProgressCenterPanel.prototype.updateCloseView = function(summarizedItem) { 183 this.closeView_.classList.toggle('single', !summarizedItem.summarized); 184 ProgressCenterPanel.updateItemElement_(this.closeView_, summarizedItem); 185}; 186 187/** 188 * Remove all the items. 189 * @param {boolean=} opt_force True if we force to reset and do not wait the 190 * transition of progress bar. False otherwise. False is default. 191 */ 192ProgressCenterPanel.prototype.reset = function(opt_force) { 193 if (!opt_force && this.element_.querySelector('[pre-complete]')) { 194 this.resetRequested_ = true; 195 return; 196 } 197 198 // Clear the flag. 199 this.resetRequested_ = false; 200 201 // Clear the all compete item. 202 this.openView_.innerHTML = ''; 203 204 // Clear track width of close view. 205 this.closeView_.querySelector('.progress-track').style.width = ''; 206 207 // Hide the progress center. 208 this.element_.hidden = true; 209 this.closeView_.querySelector('.progress-track').hidden = true; 210 this.element_.classList.remove('opened'); 211}; 212 213/** 214 * Gets an item element having the specified ID. 215 * @param {string} id progress item ID. 216 * @return {HTMLElement} Item element having the ID. 217 * @private 218 */ 219ProgressCenterPanel.prototype.getItemElement_ = function(id) { 220 var query = 'li[data-progress-id="' + id + '"]'; 221 return this.openView_.querySelector(query); 222}; 223 224/** 225 * Creates an item element. 226 * @return {HTMLElement} Created item element. 227 * @private 228 */ 229ProgressCenterPanel.prototype.createNewItemElement_ = function() { 230 var label = this.element_.ownerDocument.createElement('label'); 231 label.className = 'label'; 232 233 var progressBarIndicator = this.element_.ownerDocument.createElement('div'); 234 progressBarIndicator.className = 'progress-track'; 235 236 var progressBar = this.element_.ownerDocument.createElement('div'); 237 progressBar.className = 'progress-bar'; 238 progressBar.appendChild(progressBarIndicator); 239 240 var cancelButton = this.element_.ownerDocument.createElement('button'); 241 cancelButton.className = 'cancel'; 242 cancelButton.setAttribute('tabindex', '-1'); 243 244 var progressFrame = this.element_.ownerDocument.createElement('div'); 245 progressFrame.className = 'progress-frame'; 246 progressFrame.appendChild(progressBar); 247 progressFrame.appendChild(cancelButton); 248 249 var itemElement = this.element_.ownerDocument.createElement('li'); 250 itemElement.appendChild(label); 251 itemElement.appendChild(progressFrame); 252 253 return itemElement; 254}; 255 256/** 257 * Handles the animation end event of the progress center. 258 * @param {Event} event Animation end event. 259 * @private 260 */ 261ProgressCenterPanel.prototype.onToggleAnimationEnd_ = function(event) { 262 // Transition end of the root element's height. 263 if (event.target === this.element_ && 264 event.animationName === 'progress-center-toggle') { 265 this.element_.classList.remove('animated'); 266 return; 267 } 268}; 269 270/** 271 * Handles the transition end event of items. 272 * @param {Event} event Transition end event. 273 * @private 274 */ 275ProgressCenterPanel.prototype.onItemTransitionEnd_ = function(event) { 276 var itemElement = event.target.parentNode.parentNode.parentNode; 277 if (!itemElement.hasAttribute('pre-complete') || 278 event.propertyName !== 'width') 279 return; 280 if (itemElement !== this.closeView_) 281 this.openView_.removeChild(itemElement); 282 itemElement.removeAttribute('pre-complete'); 283 284 if (this.resetRequested_) 285 this.reset(); 286}; 287 288/** 289 * Handles the click event. 290 * @param {Event} event Click event. 291 * @private 292 */ 293ProgressCenterPanel.prototype.onClick_ = function(event) { 294 // Toggle button. 295 if (event.target.classList.contains('toggle') && 296 (!this.closeView_.classList.contains('single') || 297 this.element_.classList.contains('opened'))) { 298 299 // If the progress center has already animated, just return. 300 if (this.element_.classList.contains('animated')) 301 return; 302 303 // Obtains current and target height. 304 var currentHeight; 305 var targetHeight; 306 if (this.element_.classList.contains('opened')) { 307 currentHeight = this.openView_.getBoundingClientRect().height; 308 targetHeight = this.closeView_.getBoundingClientRect().height; 309 } else { 310 currentHeight = this.closeView_.getBoundingClientRect().height; 311 targetHeight = this.openView_.getBoundingClientRect().height; 312 } 313 314 // Set styles for animation. 315 this.toggleAnimation_.cssRules[0].style.height = currentHeight + 'px'; 316 this.toggleAnimation_.cssRules[1].style.height = targetHeight + 'px'; 317 this.element_.classList.add('animated'); 318 this.element_.classList.toggle('opened'); 319 return; 320 } 321 322 // Cancel button. 323 if (this.cancelCallback) { 324 var id = event.target.classList.contains('toggle') ? 325 this.closeView_.getAttribute('data-progress-id') : 326 event.target.parentNode.parentNode.getAttribute('data-progress-id'); 327 this.cancelCallback(id); 328 } 329}; 330