| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2012 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 * Namespace for utility functions. | |
| 9 */ | |
| 10 var filelist = {}; | |
| 11 | |
| 12 /** | |
| 13 * Custom column model for advanced auto-resizing. | |
| 14 * | |
| 15 * @param {Array.<cr.ui.table.TableColumn>} tableColumns Table columns. | |
| 16 * @extends {cr.ui.table.TableColumnModel} | |
| 17 * @constructor | |
| 18 */ | |
| 19 function FileTableColumnModel(tableColumns) { | |
| 20 cr.ui.table.TableColumnModel.call(this, tableColumns); | |
| 21 } | |
| 22 | |
| 23 /** | |
| 24 * The columns whose index is less than the constant are resizable. | |
| 25 * @const | |
| 26 * @type {number} | |
| 27 * @private | |
| 28 */ | |
| 29 FileTableColumnModel.RESIZABLE_LENGTH_ = 4; | |
| 30 | |
| 31 /** | |
| 32 * Inherits from cr.ui.TableColumnModel. | |
| 33 */ | |
| 34 FileTableColumnModel.prototype.__proto__ = | |
| 35 cr.ui.table.TableColumnModel.prototype; | |
| 36 | |
| 37 /** | |
| 38 * Minimum width of column. | |
| 39 * @const | |
| 40 * @type {number} | |
| 41 * @private | |
| 42 */ | |
| 43 FileTableColumnModel.MIN_WIDTH_ = 10; | |
| 44 | |
| 45 /** | |
| 46 * Sets column width so that the column dividers move to the specified position. | |
| 47 * This function also check the width of each column and keep the width larger | |
| 48 * than MIN_WIDTH_. | |
| 49 * | |
| 50 * @private | |
| 51 * @param {Array.<number>} newPos Positions of each column dividers. | |
| 52 */ | |
| 53 FileTableColumnModel.prototype.applyColumnPositions_ = function(newPos) { | |
| 54 // Check the minimum width and adjust the positions. | |
| 55 for (var i = 0; i < newPos.length - 2; i++) { | |
| 56 if (newPos[i + 1] - newPos[i] < FileTableColumnModel.MIN_WIDTH_) { | |
| 57 newPos[i + 1] = newPos[i] + FileTableColumnModel.MIN_WIDTH_; | |
| 58 } | |
| 59 } | |
| 60 for (var i = newPos.length - 1; i >= 2; i--) { | |
| 61 if (newPos[i] - newPos[i - 1] < FileTableColumnModel.MIN_WIDTH_) { | |
| 62 newPos[i - 1] = newPos[i] - FileTableColumnModel.MIN_WIDTH_; | |
| 63 } | |
| 64 } | |
| 65 // Set the new width of columns | |
| 66 for (var i = 0; i < FileTableColumnModel.RESIZABLE_LENGTH_; i++) { | |
| 67 this.columns_[i].width = newPos[i + 1] - newPos[i]; | |
| 68 } | |
| 69 }; | |
| 70 | |
| 71 /** | |
| 72 * Normalizes widths to make their sum 100% if possible. Uses the proportional | |
| 73 * approach with some additional constraints. | |
| 74 * | |
| 75 * @param {number} contentWidth Target width. | |
| 76 * @override | |
| 77 */ | |
| 78 FileTableColumnModel.prototype.normalizeWidths = function(contentWidth) { | |
| 79 var totalWidth = 0; | |
| 80 var fixedWidth = 0; | |
| 81 // Some columns have fixed width. | |
| 82 for (var i = 0; i < this.columns_.length; i++) { | |
| 83 if (i < FileTableColumnModel.RESIZABLE_LENGTH_) | |
| 84 totalWidth += this.columns_[i].width; | |
| 85 else | |
| 86 fixedWidth += this.columns_[i].width; | |
| 87 } | |
| 88 var newTotalWidth = Math.max(contentWidth - fixedWidth, 0); | |
| 89 var positions = [0]; | |
| 90 var sum = 0; | |
| 91 for (var i = 0; i < FileTableColumnModel.RESIZABLE_LENGTH_; i++) { | |
| 92 var column = this.columns_[i]; | |
| 93 sum += column.width; | |
| 94 // Faster alternative to Math.floor for non-negative numbers. | |
| 95 positions[i + 1] = ~~(newTotalWidth * sum / totalWidth); | |
| 96 } | |
| 97 this.applyColumnPositions_(positions); | |
| 98 }; | |
| 99 | |
| 100 /** | |
| 101 * Handles to the start of column resizing by splitters. | |
| 102 */ | |
| 103 FileTableColumnModel.prototype.handleSplitterDragStart = function() { | |
| 104 this.columnPos_ = [0]; | |
| 105 for (var i = 0; i < this.columns_.length; i++) { | |
| 106 this.columnPos_[i + 1] = this.columns_[i].width + this.columnPos_[i]; | |
| 107 } | |
| 108 }; | |
| 109 | |
| 110 /** | |
| 111 * Handles to the end of column resizing by splitters. | |
| 112 */ | |
| 113 FileTableColumnModel.prototype.handleSplitterDragEnd = function() { | |
| 114 this.columnPos_ = null; | |
| 115 }; | |
| 116 | |
| 117 /** | |
| 118 * Sets the width of column with keeping the total width of table. | |
| 119 * @param {number} columnIndex Index of column that is resized. | |
| 120 * @param {number} columnWidth New width of the column. | |
| 121 */ | |
| 122 FileTableColumnModel.prototype.setWidthAndKeepTotal = function( | |
| 123 columnIndex, columnWidth) { | |
| 124 // Skip to resize 'selection' column | |
| 125 if (columnIndex < 0 || | |
| 126 columnIndex >= FileTableColumnModel.RESIZABLE_LENGTH_ || | |
| 127 !this.columnPos_) { | |
| 128 return; | |
| 129 } | |
| 130 | |
| 131 // Calculate new positions of column splitters. | |
| 132 var newPosStart = | |
| 133 this.columnPos_[columnIndex] + Math.max(columnWidth, | |
| 134 FileTableColumnModel.MIN_WIDTH_); | |
| 135 var newPos = []; | |
| 136 var posEnd = this.columnPos_[FileTableColumnModel.RESIZABLE_LENGTH_]; | |
| 137 for (var i = 0; i < columnIndex + 1; i++) { | |
| 138 newPos[i] = this.columnPos_[i]; | |
| 139 } | |
| 140 for (var i = columnIndex + 1; | |
| 141 i < FileTableColumnModel.RESIZABLE_LENGTH_; | |
| 142 i++) { | |
| 143 var posStart = this.columnPos_[columnIndex + 1]; | |
| 144 newPos[i] = (posEnd - newPosStart) * | |
| 145 (this.columnPos_[i] - posStart) / | |
| 146 (posEnd - posStart) + | |
| 147 newPosStart; | |
| 148 // Faster alternative to Math.floor for non-negative numbers. | |
| 149 newPos[i] = ~~newPos[i]; | |
| 150 } | |
| 151 newPos[columnIndex] = this.columnPos_[columnIndex]; | |
| 152 newPos[FileTableColumnModel.RESIZABLE_LENGTH_] = posEnd; | |
| 153 this.applyColumnPositions_(newPos); | |
| 154 | |
| 155 // Notifiy about resizing | |
| 156 cr.dispatchSimpleEvent(this, 'resize'); | |
| 157 }; | |
| 158 | |
| 159 /** | |
| 160 * Custom splitter that resizes column with retaining the sum of all the column | |
| 161 * width. | |
| 162 */ | |
| 163 var FileTableSplitter = cr.ui.define('div'); | |
| 164 | |
| 165 /** | |
| 166 * Inherits from cr.ui.TableSplitter. | |
| 167 */ | |
| 168 FileTableSplitter.prototype.__proto__ = cr.ui.TableSplitter.prototype; | |
| 169 | |
| 170 /** | |
| 171 * Handles the drag start event. | |
| 172 */ | |
| 173 FileTableSplitter.prototype.handleSplitterDragStart = function() { | |
| 174 cr.ui.TableSplitter.prototype.handleSplitterDragStart.call(this); | |
| 175 this.table_.columnModel.handleSplitterDragStart(); | |
| 176 }; | |
| 177 | |
| 178 /** | |
| 179 * Handles the drag move event. | |
| 180 * @param {number} deltaX Horizontal mouse move offset. | |
| 181 */ | |
| 182 FileTableSplitter.prototype.handleSplitterDragMove = function(deltaX) { | |
| 183 this.table_.columnModel.setWidthAndKeepTotal(this.columnIndex, | |
| 184 this.columnWidth_ + deltaX, | |
| 185 true); | |
| 186 }; | |
| 187 | |
| 188 /** | |
| 189 * Handles the drag end event. | |
| 190 */ | |
| 191 FileTableSplitter.prototype.handleSplitterDragEnd = function() { | |
| 192 cr.ui.TableSplitter.prototype.handleSplitterDragEnd.call(this); | |
| 193 this.table_.columnModel.handleSplitterDragEnd(); | |
| 194 }; | |
| 195 | |
| 196 /** | |
| 197 * File list Table View. | |
| 198 * @constructor | |
| 199 */ | |
| 200 function FileTable() { | |
| 201 throw new Error('Designed to decorate elements'); | |
| 202 } | |
| 203 | |
| 204 /** | |
| 205 * Inherits from cr.ui.Table. | |
| 206 */ | |
| 207 FileTable.prototype.__proto__ = cr.ui.Table.prototype; | |
| 208 | |
| 209 /** | |
| 210 * Decorates the element. | |
| 211 * @param {HTMLElement} self Table to decorate. | |
| 212 * @param {MetadataCache} metadataCache To retrieve metadata. | |
| 213 * @param {boolean} fullPage True if it's full page File Manager, | |
| 214 * False if a file open/save dialog. | |
| 215 */ | |
| 216 FileTable.decorate = function(self, metadataCache, fullPage) { | |
| 217 cr.ui.Table.decorate(self); | |
| 218 self.__proto__ = FileTable.prototype; | |
| 219 self.metadataCache_ = metadataCache; | |
| 220 self.collator_ = Intl.Collator([], {numeric: true, sensitivity: 'base'}); | |
| 221 | |
| 222 var columns = [ | |
| 223 new cr.ui.table.TableColumn('name', str('NAME_COLUMN_LABEL'), | |
| 224 fullPage ? 386 : 324), | |
| 225 new cr.ui.table.TableColumn('size', str('SIZE_COLUMN_LABEL'), | |
| 226 110, true), | |
| 227 new cr.ui.table.TableColumn('type', str('TYPE_COLUMN_LABEL'), | |
| 228 fullPage ? 110 : 110), | |
| 229 new cr.ui.table.TableColumn('modificationTime', | |
| 230 str('DATE_COLUMN_LABEL'), | |
| 231 fullPage ? 150 : 210) | |
| 232 ]; | |
| 233 | |
| 234 columns[0].renderFunction = self.renderName_.bind(self); | |
| 235 columns[1].renderFunction = self.renderSize_.bind(self); | |
| 236 columns[1].defaultOrder = 'desc'; | |
| 237 columns[2].renderFunction = self.renderType_.bind(self); | |
| 238 columns[3].renderFunction = self.renderDate_.bind(self); | |
| 239 columns[3].defaultOrder = 'desc'; | |
| 240 | |
| 241 var tableColumnModelClass; | |
| 242 tableColumnModelClass = FileTableColumnModel; | |
| 243 if (self.showCheckboxes) { | |
| 244 columns.push(new cr.ui.table.TableColumn('selection', | |
| 245 '', | |
| 246 50, true)); | |
| 247 columns[4].renderFunction = self.renderSelection_.bind(self); | |
| 248 columns[4].headerRenderFunction = | |
| 249 self.renderSelectionColumnHeader_.bind(self); | |
| 250 columns[4].fixed = true; | |
| 251 } | |
| 252 | |
| 253 var columnModel = Object.create(tableColumnModelClass.prototype, { | |
| 254 /** | |
| 255 * The number of columns. | |
| 256 * @type {number} | |
| 257 */ | |
| 258 size: { | |
| 259 /** | |
| 260 * @this {FileTableColumnModel} | |
| 261 * @return {number} Number of columns. | |
| 262 */ | |
| 263 get: function() { | |
| 264 return this.totalSize; | |
| 265 } | |
| 266 }, | |
| 267 | |
| 268 /** | |
| 269 * The number of columns. | |
| 270 * @type {number} | |
| 271 */ | |
| 272 totalSize: { | |
| 273 /** | |
| 274 * @this {FileTableColumnModel} | |
| 275 * @return {number} Number of columns. | |
| 276 */ | |
| 277 get: function() { | |
| 278 return columns.length; | |
| 279 } | |
| 280 }, | |
| 281 | |
| 282 /** | |
| 283 * Obtains a column by the specified horizontal position. | |
| 284 */ | |
| 285 getHitColumn: { | |
| 286 /** | |
| 287 * @this {FileTableColumnModel} | |
| 288 * @param {number} x Horizontal position. | |
| 289 * @return {object} The object that contains column index, column width, | |
| 290 * and hitPosition where the horizontal position is hit in the column. | |
| 291 */ | |
| 292 value: function(x) { | |
| 293 for (var i = 0; x >= this.columns_[i].width; i++) { | |
| 294 x -= this.columns_[i].width; | |
| 295 } | |
| 296 if (i >= this.columns_.length) | |
| 297 return null; | |
| 298 return {index: i, hitPosition: x, width: this.columns_[i].width}; | |
| 299 } | |
| 300 } | |
| 301 }); | |
| 302 | |
| 303 tableColumnModelClass.call(columnModel, columns); | |
| 304 self.columnModel = columnModel; | |
| 305 self.setDateTimeFormat(true); | |
| 306 self.setRenderFunction(self.renderTableRow_.bind(self, | |
| 307 self.getRenderFunction())); | |
| 308 | |
| 309 self.scrollBar_ = MainPanelScrollBar(); | |
| 310 self.scrollBar_.initialize(self, self.list); | |
| 311 // Keep focus on the file list when clicking on the header. | |
| 312 self.header.addEventListener('mousedown', function(e) { | |
| 313 self.list.focus(); | |
| 314 e.preventDefault(); | |
| 315 }); | |
| 316 | |
| 317 var handleSelectionChange = function() { | |
| 318 var selectAll = self.querySelector('#select-all-checkbox'); | |
| 319 if (selectAll) | |
| 320 self.updateSelectAllCheckboxState_(selectAll); | |
| 321 }; | |
| 322 | |
| 323 self.relayoutAggregation_ = | |
| 324 new AsyncUtil.Aggregation(self.relayoutImmediately_.bind(self)); | |
| 325 | |
| 326 Object.defineProperty(self.list_, 'selectionModel', { | |
| 327 /** | |
| 328 * @this {cr.ui.List} | |
| 329 * @return {cr.ui.ListSelectionModel} The current selection model. | |
| 330 */ | |
| 331 get: function() { | |
| 332 return this.selectionModel_; | |
| 333 }, | |
| 334 /** | |
| 335 * @this {cr.ui.List} | |
| 336 */ | |
| 337 set: function(value) { | |
| 338 var sm = this.selectionModel; | |
| 339 if (sm) | |
| 340 sm.removeEventListener('change', handleSelectionChange); | |
| 341 | |
| 342 util.callInheritedSetter(this, 'selectionModel', value); | |
| 343 sm = value; | |
| 344 | |
| 345 if (sm) | |
| 346 sm.addEventListener('change', handleSelectionChange); | |
| 347 handleSelectionChange(); | |
| 348 } | |
| 349 }); | |
| 350 | |
| 351 // Override header#redraw to use FileTableSplitter. | |
| 352 self.header_.redraw = function() { | |
| 353 this.__proto__.redraw.call(this); | |
| 354 // Extend table splitters | |
| 355 var splitters = this.querySelectorAll('.table-header-splitter'); | |
| 356 for (var i = 0; i < splitters.length; i++) { | |
| 357 if (splitters[i] instanceof FileTableSplitter) | |
| 358 continue; | |
| 359 FileTableSplitter.decorate(splitters[i]); | |
| 360 } | |
| 361 }; | |
| 362 | |
| 363 // Save the last selection. This is used by shouldStartDragSelection. | |
| 364 self.list.addEventListener('mousedown', function(e) { | |
| 365 this.lastSelection_ = this.selectionModel.selectedIndexes; | |
| 366 }.bind(self), true); | |
| 367 self.list.shouldStartDragSelection = | |
| 368 self.shouldStartDragSelection_.bind(self); | |
| 369 | |
| 370 /** | |
| 371 * Obtains the index list of elements that are hit by the point or the | |
| 372 * rectangle. | |
| 373 * | |
| 374 * @param {number} x X coordinate value. | |
| 375 * @param {number} y Y coordinate value. | |
| 376 * @param {=number} opt_width Width of the coordinate. | |
| 377 * @param {=number} opt_height Height of the coordinate. | |
| 378 * @return {Array.<number>} Index list of hit elements. | |
| 379 */ | |
| 380 self.list.getHitElements = function(x, y, opt_width, opt_height) { | |
| 381 var currentSelection = []; | |
| 382 var bottom = y + (opt_height || 0); | |
| 383 for (var i = 0; i < this.selectionModel_.length; i++) { | |
| 384 var itemMetrics = this.getHeightsForIndex_(i); | |
| 385 if (itemMetrics.top < bottom && itemMetrics.top + itemMetrics.height >= y) | |
| 386 currentSelection.push(i); | |
| 387 } | |
| 388 return currentSelection; | |
| 389 }; | |
| 390 }; | |
| 391 | |
| 392 /** | |
| 393 * Sets date and time format. | |
| 394 * @param {boolean} use12hourClock True if 12 hours clock, False if 24 hours. | |
| 395 */ | |
| 396 FileTable.prototype.setDateTimeFormat = function(use12hourClock) { | |
| 397 this.timeFormatter_ = Intl.DateTimeFormat( | |
| 398 [] /* default locale */, | |
| 399 {hour: 'numeric', minute: 'numeric', | |
| 400 hour12: use12hourClock}); | |
| 401 this.dateFormatter_ = Intl.DateTimeFormat( | |
| 402 [] /* default locale */, | |
| 403 {year: 'numeric', month: 'short', day: 'numeric', | |
| 404 hour: 'numeric', minute: 'numeric', | |
| 405 hour12: use12hourClock}); | |
| 406 }; | |
| 407 | |
| 408 /** | |
| 409 * Obtains if the drag selection should be start or not by referring the mouse | |
| 410 * event. | |
| 411 * @param {MouseEvent} event Drag start event. | |
| 412 * @return {boolean} True if the mouse is hit to the background of the list. | |
| 413 * @private | |
| 414 */ | |
| 415 FileTable.prototype.shouldStartDragSelection_ = function(event) { | |
| 416 // If the shift key is pressed, it should starts drag selection. | |
| 417 if (event.shiftKey) | |
| 418 return true; | |
| 419 | |
| 420 // If the position values are negative, it points the out of list. | |
| 421 // It should start the drag selection. | |
| 422 var pos = DragSelector.getScrolledPosition(this.list, event); | |
| 423 if (!pos) | |
| 424 return false; | |
| 425 if (pos.x < 0 || pos.y < 0) | |
| 426 return true; | |
| 427 | |
| 428 // If the item index is out of range, it should start the drag selection. | |
| 429 var itemHeight = this.list.measureItem().height; | |
| 430 // Faster alternative to Math.floor for non-negative numbers. | |
| 431 var itemIndex = ~~(pos.y / itemHeight); | |
| 432 if (itemIndex >= this.list.dataModel.length) | |
| 433 return true; | |
| 434 | |
| 435 // If the pointed item is already selected, it should not start the drag | |
| 436 // selection. | |
| 437 if (this.lastSelection_.indexOf(itemIndex) != -1) | |
| 438 return false; | |
| 439 | |
| 440 // If the horizontal value is not hit to column, it should start the drag | |
| 441 // selection. | |
| 442 var hitColumn = this.columnModel.getHitColumn(pos.x); | |
| 443 if (!hitColumn) | |
| 444 return true; | |
| 445 | |
| 446 // Check if the point is on the column contents or not. | |
| 447 var item = this.list.getListItemByIndex(itemIndex); | |
| 448 switch (this.columnModel.columns_[hitColumn.index].id) { | |
| 449 case 'name': | |
| 450 var spanElement = item.querySelector('.filename-label span'); | |
| 451 var spanRect = spanElement.getBoundingClientRect(); | |
| 452 // The this.list.cachedBounds_ object is set by | |
| 453 // DragSelector.getScrolledPosition. | |
| 454 if (!this.list.cachedBounds) | |
| 455 return true; | |
| 456 var textRight = | |
| 457 spanRect.left - this.list.cachedBounds.left + spanRect.width; | |
| 458 return textRight <= hitColumn.hitPosition; | |
| 459 default: | |
| 460 return true; | |
| 461 } | |
| 462 }; | |
| 463 | |
| 464 /** | |
| 465 * Update check and disable states of the 'Select all' checkbox. | |
| 466 * @param {HTMLInputElement} checkbox The checkbox. If not passed, using | |
| 467 * the default one. | |
| 468 * @private | |
| 469 */ | |
| 470 FileTable.prototype.updateSelectAllCheckboxState_ = function(checkbox) { | |
| 471 // TODO(serya): introduce this.selectionModel.selectedCount. | |
| 472 checkbox.checked = this.dataModel.length > 0 && | |
| 473 this.dataModel.length == this.selectionModel.selectedIndexes.length; | |
| 474 checkbox.disabled = this.dataModel.length == 0; | |
| 475 }; | |
| 476 | |
| 477 /** | |
| 478 * Prepares the data model to be sorted by columns. | |
| 479 * @param {cr.ui.ArrayDataModel} dataModel Data model to prepare. | |
| 480 */ | |
| 481 FileTable.prototype.setupCompareFunctions = function(dataModel) { | |
| 482 dataModel.setCompareFunction('name', | |
| 483 this.compareName_.bind(this)); | |
| 484 dataModel.setCompareFunction('modificationTime', | |
| 485 this.compareMtime_.bind(this)); | |
| 486 dataModel.setCompareFunction('size', | |
| 487 this.compareSize_.bind(this)); | |
| 488 dataModel.setCompareFunction('type', | |
| 489 this.compareType_.bind(this)); | |
| 490 }; | |
| 491 | |
| 492 /** | |
| 493 * Render the Name column of the detail table. | |
| 494 * | |
| 495 * Invoked by cr.ui.Table when a file needs to be rendered. | |
| 496 * | |
| 497 * @param {Entry} entry The Entry object to render. | |
| 498 * @param {string} columnId The id of the column to be rendered. | |
| 499 * @param {cr.ui.Table} table The table doing the rendering. | |
| 500 * @return {HTMLDivElement} Created element. | |
| 501 * @private | |
| 502 */ | |
| 503 FileTable.prototype.renderName_ = function(entry, columnId, table) { | |
| 504 var label = this.ownerDocument.createElement('div'); | |
| 505 label.appendChild(this.renderIconType_(entry, columnId, table)); | |
| 506 label.entry = entry; | |
| 507 label.className = 'detail-name'; | |
| 508 label.appendChild(filelist.renderFileNameLabel(this.ownerDocument, entry)); | |
| 509 return label; | |
| 510 }; | |
| 511 | |
| 512 /** | |
| 513 * Render the Selection column of the detail table. | |
| 514 * | |
| 515 * Invoked by cr.ui.Table when a file needs to be rendered. | |
| 516 * | |
| 517 * @param {Entry} entry The Entry object to render. | |
| 518 * @param {string} columnId The id of the column to be rendered. | |
| 519 * @param {cr.ui.Table} table The table doing the rendering. | |
| 520 * @return {HTMLDivElement} Created element. | |
| 521 * @private | |
| 522 */ | |
| 523 FileTable.prototype.renderSelection_ = function(entry, columnId, table) { | |
| 524 var label = this.ownerDocument.createElement('div'); | |
| 525 label.className = 'selection-label'; | |
| 526 if (this.selectionModel.multiple) { | |
| 527 var checkBox = this.ownerDocument.createElement('input'); | |
| 528 filelist.decorateSelectionCheckbox(checkBox, entry, this.list); | |
| 529 label.appendChild(checkBox); | |
| 530 } | |
| 531 return label; | |
| 532 }; | |
| 533 | |
| 534 /** | |
| 535 * Render the Size column of the detail table. | |
| 536 * | |
| 537 * @param {Entry} entry The Entry object to render. | |
| 538 * @param {string} columnId The id of the column to be rendered. | |
| 539 * @param {cr.ui.Table} table The table doing the rendering. | |
| 540 * @return {HTMLDivElement} Created element. | |
| 541 * @private | |
| 542 */ | |
| 543 FileTable.prototype.renderSize_ = function(entry, columnId, table) { | |
| 544 var div = this.ownerDocument.createElement('div'); | |
| 545 div.className = 'size'; | |
| 546 this.updateSize_( | |
| 547 div, entry, this.metadataCache_.getCached(entry, 'filesystem')); | |
| 548 | |
| 549 return div; | |
| 550 }; | |
| 551 | |
| 552 /** | |
| 553 * Sets up or updates the size cell. | |
| 554 * | |
| 555 * @param {HTMLDivElement} div The table cell. | |
| 556 * @param {Entry} entry The corresponding entry. | |
| 557 * @param {Object} filesystemProps Metadata. | |
| 558 * @private | |
| 559 */ | |
| 560 FileTable.prototype.updateSize_ = function(div, entry, filesystemProps) { | |
| 561 if (!filesystemProps) { | |
| 562 div.textContent = '...'; | |
| 563 } else if (filesystemProps.size == -1) { | |
| 564 div.textContent = '--'; | |
| 565 } else if (filesystemProps.size == 0 && | |
| 566 FileType.isHosted(entry)) { | |
| 567 div.textContent = '--'; | |
| 568 } else { | |
| 569 div.textContent = util.bytesToString(filesystemProps.size); | |
| 570 } | |
| 571 }; | |
| 572 | |
| 573 /** | |
| 574 * Render the Type column of the detail table. | |
| 575 * | |
| 576 * @param {Entry} entry The Entry object to render. | |
| 577 * @param {string} columnId The id of the column to be rendered. | |
| 578 * @param {cr.ui.Table} table The table doing the rendering. | |
| 579 * @return {HTMLDivElement} Created element. | |
| 580 * @private | |
| 581 */ | |
| 582 FileTable.prototype.renderType_ = function(entry, columnId, table) { | |
| 583 var div = this.ownerDocument.createElement('div'); | |
| 584 div.className = 'type'; | |
| 585 div.textContent = FileType.getTypeString(entry); | |
| 586 return div; | |
| 587 }; | |
| 588 | |
| 589 /** | |
| 590 * Render the Date column of the detail table. | |
| 591 * | |
| 592 * @param {Entry} entry The Entry object to render. | |
| 593 * @param {string} columnId The id of the column to be rendered. | |
| 594 * @param {cr.ui.Table} table The table doing the rendering. | |
| 595 * @return {HTMLDivElement} Created element. | |
| 596 * @private | |
| 597 */ | |
| 598 FileTable.prototype.renderDate_ = function(entry, columnId, table) { | |
| 599 var div = this.ownerDocument.createElement('div'); | |
| 600 div.className = 'date'; | |
| 601 | |
| 602 this.updateDate_(div, | |
| 603 this.metadataCache_.getCached(entry, 'filesystem')); | |
| 604 return div; | |
| 605 }; | |
| 606 | |
| 607 /** | |
| 608 * Sets up or updates the date cell. | |
| 609 * | |
| 610 * @param {HTMLDivElement} div The table cell. | |
| 611 * @param {Object} filesystemProps Metadata. | |
| 612 * @private | |
| 613 */ | |
| 614 FileTable.prototype.updateDate_ = function(div, filesystemProps) { | |
| 615 if (!filesystemProps) { | |
| 616 div.textContent = '...'; | |
| 617 return; | |
| 618 } | |
| 619 | |
| 620 var modTime = filesystemProps.modificationTime; | |
| 621 var today = new Date(); | |
| 622 today.setHours(0); | |
| 623 today.setMinutes(0); | |
| 624 today.setSeconds(0); | |
| 625 today.setMilliseconds(0); | |
| 626 | |
| 627 /** | |
| 628 * Number of milliseconds in a day. | |
| 629 */ | |
| 630 var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000; | |
| 631 | |
| 632 if (modTime >= today && | |
| 633 modTime < today.getTime() + MILLISECONDS_IN_DAY) { | |
| 634 div.textContent = strf('TIME_TODAY', this.timeFormatter_.format(modTime)); | |
| 635 } else if (modTime >= today - MILLISECONDS_IN_DAY && modTime < today) { | |
| 636 div.textContent = strf('TIME_YESTERDAY', | |
| 637 this.timeFormatter_.format(modTime)); | |
| 638 } else { | |
| 639 div.textContent = | |
| 640 this.dateFormatter_.format(filesystemProps.modificationTime); | |
| 641 } | |
| 642 }; | |
| 643 | |
| 644 /** | |
| 645 * Updates the file metadata in the table item. | |
| 646 * | |
| 647 * @param {Element} item Table item. | |
| 648 * @param {Entry} entry File entry. | |
| 649 */ | |
| 650 FileTable.prototype.updateFileMetadata = function(item, entry) { | |
| 651 var props = this.metadataCache_.getCached(entry, 'filesystem'); | |
| 652 this.updateDate_(item.querySelector('.date'), props); | |
| 653 this.updateSize_(item.querySelector('.size'), entry, props); | |
| 654 }; | |
| 655 | |
| 656 /** | |
| 657 * Updates list items 'in place' on metadata change. | |
| 658 * @param {string} type Type of metadata change. | |
| 659 * @param {Object.<sting, Object>} propsMap Map from entry URLs to metadata | |
| 660 * properties. | |
| 661 */ | |
| 662 FileTable.prototype.updateListItemsMetadata = function(type, propsMap) { | |
| 663 var forEachCell = function(selector, callback) { | |
| 664 var cells = this.querySelectorAll(selector); | |
| 665 for (var i = 0; i < cells.length; i++) { | |
| 666 var cell = cells[i]; | |
| 667 var listItem = this.list_.getListItemAncestor(cell); | |
| 668 var entry = this.dataModel.item(listItem.listIndex); | |
| 669 if (entry) { | |
| 670 var props = propsMap[entry.toURL()]; | |
| 671 if (props) | |
| 672 callback.call(this, cell, entry, props, listItem); | |
| 673 } | |
| 674 } | |
| 675 }.bind(this); | |
| 676 if (type == 'filesystem') { | |
| 677 forEachCell('.table-row-cell > .date', function(item, entry, props) { | |
| 678 this.updateDate_(item, props); | |
| 679 }); | |
| 680 forEachCell('.table-row-cell > .size', function(item, entry, props) { | |
| 681 this.updateSize_(item, entry, props); | |
| 682 }); | |
| 683 } else if (type == 'drive') { | |
| 684 // The cell name does not matter as the entire list item is needed. | |
| 685 forEachCell('.table-row-cell > .date', | |
| 686 function(item, entry, props, listItem) { | |
| 687 filelist.updateListItemDriveProps(listItem, props); | |
| 688 }); | |
| 689 } | |
| 690 }; | |
| 691 | |
| 692 /** | |
| 693 * Compare by mtime first, then by name. | |
| 694 * @param {Entry} a First entry. | |
| 695 * @param {Entry} b Second entry. | |
| 696 * @return {number} Compare result. | |
| 697 * @private | |
| 698 */ | |
| 699 FileTable.prototype.compareName_ = function(a, b) { | |
| 700 return this.collator_.compare(a.name, b.name); | |
| 701 }; | |
| 702 | |
| 703 /** | |
| 704 * Compare by mtime first, then by name. | |
| 705 * @param {Entry} a First entry. | |
| 706 * @param {Entry} b Second entry. | |
| 707 * @return {number} Compare result. | |
| 708 * @private | |
| 709 */ | |
| 710 FileTable.prototype.compareMtime_ = function(a, b) { | |
| 711 var aCachedFilesystem = this.metadataCache_.getCached(a, 'filesystem'); | |
| 712 var aTime = aCachedFilesystem ? aCachedFilesystem.modificationTime : 0; | |
| 713 | |
| 714 var bCachedFilesystem = this.metadataCache_.getCached(b, 'filesystem'); | |
| 715 var bTime = bCachedFilesystem ? bCachedFilesystem.modificationTime : 0; | |
| 716 | |
| 717 if (aTime > bTime) | |
| 718 return 1; | |
| 719 | |
| 720 if (aTime < bTime) | |
| 721 return -1; | |
| 722 | |
| 723 return this.collator_.compare(a.name, b.name); | |
| 724 }; | |
| 725 | |
| 726 /** | |
| 727 * Compare by size first, then by name. | |
| 728 * @param {Entry} a First entry. | |
| 729 * @param {Entry} b Second entry. | |
| 730 * @return {number} Compare result. | |
| 731 * @private | |
| 732 */ | |
| 733 FileTable.prototype.compareSize_ = function(a, b) { | |
| 734 var aCachedFilesystem = this.metadataCache_.getCached(a, 'filesystem'); | |
| 735 var aSize = aCachedFilesystem ? aCachedFilesystem.size : 0; | |
| 736 | |
| 737 var bCachedFilesystem = this.metadataCache_.getCached(b, 'filesystem'); | |
| 738 var bSize = bCachedFilesystem ? bCachedFilesystem.size : 0; | |
| 739 | |
| 740 if (aSize != bSize) return aSize - bSize; | |
| 741 return this.collator_.compare(a.name, b.name); | |
| 742 }; | |
| 743 | |
| 744 /** | |
| 745 * Compare by type first, then by subtype and then by name. | |
| 746 * @param {Entry} a First entry. | |
| 747 * @param {Entry} b Second entry. | |
| 748 * @return {number} Compare result. | |
| 749 * @private | |
| 750 */ | |
| 751 FileTable.prototype.compareType_ = function(a, b) { | |
| 752 // Directories precede files. | |
| 753 if (a.isDirectory != b.isDirectory) | |
| 754 return Number(b.isDirectory) - Number(a.isDirectory); | |
| 755 | |
| 756 var aType = FileType.getTypeString(a); | |
| 757 var bType = FileType.getTypeString(b); | |
| 758 | |
| 759 var result = this.collator_.compare(aType, bType); | |
| 760 if (result != 0) | |
| 761 return result; | |
| 762 | |
| 763 return this.collator_.compare(a.name, b.name); | |
| 764 }; | |
| 765 | |
| 766 /** | |
| 767 * Renders table row. | |
| 768 * @param {function(Entry, cr.ui.Table)} baseRenderFunction Base renderer. | |
| 769 * @param {Entry} entry Corresponding entry. | |
| 770 * @return {HTMLLiElement} Created element. | |
| 771 * @private | |
| 772 */ | |
| 773 FileTable.prototype.renderTableRow_ = function(baseRenderFunction, entry) { | |
| 774 var item = baseRenderFunction(entry, this); | |
| 775 filelist.decorateListItem(item, entry, this.metadataCache_); | |
| 776 return item; | |
| 777 }; | |
| 778 | |
| 779 /** | |
| 780 * Renders the name column header. | |
| 781 * @param {string} name Localized column name. | |
| 782 * @return {HTMLLiElement} Created element. | |
| 783 * @private | |
| 784 */ | |
| 785 FileTable.prototype.renderNameColumnHeader_ = function(name) { | |
| 786 if (!this.selectionModel.multiple) | |
| 787 return this.ownerDocument.createTextNode(name); | |
| 788 | |
| 789 var input = this.ownerDocument.createElement('input'); | |
| 790 input.setAttribute('type', 'checkbox'); | |
| 791 input.setAttribute('tabindex', -1); | |
| 792 input.id = 'select-all-checkbox'; | |
| 793 input.className = 'common'; | |
| 794 | |
| 795 this.updateSelectAllCheckboxState_(input); | |
| 796 | |
| 797 input.addEventListener('click', function(event) { | |
| 798 if (input.checked) | |
| 799 this.selectionModel.selectAll(); | |
| 800 else | |
| 801 this.selectionModel.unselectAll(); | |
| 802 event.stopPropagation(); | |
| 803 }.bind(this)); | |
| 804 | |
| 805 var fragment = this.ownerDocument.createDocumentFragment(); | |
| 806 fragment.appendChild(input); | |
| 807 fragment.appendChild(this.ownerDocument.createTextNode(name)); | |
| 808 return fragment; | |
| 809 }; | |
| 810 | |
| 811 /** | |
| 812 * Renders the selection column header. | |
| 813 * @param {string} name Localized column name. | |
| 814 * @return {HTMLLiElement} Created element. | |
| 815 * @private | |
| 816 */ | |
| 817 FileTable.prototype.renderSelectionColumnHeader_ = function(name) { | |
| 818 if (!this.selectionModel.multiple) | |
| 819 return this.ownerDocument.createTextNode(''); | |
| 820 | |
| 821 var input = this.ownerDocument.createElement('input'); | |
| 822 input.setAttribute('type', 'checkbox'); | |
| 823 input.setAttribute('tabindex', -1); | |
| 824 input.id = 'select-all-checkbox'; | |
| 825 input.className = 'common'; | |
| 826 | |
| 827 this.updateSelectAllCheckboxState_(input); | |
| 828 | |
| 829 input.addEventListener('click', function(event) { | |
| 830 if (input.checked) | |
| 831 this.selectionModel.selectAll(); | |
| 832 else | |
| 833 this.selectionModel.unselectAll(); | |
| 834 event.stopPropagation(); | |
| 835 }.bind(this)); | |
| 836 | |
| 837 var fragment = this.ownerDocument.createDocumentFragment(); | |
| 838 fragment.appendChild(input); | |
| 839 return fragment; | |
| 840 }; | |
| 841 | |
| 842 /** | |
| 843 * Render the type column of the detail table. | |
| 844 * | |
| 845 * Invoked by cr.ui.Table when a file needs to be rendered. | |
| 846 * | |
| 847 * @param {Entry} entry The Entry object to render. | |
| 848 * @param {string} columnId The id of the column to be rendered. | |
| 849 * @param {cr.ui.Table} table The table doing the rendering. | |
| 850 * @return {HTMLDivElement} Created element. | |
| 851 * @private | |
| 852 */ | |
| 853 FileTable.prototype.renderIconType_ = function(entry, columnId, table) { | |
| 854 var icon = this.ownerDocument.createElement('div'); | |
| 855 icon.className = 'detail-icon'; | |
| 856 icon.setAttribute('file-type-icon', FileType.getIcon(entry)); | |
| 857 return icon; | |
| 858 }; | |
| 859 | |
| 860 /** | |
| 861 * Sets the margin height for the transparent preview panel at the bottom. | |
| 862 * @param {number} margin Margin to be set in px. | |
| 863 */ | |
| 864 FileTable.prototype.setBottomMarginForPanel = function(margin) { | |
| 865 this.list_.style.paddingBottom = margin + 'px'; | |
| 866 this.scrollBar_.setBottomMarginForPanel(margin); | |
| 867 }; | |
| 868 | |
| 869 /** | |
| 870 * Redraws the UI. Skips multiple consecutive calls. | |
| 871 */ | |
| 872 FileTable.prototype.relayout = function() { | |
| 873 this.relayoutAggregation_.run(); | |
| 874 }; | |
| 875 | |
| 876 /** | |
| 877 * Redraws the UI immediately. | |
| 878 * @private | |
| 879 */ | |
| 880 FileTable.prototype.relayoutImmediately_ = function() { | |
| 881 if (this.clientWidth > 0) | |
| 882 this.normalizeColumns(); | |
| 883 this.redraw(); | |
| 884 cr.dispatchSimpleEvent(this.list, 'relayout'); | |
| 885 }; | |
| 886 | |
| 887 /** | |
| 888 * Decorates (and wire up) a checkbox to be used in either a detail or a | |
| 889 * thumbnail list item. | |
| 890 * @param {HTMLInputElement} input Element to decorate. | |
| 891 */ | |
| 892 filelist.decorateCheckbox = function(input) { | |
| 893 var stopEventPropagation = function(event) { | |
| 894 if (!event.shiftKey) | |
| 895 event.stopPropagation(); | |
| 896 }; | |
| 897 input.setAttribute('type', 'checkbox'); | |
| 898 input.setAttribute('tabindex', -1); | |
| 899 input.classList.add('common'); | |
| 900 input.addEventListener('mousedown', stopEventPropagation); | |
| 901 input.addEventListener('mouseup', stopEventPropagation); | |
| 902 | |
| 903 input.addEventListener( | |
| 904 'click', | |
| 905 /** | |
| 906 * @this {HTMLInputElement} | |
| 907 */ | |
| 908 function(event) { | |
| 909 // Revert default action and swallow the event | |
| 910 // if this is a multiple click or Shift is pressed. | |
| 911 if (event.detail > 1 || event.shiftKey) { | |
| 912 this.checked = !this.checked; | |
| 913 stopEventPropagation(event); | |
| 914 } | |
| 915 }); | |
| 916 }; | |
| 917 | |
| 918 /** | |
| 919 * Decorates selection checkbox. | |
| 920 * @param {HTMLInputElement} input Element to decorate. | |
| 921 * @param {Entry} entry Corresponding entry. | |
| 922 * @param {cr.ui.List} list Owner list. | |
| 923 */ | |
| 924 filelist.decorateSelectionCheckbox = function(input, entry, list) { | |
| 925 filelist.decorateCheckbox(input); | |
| 926 input.classList.add('file-checkbox'); | |
| 927 input.addEventListener('click', function(e) { | |
| 928 var sm = list.selectionModel; | |
| 929 var listIndex = list.getListItemAncestor(this).listIndex; | |
| 930 sm.setIndexSelected(listIndex, this.checked); | |
| 931 sm.leadIndex = listIndex; | |
| 932 if (sm.anchorIndex == -1) | |
| 933 sm.anchorIndex = listIndex; | |
| 934 | |
| 935 }); | |
| 936 // Since we do not want to open the item when tap on checkbox, we need to | |
| 937 // stop propagation of TAP event dispatched by checkbox ideally. But all | |
| 938 // touch events from touch_handler are dispatched to the list control. So we | |
| 939 // have to stop propagation of native touchstart event to prevent list | |
| 940 // control from generating TAP event here. The synthetic click event will | |
| 941 // select the touched checkbox/item. | |
| 942 input.addEventListener('touchstart', | |
| 943 function(e) { e.stopPropagation() }); | |
| 944 | |
| 945 var index = list.dataModel.indexOf(entry); | |
| 946 // Our DOM nodes get discarded as soon as we're scrolled out of view, | |
| 947 // so we have to make sure the check state is correct when we're brought | |
| 948 // back to life. | |
| 949 input.checked = list.selectionModel.getIndexSelected(index); | |
| 950 }; | |
| 951 | |
| 952 /** | |
| 953 * Common item decoration for table's and grid's items. | |
| 954 * @param {ListItem} li List item. | |
| 955 * @param {Entry} entry The entry. | |
| 956 * @param {MetadataCache} metadataCache Cache to retrieve metadada. | |
| 957 */ | |
| 958 filelist.decorateListItem = function(li, entry, metadataCache) { | |
| 959 li.classList.add(entry.isDirectory ? 'directory' : 'file'); | |
| 960 if (FileType.isOnDrive(entry)) { | |
| 961 // The metadata may not yet be ready. In that case, the list item will be | |
| 962 // updated when the metadata is ready via updateListItemsMetadata. | |
| 963 var driveProps = metadataCache.getCached(entry, 'drive'); | |
| 964 if (driveProps) | |
| 965 filelist.updateListItemDriveProps(li, driveProps); | |
| 966 } | |
| 967 | |
| 968 // Overriding the default role 'list' to 'listbox' for better | |
| 969 // accessibility on ChromeOS. | |
| 970 li.setAttribute('role', 'option'); | |
| 971 | |
| 972 Object.defineProperty(li, 'selected', { | |
| 973 /** | |
| 974 * @this {ListItem} | |
| 975 * @return {boolean} True if the list item is selected. | |
| 976 */ | |
| 977 get: function() { | |
| 978 return this.hasAttribute('selected'); | |
| 979 }, | |
| 980 | |
| 981 /** | |
| 982 * @this {ListItem} | |
| 983 */ | |
| 984 set: function(v) { | |
| 985 if (v) | |
| 986 this.setAttribute('selected'); | |
| 987 else | |
| 988 this.removeAttribute('selected'); | |
| 989 var checkBox = this.querySelector('input.file-checkbox'); | |
| 990 if (checkBox) | |
| 991 checkBox.checked = !!v; | |
| 992 } | |
| 993 }); | |
| 994 }; | |
| 995 | |
| 996 /** | |
| 997 * Render filename label for grid and list view. | |
| 998 * @param {HTMLDocument} doc Owner document. | |
| 999 * @param {Entry} entry The Entry object to render. | |
| 1000 * @return {HTMLDivElement} The label. | |
| 1001 */ | |
| 1002 filelist.renderFileNameLabel = function(doc, entry) { | |
| 1003 // Filename need to be in a '.filename-label' container for correct | |
| 1004 // work of inplace renaming. | |
| 1005 var box = doc.createElement('div'); | |
| 1006 box.className = 'filename-label'; | |
| 1007 var fileName = doc.createElement('span'); | |
| 1008 fileName.textContent = entry.name; | |
| 1009 box.appendChild(fileName); | |
| 1010 | |
| 1011 return box; | |
| 1012 }; | |
| 1013 | |
| 1014 /** | |
| 1015 * Updates grid item or table row for the driveProps. | |
| 1016 * @param {cr.ui.ListItem} li List item. | |
| 1017 * @param {Object} driveProps Metadata. | |
| 1018 */ | |
| 1019 filelist.updateListItemDriveProps = function(li, driveProps) { | |
| 1020 if (li.classList.contains('file')) { | |
| 1021 if (driveProps.availableOffline) | |
| 1022 li.classList.remove('dim-offline'); | |
| 1023 else | |
| 1024 li.classList.add('dim-offline'); | |
| 1025 // TODO(mtomasz): Consider adding some vidual indication for files which | |
| 1026 // are not cached on LTE. Currently we show them as normal files. | |
| 1027 // crbug.com/246611. | |
| 1028 } | |
| 1029 | |
| 1030 if (driveProps.customIconUrl) { | |
| 1031 var iconDiv = li.querySelector('.detail-icon'); | |
| 1032 if (!iconDiv) | |
| 1033 return; | |
| 1034 iconDiv.style.backgroundImage = 'url(' + driveProps.customIconUrl + ')'; | |
| 1035 } | |
| 1036 }; | |
| OLD | NEW |