blob: 70de83c17107ea8a76e8e514f6cf068711abd771 [file] [log] [blame]
// Copyright 2015 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.
/**
* Namespace for utility functions.
*/
var filelist = {};
/**
* File table list.
* @constructor
* @struct
* @extends {cr.ui.table.TableList}
*/
function FileTableList() {
throw new Error('Designed to decorate elements');
}
/**
* Decorates TableList as FileTableList.
* @param {!cr.ui.table.TableList} self A tabel list element.
*/
FileTableList.decorate = function(self) {
self.__proto__ = FileTableList.prototype;
};
FileTableList.prototype.__proto__ = cr.ui.table.TableList.prototype;
/**
* @type {?function(number, number)}
*/
FileTableList.prototype.onMergeItems_ = null;
/**
* @param {function(number, number)} onMergeItems callback called from
* |mergeItems| with the parameters |beginIndex| and |endIndex|.
*/
FileTableList.prototype.setOnMergeItems = function(onMergeItems) {
assert(!this.onMergeItems_);
this.onMergeItems_ = onMergeItems;
};
/** @override */
FileTableList.prototype.mergeItems = function(beginIndex, endIndex) {
cr.ui.table.TableList.prototype.mergeItems.call(this, beginIndex, endIndex);
// Make sure that list item's selected attribute is updated just after the
// mergeItems operation is done. This prevents checkmarks on selected items
// from being animated unintentionally by redraw.
for (var i = beginIndex; i < endIndex; i++) {
var item = this.getListItemByIndex(i);
if (!item)
continue;
var isSelected = this.selectionModel.getIndexSelected(i);
if (item.selected != isSelected)
item.selected = isSelected;
}
if (this.onMergeItems_) {
this.onMergeItems_(beginIndex, endIndex);
}
};
/** @override */
FileTableList.prototype.createSelectionController = function(sm) {
return new FileListSelectionController(assert(sm));
};
/**
* Selection controller for the file table list.
* @param {!cr.ui.ListSelectionModel} selectionModel The selection model to
* interact with.
* @constructor
* @extends {cr.ui.ListSelectionController}
* @struct
*/
function FileListSelectionController(selectionModel) {
cr.ui.ListSelectionController.call(this, selectionModel);
/**
* Whether to allow touch-specific interaction.
* @type {boolean}
*/
this.enableTouchMode_ = false;
util.isTouchModeEnabled().then(function(enabled) {
this.enableTouchMode_ = enabled;
}.bind(this));
/**
* @type {!FileTapHandler}
* @const
*/
this.tapHandler_ = new FileTapHandler();
}
FileListSelectionController.prototype = /** @struct */ {
__proto__: cr.ui.ListSelectionController.prototype
};
/** @override */
FileListSelectionController.prototype.handlePointerDownUp = function(e, index) {
filelist.handlePointerDownUp.call(this, e, index);
};
/** @override */
FileListSelectionController.prototype.handleTouchEvents = function(e, index) {
if (!this.enableTouchMode_)
return;
if (this.tapHandler_.handleTouchEvents(
e, index, filelist.handleTap.bind(this)))
// If a tap event is processed, FileTapHandler cancels the event to prevent
// triggering click events. Then it results not moving the focus to the
// list. So we do that here explicitly.
filelist.focusParentList(e);
};
/** @override */
FileListSelectionController.prototype.handleKeyDown = function(e) {
filelist.handleKeyDown.call(this, e);
};
/**
* Common item decoration for table's and grid's items.
* @param {cr.ui.ListItem} li List item.
* @param {Entry} entry The entry.
* @param {!MetadataModel} metadataModel Cache to
* retrieve metadada.
*/
filelist.decorateListItem = function(li, entry, metadataModel) {
li.classList.add(entry.isDirectory ? 'directory' : 'file');
// The metadata may not yet be ready. In that case, the list item will be
// updated when the metadata is ready via updateListItemsMetadata. For files
// not on an external backend, externalProps is not available.
var externalProps = metadataModel.getCache([entry], [
'hosted', 'availableOffline', 'customIconUrl', 'shared', 'isMachineRoot',
'isExternalMedia'
])[0];
filelist.updateListItemExternalProps(
li, externalProps, util.isTeamDriveRoot(entry));
// Overriding the default role 'list' to 'listbox' for better
// accessibility on ChromeOS.
li.setAttribute('role', 'option');
li.setAttribute('aria-describedby', 'more-actions-info');
Object.defineProperty(li, 'selected', {
/**
* @this {cr.ui.ListItem}
* @return {boolean} True if the list item is selected.
*/
get: function() {
return this.hasAttribute('selected');
},
/**
* @this {cr.ui.ListItem}
*/
set: function(v) {
if (v)
this.setAttribute('selected', '');
else
this.removeAttribute('selected');
}
});
};
/**
* Render the type column of the detail table.
* @param {!Document} doc Owner document.
* @param {!Entry} entry The Entry object to render.
* @param {string=} opt_mimeType Optional mime type for the file.
* @return {!HTMLDivElement} Created element.
*/
filelist.renderFileTypeIcon = function(doc, entry, opt_mimeType) {
var icon = /** @type {!HTMLDivElement} */ (doc.createElement('div'));
icon.className = 'detail-icon';
icon.setAttribute('file-type-icon', FileType.getIcon(entry, opt_mimeType));
return icon;
};
/**
* Render filename label for grid and list view.
* @param {!Document} 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 = /** @type {!HTMLDivElement} */ (doc.createElement('div'));
box.className = 'filename-label';
var fileName = doc.createElement('span');
fileName.className = 'entry-name';
fileName.textContent = entry.name;
box.appendChild(fileName);
return box;
};
/**
* Updates grid item or table row for the externalProps.
* @param {cr.ui.ListItem} li List item.
* @param {Object} externalProps Metadata.
*/
filelist.updateListItemExternalProps = function(
li, externalProps, isTeamDriveRoot) {
if (li.classList.contains('file')) {
if (externalProps.availableOffline === false)
li.classList.add('dim-offline');
else
li.classList.remove('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 (externalProps.hosted === true)
li.classList.add('dim-hosted');
else
li.classList.remove('dim-hosted');
}
var iconDiv = li.querySelector('.detail-icon');
if (!iconDiv)
return;
if (externalProps.customIconUrl)
iconDiv.style.backgroundImage = 'url(' + externalProps.customIconUrl + ')';
else
iconDiv.style.backgroundImage = ''; // Back to the default image.
if (li.classList.contains('directory')) {
iconDiv.classList.toggle('shared', !!externalProps.shared);
iconDiv.classList.toggle('team-drive-root', !!isTeamDriveRoot);
iconDiv.classList.toggle('computers-root', !!externalProps.isMachineRoot);
iconDiv.classList.toggle(
'external-media-root', !!externalProps.isExternalMedia);
}
};
/**
* Handles tap events on file list to change the selection state.
*
* @param {!Event} e The browser mouse event.
* @param {number} index The index that was under the mouse pointer, -1 if
* none.
* @param {!FileTapHandler.TapEvent} eventType
* @return True if conducted any action. False when if did nothing special for
* tap.
* @this {cr.ui.ListSelectionController}
*/
filelist.handleTap = function(e, index, eventType) {
var sm = /** @type {!FileListSelectionModel|!FileListSingleSelectionModel} */
(this.selectionModel);
if (eventType == FileTapHandler.TapEvent.TWO_FINGER_TAP) {
// Prepare to open the context menu in the same manner as the right click.
// If the target is any of the selected files, open a one for those files.
// If the target is a non-selected file, cancel current selection and open
// context menu for the single file.
// Otherwise (when the target is the background), for the current folder.
if (index == -1) {
// Two-finger tap outside the list should be handled here because it does
// not produce mousedown/click events.
sm.unselectAll();
} else {
var indexSelected = sm.getIndexSelected(index);
if (!indexSelected) {
// Prepare to open context menu of the new item by selecting only it.
if (sm.getCheckSelectMode()) {
// Unselect all items once to ensure that the check-select mode is
// terminated.
sm.unselectAll();
}
sm.beginChange();
sm.selectedIndex = index;
sm.endChange();
}
}
// Context menu will be opened for the selected files by the following
// 'contextmenu' event.
return false;
}
if (index == -1) {
return false;
}
var isTap = eventType == FileTapHandler.TapEvent.TAP ||
eventType == FileTapHandler.TapEvent.LONG_TAP;
// Revert to click handling for single tap on checkbox or tap during rename.
// Single tap on the checkbox in the list view mode should toggle select.
// Single tap on input for rename should focus on input.
var isCheckbox = e.target.classList.contains('detail-checkmark');
var isRename = e.target.localName == 'input';
if (eventType == FileTapHandler.TapEvent.TAP && (isCheckbox || isRename)) {
return false;
}
if (sm.multiple && sm.getCheckSelectMode() && isTap && !e.shiftKey) {
// toggle item selection. Equivalent to mouse click on checkbox.
sm.beginChange();
sm.setIndexSelected(index, !sm.getIndexSelected(index));
// Toggle the current one and make it anchor index.
sm.leadIndex = index;
sm.anchorIndex = index;
sm.endChange();
return true;
} else if (sm.multiple && (eventType == FileTapHandler.TapEvent.LONG_PRESS)) {
sm.beginChange();
if (!sm.getCheckSelectMode()) {
// Make sure to unselect the leading item that was not the touch target.
sm.unselectAll();
sm.setCheckSelectMode(true);
}
sm.setIndexSelected(index, true);
sm.leadIndex = index;
sm.anchorIndex = index;
sm.endChange();
return true;
// Do not toggle selection yet, so as to avoid unselecting before drag.
} else if (
eventType == FileTapHandler.TapEvent.TAP && !sm.getCheckSelectMode()) {
// Single tap should open the item with default action.
// Select the item, so that MainWindowComponent will execute action of it.
sm.beginChange();
sm.unselectAll();
sm.setIndexSelected(index, true);
sm.leadIndex = index;
sm.anchorIndex = index;
sm.endChange();
}
return false;
};
/**
* Handles mouseup/mousedown events on file list to change the selection state.
*
* Basically the content of this function is identical to
* cr.ui.ListSelectionController's handlePointerDownUp(), but following
* handlings are inserted to control the check-select mode.
*
* 1) When checkmark area is clicked, toggle item selection and enable the
* check-select mode.
* 2) When non-checkmark area is clicked in check-select mode, disable the
* check-select mode.
*
* @param {!Event} e The browser mouse event.
* @param {number} index The index that was under the mouse pointer, -1 if
* none.
* @this {cr.ui.ListSelectionController}
*/
filelist.handlePointerDownUp = function(e, index) {
var sm = /** @type {!FileListSelectionModel|!FileListSingleSelectionModel} */
(this.selectionModel);
var anchorIndex = sm.anchorIndex;
var isDown = (e.type == 'mousedown');
var isTargetCheckmark = e.target.classList.contains('detail-checkmark') ||
e.target.classList.contains('checkmark');
// If multiple selection is allowed and the checkmark is clicked without
// modifiers(Ctrl/Shift), the click should toggle the item's selection.
// (i.e. same behavior as Ctrl+Click)
var isClickOnCheckmark = isTargetCheckmark && sm.multiple && index != -1 &&
!e.shiftKey && !e.ctrlKey && e.button == 0;
sm.beginChange();
if (index == -1) {
sm.leadIndex = sm.anchorIndex = -1;
sm.unselectAll();
} else {
if (sm.multiple && (e.ctrlKey || isClickOnCheckmark) && !e.shiftKey) {
// Selection is handled at mouseUp.
if (!isDown) {
// 1) When checkmark area is clicked, toggle item selection and enable
// the check-select mode.
if (isClickOnCheckmark) {
// If Files app enters check-select mode by clicking an item's icon,
// existing selection should be cleared.
if (!sm.getCheckSelectMode())
sm.unselectAll();
}
// Always enables check-select mode when the selection is updated by
// Ctrl+Click or Click on an item's icon.
sm.setCheckSelectMode(true);
// Toggle the current one and make it anchor index.
sm.setIndexSelected(index, !sm.getIndexSelected(index));
sm.leadIndex = index;
sm.anchorIndex = index;
}
} else if (e.shiftKey && anchorIndex != -1 && anchorIndex != index) {
// Shift is done in mousedown.
if (isDown) {
sm.unselectAll();
sm.leadIndex = index;
if (sm.multiple)
sm.selectRange(anchorIndex, index);
else
sm.setIndexSelected(index, true);
}
} else {
// Right click for a context menu needs to not clear the selection.
var isRightClick = e.button == 2;
// If the index is selected this is handled in mouseup.
var indexSelected = sm.getIndexSelected(index);
if ((indexSelected && !isDown || !indexSelected && isDown) &&
!(indexSelected && isRightClick)) {
// 2) When non-checkmark area is clicked in check-select mode, disable
// the check-select mode.
if (sm.getCheckSelectMode()) {
// Unselect all items once to ensure that the check-select mode is
// terminated.
sm.endChange();
sm.unselectAll();
sm.beginChange();
}
sm.selectedIndex = index;
}
}
}
sm.endChange();
};
/**
* Handles key events on file list to change the selection state.
*
* Basically the content of this function is identical to
* cr.ui.ListSelectionController's handleKeyDown(), but following handlings is
* inserted to control the check-select mode.
*
* 1) When pressing direction key results in a single selection, the
* check-select mode should be terminated.
*
* @param {Event} e The keydown event.
* @this {cr.ui.ListSelectionController}
*/
filelist.handleKeyDown = function(e) {
var SPACE_KEY_CODE = 32;
var tagName = e.target.tagName;
// If focus is in an input field of some kind, only handle navigation keys
// that aren't likely to conflict with input interaction (e.g., text
// editing, or changing the value of a checkbox or select).
if (tagName == 'INPUT') {
var inputType = e.target.type;
// Just protect space (for toggling) for checkbox and radio.
if (inputType == 'checkbox' || inputType == 'radio') {
if (e.keyCode == SPACE_KEY_CODE)
return;
// Protect all but the most basic navigation commands in anything else.
} else if (e.key != 'ArrowUp' && e.key != 'ArrowDown') {
return;
}
}
// Similarly, don't interfere with select element handling.
if (tagName == 'SELECT')
return;
var sm = /** @type {!FileListSelectionModel|!FileListSingleSelectionModel} */
(this.selectionModel);
var newIndex = -1;
var leadIndex = sm.leadIndex;
var prevent = true;
// Ctrl/Meta+A
if (sm.multiple && e.keyCode == 65 &&
(cr.isMac && e.metaKey || !cr.isMac && e.ctrlKey)) {
sm.setCheckSelectMode(true);
sm.selectAll();
e.preventDefault();
return;
}
// Esc
if (e.keyCode === 27 && !e.ctrlKey && !e.shiftKey) {
sm.unselectAll();
e.preventDefault();
return;
}
// Space
if (e.keyCode == SPACE_KEY_CODE) {
if (leadIndex != -1) {
var selected = sm.getIndexSelected(leadIndex);
if (e.ctrlKey || !selected) {
sm.setIndexSelected(leadIndex, !selected || !sm.multiple);
return;
}
}
}
switch (e.key) {
case 'Home':
newIndex = this.getFirstIndex();
break;
case 'End':
newIndex = this.getLastIndex();
break;
case 'ArrowUp':
newIndex = leadIndex == -1 ?
this.getLastIndex() : this.getIndexAbove(leadIndex);
break;
case 'ArrowDown':
newIndex = leadIndex == -1 ?
this.getFirstIndex() : this.getIndexBelow(leadIndex);
break;
case 'ArrowLeft':
case 'MediaTrackPrevious':
newIndex = leadIndex == -1 ?
this.getLastIndex() : this.getIndexBefore(leadIndex);
break;
case 'ArrowRight':
case 'MediaTrackNext':
newIndex = leadIndex == -1 ?
this.getFirstIndex() : this.getIndexAfter(leadIndex);
break;
default:
prevent = false;
}
if (newIndex >= 0 && newIndex < sm.length) {
sm.beginChange();
sm.leadIndex = newIndex;
if (e.shiftKey) {
var anchorIndex = sm.anchorIndex;
if (sm.multiple)
sm.unselectAll();
if (anchorIndex == -1) {
sm.setIndexSelected(newIndex, true);
sm.anchorIndex = newIndex;
} else {
sm.selectRange(anchorIndex, newIndex);
}
} else {
// 1) When pressing direction key results in a single selection, the
// check-select mode should be terminated.
sm.setCheckSelectMode(false);
if (sm.multiple)
sm.unselectAll();
sm.setIndexSelected(newIndex, true);
sm.anchorIndex = newIndex;
}
sm.endChange();
if (prevent)
e.preventDefault();
}
};
/**
* Focus on the file list that contains the event target.
* @param {!Event} event the touch event.
*/
filelist.focusParentList = function(event) {
var element = event.target;
while (element && !(element instanceof cr.ui.List)) {
element = element.parentElement;
}
if (element) {
element.focus();
}
};