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 */ 10var 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 */ 19function 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 */ 29FileTableColumnModel.RESIZABLE_LENGTH_ = 4; 30 31/** 32 * Inherits from cr.ui.TableColumnModel. 33 */ 34FileTableColumnModel.prototype.__proto__ = 35 cr.ui.table.TableColumnModel.prototype; 36 37/** 38 * Minimum width of column. 39 * @const 40 * @type {number} 41 * @private 42 */ 43FileTableColumnModel.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 */ 53FileTableColumnModel.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 */ 78FileTableColumnModel.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 */ 103FileTableColumnModel.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 */ 113FileTableColumnModel.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 */ 122FileTableColumnModel.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 */ 163var FileTableSplitter = cr.ui.define('div'); 164 165/** 166 * Inherits from cr.ui.TableSplitter. 167 */ 168FileTableSplitter.prototype.__proto__ = cr.ui.TableSplitter.prototype; 169 170/** 171 * Handles the drag start event. 172 */ 173FileTableSplitter.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 */ 182FileTableSplitter.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 */ 191FileTableSplitter.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 */ 200function FileTable() { 201 throw new Error('Designed to decorate elements'); 202} 203 204/** 205 * Inherits from cr.ui.Table. 206 */ 207FileTable.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 */ 216FileTable.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 */ 396FileTable.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 */ 415FileTable.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 */ 470FileTable.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 */ 481FileTable.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 */ 503FileTable.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 */ 523FileTable.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 */ 543FileTable.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 */ 560FileTable.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 */ 582FileTable.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 */ 598FileTable.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 */ 614FileTable.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 */ 650FileTable.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 */ 662FileTable.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 */ 699FileTable.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 */ 710FileTable.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 */ 733FileTable.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 */ 751FileTable.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 */ 773FileTable.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 */ 785FileTable.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 */ 817FileTable.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 */ 853FileTable.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 */ 864FileTable.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 */ 872FileTable.prototype.relayout = function() { 873 this.relayoutAggregation_.run(); 874}; 875 876/** 877 * Redraws the UI immediately. 878 * @private 879 */ 880FileTable.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 */ 892filelist.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 */ 924filelist.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 */ 958filelist.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 */ 1002filelist.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 */ 1019filelist.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}; 1037