| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| 'use strict'; |
| |
| /** |
| * Namespace for utility functions. |
| */ |
| var filelist = {}; |
| |
| /** |
| * Custom column model for advanced auto-resizing. |
| * |
| * @param {Array.<cr.ui.table.TableColumn>} tableColumns Table columns. |
| * @extends {cr.ui.table.TableColumnModel} |
| * @constructor |
| */ |
| function FileTableColumnModel(tableColumns) { |
| cr.ui.table.TableColumnModel.call(this, tableColumns); |
| } |
| |
| /** |
| * The columns whose index is less than the constant are resizable. |
| * @const |
| * @type {number} |
| * @private |
| */ |
| FileTableColumnModel.RESIZABLE_LENGTH_ = 4; |
| |
| /** |
| * Inherits from cr.ui.TableColumnModel. |
| */ |
| FileTableColumnModel.prototype.__proto__ = |
| cr.ui.table.TableColumnModel.prototype; |
| |
| /** |
| * Minimum width of column. |
| * @const |
| * @type {number} |
| * @private |
| */ |
| FileTableColumnModel.MIN_WIDTH_ = 10; |
| |
| /** |
| * Sets column width so that the column dividers move to the specified position. |
| * This function also check the width of each column and keep the width larger |
| * than MIN_WIDTH_. |
| * |
| * @private |
| * @param {Array.<number>} newPos Positions of each column dividers. |
| */ |
| FileTableColumnModel.prototype.applyColumnPositions_ = function(newPos) { |
| // Check the minimum width and adjust the positions. |
| for (var i = 0; i < newPos.length - 2; i++) { |
| if (newPos[i + 1] - newPos[i] < FileTableColumnModel.MIN_WIDTH_) { |
| newPos[i + 1] = newPos[i] + FileTableColumnModel.MIN_WIDTH_; |
| } |
| } |
| for (var i = newPos.length - 1; i >= 2; i--) { |
| if (newPos[i] - newPos[i - 1] < FileTableColumnModel.MIN_WIDTH_) { |
| newPos[i - 1] = newPos[i] - FileTableColumnModel.MIN_WIDTH_; |
| } |
| } |
| // Set the new width of columns |
| for (var i = 0; i < FileTableColumnModel.RESIZABLE_LENGTH_; i++) { |
| this.columns_[i].width = newPos[i + 1] - newPos[i]; |
| } |
| }; |
| |
| /** |
| * Normalizes widths to make their sum 100% if possible. Uses the proportional |
| * approach with some additional constraints. |
| * |
| * @param {number} contentWidth Target width. |
| * @override |
| */ |
| FileTableColumnModel.prototype.normalizeWidths = function(contentWidth) { |
| var totalWidth = 0; |
| var fixedWidth = 0; |
| // Some columns have fixed width. |
| for (var i = 0; i < this.columns_.length; i++) { |
| if (i < FileTableColumnModel.RESIZABLE_LENGTH_) |
| totalWidth += this.columns_[i].width; |
| else |
| fixedWidth += this.columns_[i].width; |
| } |
| var newTotalWidth = Math.max(contentWidth - fixedWidth, 0); |
| var positions = [0]; |
| var sum = 0; |
| for (var i = 0; i < FileTableColumnModel.RESIZABLE_LENGTH_; i++) { |
| var column = this.columns_[i]; |
| sum += column.width; |
| // Faster alternative to Math.floor for non-negative numbers. |
| positions[i + 1] = ~~(newTotalWidth * sum / totalWidth); |
| } |
| this.applyColumnPositions_(positions); |
| }; |
| |
| /** |
| * Handles to the start of column resizing by splitters. |
| */ |
| FileTableColumnModel.prototype.handleSplitterDragStart = function() { |
| this.columnPos_ = [0]; |
| for (var i = 0; i < this.columns_.length; i++) { |
| this.columnPos_[i + 1] = this.columns_[i].width + this.columnPos_[i]; |
| } |
| }; |
| |
| /** |
| * Handles to the end of column resizing by splitters. |
| */ |
| FileTableColumnModel.prototype.handleSplitterDragEnd = function() { |
| this.columnPos_ = null; |
| }; |
| |
| /** |
| * Sets the width of column with keeping the total width of table. |
| * @param {number} columnIndex Index of column that is resized. |
| * @param {number} columnWidth New width of the column. |
| */ |
| FileTableColumnModel.prototype.setWidthAndKeepTotal = function( |
| columnIndex, columnWidth) { |
| // Skip to resize 'selection' column |
| if (columnIndex < 0 || |
| columnIndex >= FileTableColumnModel.RESIZABLE_LENGTH_ || |
| !this.columnPos_) { |
| return; |
| } |
| |
| // Calculate new positions of column splitters. |
| var newPosStart = |
| this.columnPos_[columnIndex] + Math.max(columnWidth, |
| FileTableColumnModel.MIN_WIDTH_); |
| var newPos = []; |
| var posEnd = this.columnPos_[FileTableColumnModel.RESIZABLE_LENGTH_]; |
| for (var i = 0; i < columnIndex + 1; i++) { |
| newPos[i] = this.columnPos_[i]; |
| } |
| for (var i = columnIndex + 1; |
| i < FileTableColumnModel.RESIZABLE_LENGTH_; |
| i++) { |
| var posStart = this.columnPos_[columnIndex + 1]; |
| newPos[i] = (posEnd - newPosStart) * |
| (this.columnPos_[i] - posStart) / |
| (posEnd - posStart) + |
| newPosStart; |
| // Faster alternative to Math.floor for non-negative numbers. |
| newPos[i] = ~~newPos[i]; |
| } |
| newPos[columnIndex] = this.columnPos_[columnIndex]; |
| newPos[FileTableColumnModel.RESIZABLE_LENGTH_] = posEnd; |
| this.applyColumnPositions_(newPos); |
| |
| // Notifiy about resizing |
| cr.dispatchSimpleEvent(this, 'resize'); |
| }; |
| |
| /** |
| * Custom splitter that resizes column with retaining the sum of all the column |
| * width. |
| */ |
| var FileTableSplitter = cr.ui.define('div'); |
| |
| /** |
| * Inherits from cr.ui.TableSplitter. |
| */ |
| FileTableSplitter.prototype.__proto__ = cr.ui.TableSplitter.prototype; |
| |
| /** |
| * Handles the drag start event. |
| */ |
| FileTableSplitter.prototype.handleSplitterDragStart = function() { |
| cr.ui.TableSplitter.prototype.handleSplitterDragStart.call(this); |
| this.table_.columnModel.handleSplitterDragStart(); |
| }; |
| |
| /** |
| * Handles the drag move event. |
| * @param {number} deltaX Horizontal mouse move offset. |
| */ |
| FileTableSplitter.prototype.handleSplitterDragMove = function(deltaX) { |
| this.table_.columnModel.setWidthAndKeepTotal(this.columnIndex, |
| this.columnWidth_ + deltaX, |
| true); |
| }; |
| |
| /** |
| * Handles the drag end event. |
| */ |
| FileTableSplitter.prototype.handleSplitterDragEnd = function() { |
| cr.ui.TableSplitter.prototype.handleSplitterDragEnd.call(this); |
| this.table_.columnModel.handleSplitterDragEnd(); |
| }; |
| |
| /** |
| * File list Table View. |
| * @constructor |
| */ |
| function FileTable() { |
| throw new Error('Designed to decorate elements'); |
| } |
| |
| /** |
| * Inherits from cr.ui.Table. |
| */ |
| FileTable.prototype.__proto__ = cr.ui.Table.prototype; |
| |
| /** |
| * Decorates the element. |
| * @param {HTMLElement} self Table to decorate. |
| * @param {MetadataCache} metadataCache To retrieve metadata. |
| * @param {boolean} fullPage True if it's full page File Manager, |
| * False if a file open/save dialog. |
| */ |
| FileTable.decorate = function(self, metadataCache, fullPage) { |
| cr.ui.Table.decorate(self); |
| self.__proto__ = FileTable.prototype; |
| self.metadataCache_ = metadataCache; |
| self.collator_ = Intl.Collator([], {numeric: true, sensitivity: 'base'}); |
| |
| var columns = [ |
| new cr.ui.table.TableColumn('name', str('NAME_COLUMN_LABEL'), |
| fullPage ? 386 : 324), |
| new cr.ui.table.TableColumn('size', str('SIZE_COLUMN_LABEL'), |
| 110, true), |
| new cr.ui.table.TableColumn('type', str('TYPE_COLUMN_LABEL'), |
| fullPage ? 110 : 110), |
| new cr.ui.table.TableColumn('modificationTime', |
| str('DATE_COLUMN_LABEL'), |
| fullPage ? 150 : 210) |
| ]; |
| |
| columns[0].renderFunction = self.renderName_.bind(self); |
| columns[1].renderFunction = self.renderSize_.bind(self); |
| columns[1].defaultOrder = 'desc'; |
| columns[2].renderFunction = self.renderType_.bind(self); |
| columns[3].renderFunction = self.renderDate_.bind(self); |
| columns[3].defaultOrder = 'desc'; |
| |
| var tableColumnModelClass; |
| tableColumnModelClass = FileTableColumnModel; |
| if (self.showCheckboxes) { |
| columns.push(new cr.ui.table.TableColumn('selection', |
| '', |
| 50, true)); |
| columns[4].renderFunction = self.renderSelection_.bind(self); |
| columns[4].headerRenderFunction = |
| self.renderSelectionColumnHeader_.bind(self); |
| columns[4].fixed = true; |
| } |
| |
| var columnModel = Object.create(tableColumnModelClass.prototype, { |
| /** |
| * The number of columns. |
| * @type {number} |
| */ |
| size: { |
| /** |
| * @this {FileTableColumnModel} |
| * @return {number} Number of columns. |
| */ |
| get: function() { |
| return this.totalSize; |
| } |
| }, |
| |
| /** |
| * The number of columns. |
| * @type {number} |
| */ |
| totalSize: { |
| /** |
| * @this {FileTableColumnModel} |
| * @return {number} Number of columns. |
| */ |
| get: function() { |
| return columns.length; |
| } |
| }, |
| |
| /** |
| * Obtains a column by the specified horizontal position. |
| */ |
| getHitColumn: { |
| /** |
| * @this {FileTableColumnModel} |
| * @param {number} x Horizontal position. |
| * @return {object} The object that contains column index, column width, |
| * and hitPosition where the horizontal position is hit in the column. |
| */ |
| value: function(x) { |
| for (var i = 0; x >= this.columns_[i].width; i++) { |
| x -= this.columns_[i].width; |
| } |
| if (i >= this.columns_.length) |
| return null; |
| return {index: i, hitPosition: x, width: this.columns_[i].width}; |
| } |
| } |
| }); |
| |
| tableColumnModelClass.call(columnModel, columns); |
| self.columnModel = columnModel; |
| self.setDateTimeFormat(true); |
| self.setRenderFunction(self.renderTableRow_.bind(self, |
| self.getRenderFunction())); |
| |
| self.scrollBar_ = MainPanelScrollBar(); |
| self.scrollBar_.initialize(self, self.list); |
| // Keep focus on the file list when clicking on the header. |
| self.header.addEventListener('mousedown', function(e) { |
| self.list.focus(); |
| e.preventDefault(); |
| }); |
| |
| var handleSelectionChange = function() { |
| var selectAll = self.querySelector('#select-all-checkbox'); |
| if (selectAll) |
| self.updateSelectAllCheckboxState_(selectAll); |
| }; |
| |
| self.relayoutAggregation_ = |
| new AsyncUtil.Aggregation(self.relayoutImmediately_.bind(self)); |
| |
| Object.defineProperty(self.list_, 'selectionModel', { |
| /** |
| * @this {cr.ui.List} |
| * @return {cr.ui.ListSelectionModel} The current selection model. |
| */ |
| get: function() { |
| return this.selectionModel_; |
| }, |
| /** |
| * @this {cr.ui.List} |
| */ |
| set: function(value) { |
| var sm = this.selectionModel; |
| if (sm) |
| sm.removeEventListener('change', handleSelectionChange); |
| |
| util.callInheritedSetter(this, 'selectionModel', value); |
| sm = value; |
| |
| if (sm) |
| sm.addEventListener('change', handleSelectionChange); |
| handleSelectionChange(); |
| } |
| }); |
| |
| // Override header#redraw to use FileTableSplitter. |
| self.header_.redraw = function() { |
| this.__proto__.redraw.call(this); |
| // Extend table splitters |
| var splitters = this.querySelectorAll('.table-header-splitter'); |
| for (var i = 0; i < splitters.length; i++) { |
| if (splitters[i] instanceof FileTableSplitter) |
| continue; |
| FileTableSplitter.decorate(splitters[i]); |
| } |
| }; |
| |
| // Save the last selection. This is used by shouldStartDragSelection. |
| self.list.addEventListener('mousedown', function(e) { |
| this.lastSelection_ = this.selectionModel.selectedIndexes; |
| }.bind(self), true); |
| self.list.shouldStartDragSelection = |
| self.shouldStartDragSelection_.bind(self); |
| |
| /** |
| * Obtains the index list of elements that are hit by the point or the |
| * rectangle. |
| * |
| * @param {number} x X coordinate value. |
| * @param {number} y Y coordinate value. |
| * @param {=number} opt_width Width of the coordinate. |
| * @param {=number} opt_height Height of the coordinate. |
| * @return {Array.<number>} Index list of hit elements. |
| */ |
| self.list.getHitElements = function(x, y, opt_width, opt_height) { |
| var currentSelection = []; |
| var bottom = y + (opt_height || 0); |
| for (var i = 0; i < this.selectionModel_.length; i++) { |
| var itemMetrics = this.getHeightsForIndex_(i); |
| if (itemMetrics.top < bottom && itemMetrics.top + itemMetrics.height >= y) |
| currentSelection.push(i); |
| } |
| return currentSelection; |
| }; |
| }; |
| |
| /** |
| * Sets date and time format. |
| * @param {boolean} use12hourClock True if 12 hours clock, False if 24 hours. |
| */ |
| FileTable.prototype.setDateTimeFormat = function(use12hourClock) { |
| this.timeFormatter_ = Intl.DateTimeFormat( |
| [] /* default locale */, |
| {hour: 'numeric', minute: 'numeric', |
| hour12: use12hourClock}); |
| this.dateFormatter_ = Intl.DateTimeFormat( |
| [] /* default locale */, |
| {year: 'numeric', month: 'short', day: 'numeric', |
| hour: 'numeric', minute: 'numeric', |
| hour12: use12hourClock}); |
| }; |
| |
| /** |
| * Obtains if the drag selection should be start or not by referring the mouse |
| * event. |
| * @param {MouseEvent} event Drag start event. |
| * @return {boolean} True if the mouse is hit to the background of the list. |
| * @private |
| */ |
| FileTable.prototype.shouldStartDragSelection_ = function(event) { |
| // If the shift key is pressed, it should starts drag selection. |
| if (event.shiftKey) |
| return true; |
| |
| // If the position values are negative, it points the out of list. |
| // It should start the drag selection. |
| var pos = DragSelector.getScrolledPosition(this.list, event); |
| if (!pos) |
| return false; |
| if (pos.x < 0 || pos.y < 0) |
| return true; |
| |
| // If the item index is out of range, it should start the drag selection. |
| var itemHeight = this.list.measureItem().height; |
| // Faster alternative to Math.floor for non-negative numbers. |
| var itemIndex = ~~(pos.y / itemHeight); |
| if (itemIndex >= this.list.dataModel.length) |
| return true; |
| |
| // If the pointed item is already selected, it should not start the drag |
| // selection. |
| if (this.lastSelection_.indexOf(itemIndex) != -1) |
| return false; |
| |
| // If the horizontal value is not hit to column, it should start the drag |
| // selection. |
| var hitColumn = this.columnModel.getHitColumn(pos.x); |
| if (!hitColumn) |
| return true; |
| |
| // Check if the point is on the column contents or not. |
| var item = this.list.getListItemByIndex(itemIndex); |
| switch (this.columnModel.columns_[hitColumn.index].id) { |
| case 'name': |
| var spanElement = item.querySelector('.filename-label span'); |
| var spanRect = spanElement.getBoundingClientRect(); |
| // The this.list.cachedBounds_ object is set by |
| // DragSelector.getScrolledPosition. |
| if (!this.list.cachedBounds) |
| return true; |
| var textRight = |
| spanRect.left - this.list.cachedBounds.left + spanRect.width; |
| return textRight <= hitColumn.hitPosition; |
| default: |
| return true; |
| } |
| }; |
| |
| /** |
| * Update check and disable states of the 'Select all' checkbox. |
| * @param {HTMLInputElement} checkbox The checkbox. If not passed, using |
| * the default one. |
| * @private |
| */ |
| FileTable.prototype.updateSelectAllCheckboxState_ = function(checkbox) { |
| // TODO(serya): introduce this.selectionModel.selectedCount. |
| checkbox.checked = this.dataModel.length > 0 && |
| this.dataModel.length == this.selectionModel.selectedIndexes.length; |
| checkbox.disabled = this.dataModel.length == 0; |
| }; |
| |
| /** |
| * Prepares the data model to be sorted by columns. |
| * @param {cr.ui.ArrayDataModel} dataModel Data model to prepare. |
| */ |
| FileTable.prototype.setupCompareFunctions = function(dataModel) { |
| dataModel.setCompareFunction('name', |
| this.compareName_.bind(this)); |
| dataModel.setCompareFunction('modificationTime', |
| this.compareMtime_.bind(this)); |
| dataModel.setCompareFunction('size', |
| this.compareSize_.bind(this)); |
| dataModel.setCompareFunction('type', |
| this.compareType_.bind(this)); |
| }; |
| |
| /** |
| * Render the Name column of the detail table. |
| * |
| * Invoked by cr.ui.Table when a file needs to be rendered. |
| * |
| * @param {Entry} entry The Entry object to render. |
| * @param {string} columnId The id of the column to be rendered. |
| * @param {cr.ui.Table} table The table doing the rendering. |
| * @return {HTMLDivElement} Created element. |
| * @private |
| */ |
| FileTable.prototype.renderName_ = function(entry, columnId, table) { |
| var label = this.ownerDocument.createElement('div'); |
| label.appendChild(this.renderIconType_(entry, columnId, table)); |
| label.entry = entry; |
| label.className = 'detail-name'; |
| label.appendChild(filelist.renderFileNameLabel(this.ownerDocument, entry)); |
| return label; |
| }; |
| |
| /** |
| * Render the Selection column of the detail table. |
| * |
| * Invoked by cr.ui.Table when a file needs to be rendered. |
| * |
| * @param {Entry} entry The Entry object to render. |
| * @param {string} columnId The id of the column to be rendered. |
| * @param {cr.ui.Table} table The table doing the rendering. |
| * @return {HTMLDivElement} Created element. |
| * @private |
| */ |
| FileTable.prototype.renderSelection_ = function(entry, columnId, table) { |
| var label = this.ownerDocument.createElement('div'); |
| label.className = 'selection-label'; |
| if (this.selectionModel.multiple) { |
| var checkBox = this.ownerDocument.createElement('input'); |
| filelist.decorateSelectionCheckbox(checkBox, entry, this.list); |
| label.appendChild(checkBox); |
| } |
| return label; |
| }; |
| |
| /** |
| * Render the Size column of the detail table. |
| * |
| * @param {Entry} entry The Entry object to render. |
| * @param {string} columnId The id of the column to be rendered. |
| * @param {cr.ui.Table} table The table doing the rendering. |
| * @return {HTMLDivElement} Created element. |
| * @private |
| */ |
| FileTable.prototype.renderSize_ = function(entry, columnId, table) { |
| var div = this.ownerDocument.createElement('div'); |
| div.className = 'size'; |
| this.updateSize_( |
| div, entry, this.metadataCache_.getCached(entry, 'filesystem')); |
| |
| return div; |
| }; |
| |
| /** |
| * Sets up or updates the size cell. |
| * |
| * @param {HTMLDivElement} div The table cell. |
| * @param {Entry} entry The corresponding entry. |
| * @param {Object} filesystemProps Metadata. |
| * @private |
| */ |
| FileTable.prototype.updateSize_ = function(div, entry, filesystemProps) { |
| if (!filesystemProps) { |
| div.textContent = '...'; |
| } else if (filesystemProps.size == -1) { |
| div.textContent = '--'; |
| } else if (filesystemProps.size == 0 && |
| FileType.isHosted(entry)) { |
| div.textContent = '--'; |
| } else { |
| div.textContent = util.bytesToString(filesystemProps.size); |
| } |
| }; |
| |
| /** |
| * Render the Type column of the detail table. |
| * |
| * @param {Entry} entry The Entry object to render. |
| * @param {string} columnId The id of the column to be rendered. |
| * @param {cr.ui.Table} table The table doing the rendering. |
| * @return {HTMLDivElement} Created element. |
| * @private |
| */ |
| FileTable.prototype.renderType_ = function(entry, columnId, table) { |
| var div = this.ownerDocument.createElement('div'); |
| div.className = 'type'; |
| div.textContent = FileType.getTypeString(entry); |
| return div; |
| }; |
| |
| /** |
| * Render the Date column of the detail table. |
| * |
| * @param {Entry} entry The Entry object to render. |
| * @param {string} columnId The id of the column to be rendered. |
| * @param {cr.ui.Table} table The table doing the rendering. |
| * @return {HTMLDivElement} Created element. |
| * @private |
| */ |
| FileTable.prototype.renderDate_ = function(entry, columnId, table) { |
| var div = this.ownerDocument.createElement('div'); |
| div.className = 'date'; |
| |
| this.updateDate_(div, |
| this.metadataCache_.getCached(entry, 'filesystem')); |
| return div; |
| }; |
| |
| /** |
| * Sets up or updates the date cell. |
| * |
| * @param {HTMLDivElement} div The table cell. |
| * @param {Object} filesystemProps Metadata. |
| * @private |
| */ |
| FileTable.prototype.updateDate_ = function(div, filesystemProps) { |
| if (!filesystemProps) { |
| div.textContent = '...'; |
| return; |
| } |
| |
| var modTime = filesystemProps.modificationTime; |
| var today = new Date(); |
| today.setHours(0); |
| today.setMinutes(0); |
| today.setSeconds(0); |
| today.setMilliseconds(0); |
| |
| /** |
| * Number of milliseconds in a day. |
| */ |
| var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000; |
| |
| if (modTime >= today && |
| modTime < today.getTime() + MILLISECONDS_IN_DAY) { |
| div.textContent = strf('TIME_TODAY', this.timeFormatter_.format(modTime)); |
| } else if (modTime >= today - MILLISECONDS_IN_DAY && modTime < today) { |
| div.textContent = strf('TIME_YESTERDAY', |
| this.timeFormatter_.format(modTime)); |
| } else { |
| div.textContent = |
| this.dateFormatter_.format(filesystemProps.modificationTime); |
| } |
| }; |
| |
| /** |
| * Updates the file metadata in the table item. |
| * |
| * @param {Element} item Table item. |
| * @param {Entry} entry File entry. |
| */ |
| FileTable.prototype.updateFileMetadata = function(item, entry) { |
| var props = this.metadataCache_.getCached(entry, 'filesystem'); |
| this.updateDate_(item.querySelector('.date'), props); |
| this.updateSize_(item.querySelector('.size'), entry, props); |
| }; |
| |
| /** |
| * Updates list items 'in place' on metadata change. |
| * @param {string} type Type of metadata change. |
| * @param {Object.<sting, Object>} propsMap Map from entry URLs to metadata |
| * properties. |
| */ |
| FileTable.prototype.updateListItemsMetadata = function(type, propsMap) { |
| var forEachCell = function(selector, callback) { |
| var cells = this.querySelectorAll(selector); |
| for (var i = 0; i < cells.length; i++) { |
| var cell = cells[i]; |
| var listItem = this.list_.getListItemAncestor(cell); |
| var entry = this.dataModel.item(listItem.listIndex); |
| if (entry) { |
| var props = propsMap[entry.toURL()]; |
| if (props) |
| callback.call(this, cell, entry, props, listItem); |
| } |
| } |
| }.bind(this); |
| if (type == 'filesystem') { |
| forEachCell('.table-row-cell > .date', function(item, entry, props) { |
| this.updateDate_(item, props); |
| }); |
| forEachCell('.table-row-cell > .size', function(item, entry, props) { |
| this.updateSize_(item, entry, props); |
| }); |
| } else if (type == 'drive') { |
| // The cell name does not matter as the entire list item is needed. |
| forEachCell('.table-row-cell > .date', |
| function(item, entry, props, listItem) { |
| filelist.updateListItemDriveProps(listItem, props); |
| }); |
| } |
| }; |
| |
| /** |
| * Compare by mtime first, then by name. |
| * @param {Entry} a First entry. |
| * @param {Entry} b Second entry. |
| * @return {number} Compare result. |
| * @private |
| */ |
| FileTable.prototype.compareName_ = function(a, b) { |
| return this.collator_.compare(a.name, b.name); |
| }; |
| |
| /** |
| * Compare by mtime first, then by name. |
| * @param {Entry} a First entry. |
| * @param {Entry} b Second entry. |
| * @return {number} Compare result. |
| * @private |
| */ |
| FileTable.prototype.compareMtime_ = function(a, b) { |
| var aCachedFilesystem = this.metadataCache_.getCached(a, 'filesystem'); |
| var aTime = aCachedFilesystem ? aCachedFilesystem.modificationTime : 0; |
| |
| var bCachedFilesystem = this.metadataCache_.getCached(b, 'filesystem'); |
| var bTime = bCachedFilesystem ? bCachedFilesystem.modificationTime : 0; |
| |
| if (aTime > bTime) |
| return 1; |
| |
| if (aTime < bTime) |
| return -1; |
| |
| return this.collator_.compare(a.name, b.name); |
| }; |
| |
| /** |
| * Compare by size first, then by name. |
| * @param {Entry} a First entry. |
| * @param {Entry} b Second entry. |
| * @return {number} Compare result. |
| * @private |
| */ |
| FileTable.prototype.compareSize_ = function(a, b) { |
| var aCachedFilesystem = this.metadataCache_.getCached(a, 'filesystem'); |
| var aSize = aCachedFilesystem ? aCachedFilesystem.size : 0; |
| |
| var bCachedFilesystem = this.metadataCache_.getCached(b, 'filesystem'); |
| var bSize = bCachedFilesystem ? bCachedFilesystem.size : 0; |
| |
| if (aSize != bSize) return aSize - bSize; |
| return this.collator_.compare(a.name, b.name); |
| }; |
| |
| /** |
| * Compare by type first, then by subtype and then by name. |
| * @param {Entry} a First entry. |
| * @param {Entry} b Second entry. |
| * @return {number} Compare result. |
| * @private |
| */ |
| FileTable.prototype.compareType_ = function(a, b) { |
| // Directories precede files. |
| if (a.isDirectory != b.isDirectory) |
| return Number(b.isDirectory) - Number(a.isDirectory); |
| |
| var aType = FileType.getTypeString(a); |
| var bType = FileType.getTypeString(b); |
| |
| var result = this.collator_.compare(aType, bType); |
| if (result != 0) |
| return result; |
| |
| return this.collator_.compare(a.name, b.name); |
| }; |
| |
| /** |
| * Renders table row. |
| * @param {function(Entry, cr.ui.Table)} baseRenderFunction Base renderer. |
| * @param {Entry} entry Corresponding entry. |
| * @return {HTMLLiElement} Created element. |
| * @private |
| */ |
| FileTable.prototype.renderTableRow_ = function(baseRenderFunction, entry) { |
| var item = baseRenderFunction(entry, this); |
| filelist.decorateListItem(item, entry, this.metadataCache_); |
| return item; |
| }; |
| |
| /** |
| * Renders the name column header. |
| * @param {string} name Localized column name. |
| * @return {HTMLLiElement} Created element. |
| * @private |
| */ |
| FileTable.prototype.renderNameColumnHeader_ = function(name) { |
| if (!this.selectionModel.multiple) |
| return this.ownerDocument.createTextNode(name); |
| |
| var input = this.ownerDocument.createElement('input'); |
| input.setAttribute('type', 'checkbox'); |
| input.setAttribute('tabindex', -1); |
| input.id = 'select-all-checkbox'; |
| input.className = 'common'; |
| |
| this.updateSelectAllCheckboxState_(input); |
| |
| input.addEventListener('click', function(event) { |
| if (input.checked) |
| this.selectionModel.selectAll(); |
| else |
| this.selectionModel.unselectAll(); |
| event.stopPropagation(); |
| }.bind(this)); |
| |
| var fragment = this.ownerDocument.createDocumentFragment(); |
| fragment.appendChild(input); |
| fragment.appendChild(this.ownerDocument.createTextNode(name)); |
| return fragment; |
| }; |
| |
| /** |
| * Renders the selection column header. |
| * @param {string} name Localized column name. |
| * @return {HTMLLiElement} Created element. |
| * @private |
| */ |
| FileTable.prototype.renderSelectionColumnHeader_ = function(name) { |
| if (!this.selectionModel.multiple) |
| return this.ownerDocument.createTextNode(''); |
| |
| var input = this.ownerDocument.createElement('input'); |
| input.setAttribute('type', 'checkbox'); |
| input.setAttribute('tabindex', -1); |
| input.id = 'select-all-checkbox'; |
| input.className = 'common'; |
| |
| this.updateSelectAllCheckboxState_(input); |
| |
| input.addEventListener('click', function(event) { |
| if (input.checked) |
| this.selectionModel.selectAll(); |
| else |
| this.selectionModel.unselectAll(); |
| event.stopPropagation(); |
| }.bind(this)); |
| |
| var fragment = this.ownerDocument.createDocumentFragment(); |
| fragment.appendChild(input); |
| return fragment; |
| }; |
| |
| /** |
| * Render the type column of the detail table. |
| * |
| * Invoked by cr.ui.Table when a file needs to be rendered. |
| * |
| * @param {Entry} entry The Entry object to render. |
| * @param {string} columnId The id of the column to be rendered. |
| * @param {cr.ui.Table} table The table doing the rendering. |
| * @return {HTMLDivElement} Created element. |
| * @private |
| */ |
| FileTable.prototype.renderIconType_ = function(entry, columnId, table) { |
| var icon = this.ownerDocument.createElement('div'); |
| icon.className = 'detail-icon'; |
| icon.setAttribute('file-type-icon', FileType.getIcon(entry)); |
| return icon; |
| }; |
| |
| /** |
| * Sets the margin height for the transparent preview panel at the bottom. |
| * @param {number} margin Margin to be set in px. |
| */ |
| FileTable.prototype.setBottomMarginForPanel = function(margin) { |
| this.list_.style.paddingBottom = margin + 'px'; |
| this.scrollBar_.setBottomMarginForPanel(margin); |
| }; |
| |
| /** |
| * Redraws the UI. Skips multiple consecutive calls. |
| */ |
| FileTable.prototype.relayout = function() { |
| this.relayoutAggregation_.run(); |
| }; |
| |
| /** |
| * Redraws the UI immediately. |
| * @private |
| */ |
| FileTable.prototype.relayoutImmediately_ = function() { |
| if (this.clientWidth > 0) |
| this.normalizeColumns(); |
| this.redraw(); |
| cr.dispatchSimpleEvent(this.list, 'relayout'); |
| }; |
| |
| /** |
| * Decorates (and wire up) a checkbox to be used in either a detail or a |
| * thumbnail list item. |
| * @param {HTMLInputElement} input Element to decorate. |
| */ |
| filelist.decorateCheckbox = function(input) { |
| var stopEventPropagation = function(event) { |
| if (!event.shiftKey) |
| event.stopPropagation(); |
| }; |
| input.setAttribute('type', 'checkbox'); |
| input.setAttribute('tabindex', -1); |
| input.classList.add('common'); |
| input.addEventListener('mousedown', stopEventPropagation); |
| input.addEventListener('mouseup', stopEventPropagation); |
| |
| input.addEventListener( |
| 'click', |
| /** |
| * @this {HTMLInputElement} |
| */ |
| function(event) { |
| // Revert default action and swallow the event |
| // if this is a multiple click or Shift is pressed. |
| if (event.detail > 1 || event.shiftKey) { |
| this.checked = !this.checked; |
| stopEventPropagation(event); |
| } |
| }); |
| }; |
| |
| /** |
| * Decorates selection checkbox. |
| * @param {HTMLInputElement} input Element to decorate. |
| * @param {Entry} entry Corresponding entry. |
| * @param {cr.ui.List} list Owner list. |
| */ |
| filelist.decorateSelectionCheckbox = function(input, entry, list) { |
| filelist.decorateCheckbox(input); |
| input.classList.add('file-checkbox'); |
| input.addEventListener('click', function(e) { |
| var sm = list.selectionModel; |
| var listIndex = list.getListItemAncestor(this).listIndex; |
| sm.setIndexSelected(listIndex, this.checked); |
| sm.leadIndex = listIndex; |
| if (sm.anchorIndex == -1) |
| sm.anchorIndex = listIndex; |
| |
| }); |
| // Since we do not want to open the item when tap on checkbox, we need to |
| // stop propagation of TAP event dispatched by checkbox ideally. But all |
| // touch events from touch_handler are dispatched to the list control. So we |
| // have to stop propagation of native touchstart event to prevent list |
| // control from generating TAP event here. The synthetic click event will |
| // select the touched checkbox/item. |
| input.addEventListener('touchstart', |
| function(e) { e.stopPropagation() }); |
| |
| var index = list.dataModel.indexOf(entry); |
| // Our DOM nodes get discarded as soon as we're scrolled out of view, |
| // so we have to make sure the check state is correct when we're brought |
| // back to life. |
| input.checked = list.selectionModel.getIndexSelected(index); |
| }; |
| |
| /** |
| * Common item decoration for table's and grid's items. |
| * @param {ListItem} li List item. |
| * @param {Entry} entry The entry. |
| * @param {MetadataCache} metadataCache Cache to retrieve metadada. |
| */ |
| filelist.decorateListItem = function(li, entry, metadataCache) { |
| li.classList.add(entry.isDirectory ? 'directory' : 'file'); |
| if (FileType.isOnDrive(entry)) { |
| // The metadata may not yet be ready. In that case, the list item will be |
| // updated when the metadata is ready via updateListItemsMetadata. |
| var driveProps = metadataCache.getCached(entry, 'drive'); |
| if (driveProps) |
| filelist.updateListItemDriveProps(li, driveProps); |
| } |
| |
| // Overriding the default role 'list' to 'listbox' for better |
| // accessibility on ChromeOS. |
| li.setAttribute('role', 'option'); |
| |
| Object.defineProperty(li, 'selected', { |
| /** |
| * @this {ListItem} |
| * @return {boolean} True if the list item is selected. |
| */ |
| get: function() { |
| return this.hasAttribute('selected'); |
| }, |
| |
| /** |
| * @this {ListItem} |
| */ |
| set: function(v) { |
| if (v) |
| this.setAttribute('selected'); |
| else |
| this.removeAttribute('selected'); |
| var checkBox = this.querySelector('input.file-checkbox'); |
| if (checkBox) |
| checkBox.checked = !!v; |
| } |
| }); |
| }; |
| |
| /** |
| * Render filename label for grid and list view. |
| * @param {HTMLDocument} doc Owner document. |
| * @param {Entry} entry The Entry object to render. |
| * @return {HTMLDivElement} The label. |
| */ |
| filelist.renderFileNameLabel = function(doc, entry) { |
| // Filename need to be in a '.filename-label' container for correct |
| // work of inplace renaming. |
| var box = doc.createElement('div'); |
| box.className = 'filename-label'; |
| var fileName = doc.createElement('span'); |
| fileName.textContent = entry.name; |
| box.appendChild(fileName); |
| |
| return box; |
| }; |
| |
| /** |
| * Updates grid item or table row for the driveProps. |
| * @param {cr.ui.ListItem} li List item. |
| * @param {Object} driveProps Metadata. |
| */ |
| filelist.updateListItemDriveProps = function(li, driveProps) { |
| if (li.classList.contains('file')) { |
| if (driveProps.availableOffline) |
| li.classList.remove('dim-offline'); |
| else |
| li.classList.add('dim-offline'); |
| // TODO(mtomasz): Consider adding some vidual indication for files which |
| // are not cached on LTE. Currently we show them as normal files. |
| // crbug.com/246611. |
| } |
| |
| if (driveProps.customIconUrl) { |
| var iconDiv = li.querySelector('.detail-icon'); |
| if (!iconDiv) |
| return; |
| iconDiv.style.backgroundImage = 'url(' + driveProps.customIconUrl + ')'; |
| } |
| }; |