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 positon. 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 }; 368 369 /** 370 * Sets date and time format. 371 * @param {boolean} use12hourClock True if 12 hours clock, False if 24 hours. 372 */ 373 FileTable.prototype.setDateTimeFormat = function(use12hourClock) { 374 this.timeFormatter_ = Intl.DateTimeFormat( 375 [] /* default locale */, 376 {hour: 'numeric', minute: 'numeric', 377 hour12: use12hourClock}); 378 this.dateFormatter_ = Intl.DateTimeFormat( 379 [] /* default locale */, 380 {year: 'numeric', month: 'short', day: 'numeric', 381 hour: 'numeric', minute: 'numeric', 382 hour12: use12hourClock}); 383 }; 384 385 /** 386 * Obtains if the drag selection should be start or not by referring the mouse 387 * event. 388 * @param {MouseEvent} event Drag start event. 389 * @return {boolean} True if the mouse is hit to the background of the list. 390 */ 391 FileTable.prototype.shouldStartDragSelection = function(event) { 392 // If the shift key is pressed, it should starts drag selection. 393 if (event.shiftKey) 394 return true; 395 396 // If the position values are negative, it points the out of list. 397 // It should start the drag selection. 398 var pos = DragSelector.getScrolledPosition(this.list, event); 399 if (!pos) 400 return false; 401 if (pos.x < 0 || pos.y < 0) 402 return true; 403 404 // If the item index is out of range, it should start the drag selection. 405 var itemHeight = this.list.measureItem().height; 406 // Faster alternative to Math.floor for non-negative numbers. 407 var itemIndex = ~~(pos.y / itemHeight); 408 if (itemIndex >= this.list.dataModel.length) 409 return true; 410 411 // If the pointed item is already selected, it should not start the drag 412 // selection. 413 if (this.lastSelection_.indexOf(itemIndex) != -1) 414 return false; 415 416 // If the horizontal value is not hit to column, it shoud start the drag 417 // selection. 418 var hitColumn = this.columnModel.getHitColumn(pos.x); 419 if (!hitColumn) 420 return true; 421 422 // Check if the point is on the column contents or not. 423 var item = this.list.getListItemByIndex(itemIndex); 424 switch (this.columnModel.columns_[hitColumn.index].id) { 425 case 'name': 426 var spanElement = item.querySelector('.filename-label span'); 427 var spanRect = spanElement.getBoundingClientRect(); 428 // The this.list.cachedBounds_ object is set by 429 // DragSelector.getScrolledPosition. 430 if (!this.list.cachedBounds) 431 return true; 432 var textRight = 433 spanRect.left - this.list.cachedBounds.left + spanRect.width; 434 return textRight <= hitColumn.hitPosition; 435 default: 436 return true; 437 } 438 }; 439 440 /** 441 * Update check and disable states of the 'Select all' checkbox. 442 * @param {HTMLInputElement} checkbox The checkbox. If not passed, using 443 * the default one. 444 * @private 445 */ 446 FileTable.prototype.updateSelectAllCheckboxState_ = function(checkbox) { 447 // TODO(serya): introduce this.selectionModel.selectedCount. 448 checkbox.checked = this.dataModel.length > 0 && 449 this.dataModel.length == this.selectionModel.selectedIndexes.length; 450 checkbox.disabled = this.dataModel.length == 0; 451 }; 452 453 /** 454 * Prepares the data model to be sorted by columns. 455 * @param {cr.ui.ArrayDataModel} dataModel Data model to prepare. 456 */ 457 FileTable.prototype.setupCompareFunctions = function(dataModel) { 458 dataModel.setCompareFunction('name', 459 this.compareName_.bind(this)); 460 dataModel.setCompareFunction('modificationTime', 461 this.compareMtime_.bind(this)); 462 dataModel.setCompareFunction('size', 463 this.compareSize_.bind(this)); 464 dataModel.setCompareFunction('type', 465 this.compareType_.bind(this)); 466 }; 467 468 /** 469 * Render the Name column of the detail table. 470 * 471 * Invoked by cr.ui.Table when a file needs to be rendered. 472 * 473 * @param {Entry} entry The Entry object to render. 474 * @param {string} columnId The id of the column to be rendered. 475 * @param {cr.ui.Table} table The table doing the rendering. 476 * @return {HTMLDivElement} Created element. 477 * @private 478 */ 479 FileTable.prototype.renderName_ = function(entry, columnId, table) { 480 var label = this.ownerDocument.createElement('div'); 481 label.appendChild(this.renderIconType_(entry, columnId, table)); 482 label.entry = entry; 483 label.className = 'detail-name'; 484 label.appendChild(filelist.renderFileNameLabel(this.ownerDocument, entry)); 485 return label; 486 }; 487 488 /** 489 * Render the Selection column of the detail table. 490 * 491 * Invoked by cr.ui.Table when a file needs to be rendered. 492 * 493 * @param {Entry} entry The Entry object to render. 494 * @param {string} columnId The id of the column to be rendered. 495 * @param {cr.ui.Table} table The table doing the rendering. 496 * @return {HTMLDivElement} Created element. 497 * @private 498 */ 499 FileTable.prototype.renderSelection_ = function(entry, columnId, table) { 500 var label = this.ownerDocument.createElement('div'); 501 label.className = 'selection-label'; 502 if (this.selectionModel.multiple) { 503 var checkBox = this.ownerDocument.createElement('input'); 504 filelist.decorateSelectionCheckbox(checkBox, entry, this.list); 505 label.appendChild(checkBox); 506 } 507 return label; 508 }; 509 510 /** 511 * Render the Size column of the detail table. 512 * 513 * @param {Entry} entry The Entry object to render. 514 * @param {string} columnId The id of the column to be rendered. 515 * @param {cr.ui.Table} table The table doing the rendering. 516 * @return {HTMLDivElement} Created element. 517 * @private 518 */ 519 FileTable.prototype.renderSize_ = function(entry, columnId, table) { 520 var div = this.ownerDocument.createElement('div'); 521 div.className = 'size'; 522 this.updateSize_( 523 div, entry, this.metadataCache_.getCached(entry, 'filesystem')); 524 525 return div; 526 }; 527 528 /** 529 * Sets up or updates the size cell. 530 * 531 * @param {HTMLDivElement} div The table cell. 532 * @param {Entry} entry The corresponding entry. 533 * @param {Object} filesystemProps Metadata. 534 * @private 535 */ 536 FileTable.prototype.updateSize_ = function(div, entry, filesystemProps) { 537 if (!filesystemProps) { 538 div.textContent = '...'; 539 } else if (filesystemProps.size == -1) { 540 div.textContent = '--'; 541 } else if (filesystemProps.size == 0 && 542 FileType.isHosted(entry)) { 543 div.textContent = '--'; 544 } else { 545 div.textContent = util.bytesToString(filesystemProps.size); 546 } 547 }; 548 549 /** 550 * Render the Type column of the detail table. 551 * 552 * @param {Entry} entry The Entry object to render. 553 * @param {string} columnId The id of the column to be rendered. 554 * @param {cr.ui.Table} table The table doing the rendering. 555 * @return {HTMLDivElement} Created element. 556 * @private 557 */ 558 FileTable.prototype.renderType_ = function(entry, columnId, table) { 559 var div = this.ownerDocument.createElement('div'); 560 div.className = 'type'; 561 div.textContent = FileType.getTypeString(entry); 562 return div; 563 }; 564 565 /** 566 * Render the Date column of the detail table. 567 * 568 * @param {Entry} entry The Entry object to render. 569 * @param {string} columnId The id of the column to be rendered. 570 * @param {cr.ui.Table} table The table doing the rendering. 571 * @return {HTMLDivElement} Created element. 572 * @private 573 */ 574 FileTable.prototype.renderDate_ = function(entry, columnId, table) { 575 var div = this.ownerDocument.createElement('div'); 576 div.className = 'date'; 577 578 this.updateDate_(div, 579 this.metadataCache_.getCached(entry, 'filesystem')); 580 return div; 581 }; 582 583 /** 584 * Sets up or updates the date cell. 585 * 586 * @param {HTMLDivElement} div The table cell. 587 * @param {Object} filesystemProps Metadata. 588 * @private 589 */ 590 FileTable.prototype.updateDate_ = function(div, filesystemProps) { 591 if (!filesystemProps) { 592 div.textContent = '...'; 593 return; 594 } 595 596 var modTime = filesystemProps.modificationTime; 597 var today = new Date(); 598 today.setHours(0); 599 today.setMinutes(0); 600 today.setSeconds(0); 601 today.setMilliseconds(0); 602 603 /** 604 * Number of milliseconds in a day. 605 */ 606 var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000; 607 608 if (modTime >= today && 609 modTime < today.getTime() + MILLISECONDS_IN_DAY) { 610 div.textContent = strf('TIME_TODAY', this.timeFormatter_.format(modTime)); 611 } else if (modTime >= today - MILLISECONDS_IN_DAY && modTime < today) { 612 div.textContent = strf('TIME_YESTERDAY', 613 this.timeFormatter_.format(modTime)); 614 } else { 615 div.textContent = 616 this.dateFormatter_.format(filesystemProps.modificationTime); 617 } 618 }; 619 620 /** 621 * Updates the file metadata in the table item. 622 * 623 * @param {Element} item Table item. 624 * @param {Entry} entry File entry. 625 */ 626 FileTable.prototype.updateFileMetadata = function(item, entry) { 627 var props = this.metadataCache_.getCached(entry, 'filesystem'); 628 this.updateDate_(item.querySelector('.date'), props); 629 this.updateSize_(item.querySelector('.size'), entry, props); 630 }; 631 632 /** 633 * Updates list items 'in place' on metadata change. 634 * @param {string} type Type of metadata change. 635 * @param {Object.<sting, Object>} propsMap Map from entry URLs to metadata 636 * properties. 637 */ 638 FileTable.prototype.updateListItemsMetadata = function(type, propsMap) { 639 var forEachCell = function(selector, callback) { 640 var cells = this.querySelectorAll(selector); 641 for (var i = 0; i < cells.length; i++) { 642 var cell = cells[i]; 643 var listItem = this.list_.getListItemAncestor(cell); 644 var entry = this.dataModel.item(listItem.listIndex); 645 if (entry) { 646 var props = propsMap[entry.toURL()]; 647 if (props) 648 callback.call(this, cell, entry, props, listItem); 649 } 650 } 651 }.bind(this); 652 if (type == 'filesystem') { 653 forEachCell('.table-row-cell > .date', function(item, entry, props) { 654 this.updateDate_(item, props); 655 }); 656 forEachCell('.table-row-cell > .size', function(item, entry, props) { 657 this.updateSize_(item, entry, props); 658 }); 659 } else if (type == 'drive') { 660 forEachCell('.table-row-cell > .offline', 661 function(item, entry, props, listItem) { 662 this.updateOffline_(item, props); 663 filelist.updateListItemDriveProps(listItem, props); 664 }); 665 } 666 }; 667 668 /** 669 * Compare by mtime first, then by name. 670 * @param {Entry} a First entry. 671 * @param {Entry} b Second entry. 672 * @return {number} Compare result. 673 * @private 674 */ 675 FileTable.prototype.compareName_ = function(a, b) { 676 return this.collator_.compare(a.name, b.name); 677 }; 678 679 /** 680 * Compare by mtime first, then by name. 681 * @param {Entry} a First entry. 682 * @param {Entry} b Second entry. 683 * @return {number} Compare result. 684 * @private 685 */ 686 FileTable.prototype.compareMtime_ = function(a, b) { 687 var aCachedFilesystem = this.metadataCache_.getCached(a, 'filesystem'); 688 var aTime = aCachedFilesystem ? aCachedFilesystem.modificationTime : 0; 689 690 var bCachedFilesystem = this.metadataCache_.getCached(b, 'filesystem'); 691 var bTime = bCachedFilesystem ? bCachedFilesystem.modificationTime : 0; 692 693 if (aTime > bTime) 694 return 1; 695 696 if (aTime < bTime) 697 return -1; 698 699 return this.collator_.compare(a.name, b.name); 700 }; 701 702 /** 703 * Compare by size first, then by name. 704 * @param {Entry} a First entry. 705 * @param {Entry} b Second entry. 706 * @return {number} Compare result. 707 * @private 708 */ 709 FileTable.prototype.compareSize_ = function(a, b) { 710 var aCachedFilesystem = this.metadataCache_.getCached(a, 'filesystem'); 711 var aSize = aCachedFilesystem ? aCachedFilesystem.size : 0; 712 713 var bCachedFilesystem = this.metadataCache_.getCached(b, 'filesystem'); 714 var bSize = bCachedFilesystem ? bCachedFilesystem.size : 0; 715 716 if (aSize != bSize) return aSize - bSize; 717 return this.collator_.compare(a.name, b.name); 718 }; 719 720 /** 721 * Compare by type first, then by subtype and then by name. 722 * @param {Entry} a First entry. 723 * @param {Entry} b Second entry. 724 * @return {number} Compare result. 725 * @private 726 */ 727 FileTable.prototype.compareType_ = function(a, b) { 728 // Directories precede files. 729 if (a.isDirectory != b.isDirectory) 730 return Number(b.isDirectory) - Number(a.isDirectory); 731 732 var aType = FileType.getTypeString(a); 733 var bType = FileType.getTypeString(b); 734 735 var result = this.collator_.compare(aType, bType); 736 if (result != 0) 737 return result; 738 739 return this.collator_.compare(a.name, b.name); 740 }; 741 742 /** 743 * Renders table row. 744 * @param {function(Entry, cr.ui.Table)} baseRenderFunction Base renderer. 745 * @param {Entry} entry Corresponding entry. 746 * @return {HTMLLiElement} Created element. 747 * @private 748 */ 749 FileTable.prototype.renderTableRow_ = function(baseRenderFunction, entry) { 750 var item = baseRenderFunction(entry, this); 751 filelist.decorateListItem(item, entry, this.metadataCache_); 752 return item; 753 }; 754 755 /** 756 * Renders the name column header. 757 * @param {string} name Localized column name. 758 * @return {HTMLLiElement} Created element. 759 * @private 760 */ 761 FileTable.prototype.renderNameColumnHeader_ = function(name) { 762 if (!this.selectionModel.multiple) 763 return this.ownerDocument.createTextNode(name); 764 765 var input = this.ownerDocument.createElement('input'); 766 input.setAttribute('type', 'checkbox'); 767 input.setAttribute('tabindex', -1); 768 input.id = 'select-all-checkbox'; 769 input.className = 'common'; 770 771 this.updateSelectAllCheckboxState_(input); 772 773 input.addEventListener('click', function(event) { 774 if (input.checked) 775 this.selectionModel.selectAll(); 776 else 777 this.selectionModel.unselectAll(); 778 event.stopPropagation(); 779 }.bind(this)); 780 781 var fragment = this.ownerDocument.createDocumentFragment(); 782 fragment.appendChild(input); 783 fragment.appendChild(this.ownerDocument.createTextNode(name)); 784 return fragment; 785 }; 786 787 /** 788 * Renders the selection column header. 789 * @param {string} name Localized column name. 790 * @return {HTMLLiElement} Created element. 791 * @private 792 */ 793 FileTable.prototype.renderSelectionColumnHeader_ = function(name) { 794 if (!this.selectionModel.multiple) 795 return this.ownerDocument.createTextNode(''); 796 797 var input = this.ownerDocument.createElement('input'); 798 input.setAttribute('type', 'checkbox'); 799 input.setAttribute('tabindex', -1); 800 input.id = 'select-all-checkbox'; 801 input.className = 'common'; 802 803 this.updateSelectAllCheckboxState_(input); 804 805 input.addEventListener('click', function(event) { 806 if (input.checked) 807 this.selectionModel.selectAll(); 808 else 809 this.selectionModel.unselectAll(); 810 event.stopPropagation(); 811 }.bind(this)); 812 813 var fragment = this.ownerDocument.createDocumentFragment(); 814 fragment.appendChild(input); 815 return fragment; 816 }; 817 818 /** 819 * Render the type column of the detail table. 820 * 821 * Invoked by cr.ui.Table when a file needs to be rendered. 822 * 823 * @param {Entry} entry The Entry object to render. 824 * @param {string} columnId The id of the column to be rendered. 825 * @param {cr.ui.Table} table The table doing the rendering. 826 * @return {HTMLDivElement} Created element. 827 * @private 828 */ 829 FileTable.prototype.renderIconType_ = function(entry, columnId, table) { 830 var icon = this.ownerDocument.createElement('div'); 831 icon.className = 'detail-icon'; 832 icon.setAttribute('file-type-icon', FileType.getIcon(entry)); 833 return icon; 834 }; 835 836 /** 837 * Sets the margin height for the transparent preview panel at the bottom. 838 * @param {number} margin Margin to be set in px. 839 */ 840 FileTable.prototype.setBottomMarginForPanel = function(margin) { 841 this.list_.style.paddingBottom = margin + 'px'; 842 this.scrollBar_.setBottomMarginForPanel(margin); 843 }; 844 845 /** 846 * Redraws the UI. Skips multiple consecutive calls. 847 */ 848 FileTable.prototype.relayout = function() { 849 this.relayoutAggregation_.run(); 850 }; 851 852 /** 853 * Redraws the UI immediately. 854 * @private 855 */ 856 FileTable.prototype.relayoutImmediately_ = function() { 857 if (this.clientWidth > 0) 858 this.normalizeColumns(); 859 this.redraw(); 860 cr.dispatchSimpleEvent(this.list, 'relayout'); 861 }; 862 863 /** 864 * Decorates (and wire up) a checkbox to be used in either a detail or a 865 * thumbnail list item. 866 * @param {HTMLInputElement} input Element to decorate. 867 */ 868 filelist.decorateCheckbox = function(input) { 869 var stopEventPropagation = function(event) { 870 if (!event.shiftKey) 871 event.stopPropagation(); 872 }; 873 input.setAttribute('type', 'checkbox'); 874 input.setAttribute('tabindex', -1); 875 input.classList.add('common'); 876 input.addEventListener('mousedown', stopEventPropagation); 877 input.addEventListener('mouseup', stopEventPropagation); 878 879 input.addEventListener( 880 'click', 881 /** 882 * @this {HTMLInputElement} 883 */ 884 function(event) { 885 // Revert default action and swallow the event 886 // if this is a multiple click or Shift is pressed. 887 if (event.detail > 1 || event.shiftKey) { 888 this.checked = !this.checked; 889 stopEventPropagation(event); 890 } 891 }); 892 }; 893 894 /** 895 * Decorates selection checkbox. 896 * @param {HTMLInputElement} input Element to decorate. 897 * @param {Entry} entry Corresponding entry. 898 * @param {cr.ui.List} list Owner list. 899 */ 900 filelist.decorateSelectionCheckbox = function(input, entry, list) { 901 filelist.decorateCheckbox(input); 902 input.classList.add('file-checkbox'); 903 input.addEventListener('click', function(e) { 904 var sm = list.selectionModel; 905 var listIndex = list.getListItemAncestor(this).listIndex; 906 sm.setIndexSelected(listIndex, this.checked); 907 sm.leadIndex = listIndex; 908 if (sm.anchorIndex == -1) 909 sm.anchorIndex = listIndex; 910 911 }); 912 // Since we do not want to open the item when tap on checkbox, we need to 913 // stop propagation of TAP event dispatched by checkbox ideally. But all 914 // touch events from touch_handler are dispatched to the list control. So we 915 // have to stop propagation of native touchstart event to prevent list 916 // control from generating TAP event here. The synthetic click event will 917 // select the touched checkbox/item. 918 input.addEventListener('touchstart', 919 function(e) { e.stopPropagation() }); 920 921 var index = list.dataModel.indexOf(entry); 922 // Our DOM nodes get discarded as soon as we're scrolled out of view, 923 // so we have to make sure the check state is correct when we're brought 924 // back to life. 925 input.checked = list.selectionModel.getIndexSelected(index); 926 }; 927 928 /** 929 * Common item decoration for table's and grid's items. 930 * @param {ListItem} li List item. 931 * @param {Entry} entry The entry. 932 * @param {MetadataCache} metadataCache Cache to retrieve metadada. 933 */ 934 filelist.decorateListItem = function(li, entry, metadataCache) { 935 li.classList.add(entry.isDirectory ? 'directory' : 'file'); 936 if (FileType.isOnDrive(entry)) { 937 var driveProps = metadataCache.getCached(entry, 'drive'); 938 if (driveProps) 939 filelist.updateListItemDriveProps(li, driveProps); 940 } 941 942 // Overriding the default role 'list' to 'listbox' for better 943 // accessibility on ChromeOS. 944 li.setAttribute('role', 'option'); 945 946 Object.defineProperty(li, 'selected', { 947 /** 948 * @this {ListItem} 949 * @return {boolean} True if the list item is selected. 950 */ 951 get: function() { 952 return this.hasAttribute('selected'); 953 }, 954 955 /** 956 * @this {ListItem} 957 */ 958 set: function(v) { 959 if (v) 960 this.setAttribute('selected'); 961 else 962 this.removeAttribute('selected'); 963 var checkBox = this.querySelector('input.file-checkbox'); 964 if (checkBox) 965 checkBox.checked = !!v; 966 } 967 }); 968 }; 969 970 /** 971 * Render filename label for grid and list view. 972 * @param {HTMLDocument} doc Owner document. 973 * @param {Entry} entry The Entry object to render. 974 * @return {HTMLDivElement} The label. 975 */ 976 filelist.renderFileNameLabel = function(doc, entry) { 977 // Filename need to be in a '.filename-label' container for correct 978 // work of inplace renaming. 979 var box = doc.createElement('div'); 980 box.className = 'filename-label'; 981 var fileName = doc.createElement('span'); 982 fileName.textContent = entry.name; 983 box.appendChild(fileName); 984 985 return box; 986 }; 987 988 /** 989 * Updates grid item or table row for the driveProps. 990 * @param {cr.ui.ListItem} li List item. 991 * @param {Object} driveProps Metadata. 992 */ 993 filelist.updateListItemDriveProps = function(li, driveProps) { 994 if (li.classList.contains('file')) { 995 if (driveProps.availableOffline) 996 li.classList.remove('dim-offline'); 997 else 998 li.classList.add('dim-offline'); 999 // TODO(mtomasz): Consider adding some vidual indication for files which 1000 // are not cached on LTE. Currently we show them as normal files. 1001 // crbug.com/246611. 1002 } 1003 1004 if (driveProps.driveApps.length > 0) { 1005 var iconDiv = li.querySelector('.detail-icon'); 1006 if (!iconDiv) 1007 return; 1008 // Find the default app for this file. If there is none, then 1009 // leave it as the base icon for the file type. 1010 var url; 1011 for (var i = 0; i < driveProps.driveApps.length; ++i) { 1012 var app = driveProps.driveApps[i]; 1013 if (app && app.docIcon && app.isPrimary) { 1014 url = app.docIcon; 1015 break; 1016 } 1017 } 1018 if (url) { 1019 iconDiv.style.backgroundImage = 'url(' + url + ')'; 1020 } else { 1021 iconDiv.style.backgroundImage = null; 1022 } 1023 } 1024 }; 1025