blob: 6b0b208bf6b9e0f38112a3c9cdf2992d96406612 [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.
/**
* Thumbnail Mode.
* @param {!HTMLElement} container A container.
* @param {!ErrorBanner} errorBanner Error banner.
* @param {!GalleryDataModel} dataModel Gallery data model.
* @param {!cr.ui.ListSelectionModel} selectionModel List selection model.
* @param {function()} changeToSlideModeCallback A callback to be called to
* change to slide mode.
* @constructor
* @extends {cr.EventTarget}
* @struct
*/
function ThumbnailMode(container, errorBanner, dataModel, selectionModel,
changeToSlideModeCallback) {
/**
* @private {!ErrorBanner}
* @const
*/
this.errorBanner_ = errorBanner;
/**
* @private {!GalleryDataModel}
* @const
*/
this.dataModel_ = dataModel;
/**
* @private {function()}
* @const
*/
this.changeToSlideModeCallback_ = changeToSlideModeCallback;
this.dataModel_.addEventListener('splice', this.onSplice_.bind(this));
this.thumbnailView_ = new ThumbnailView(container, dataModel, selectionModel);
this.thumbnailView_.addEventListener(
'thumbnail-double-click', this.onThumbnailDoubleClick_.bind(this));
}
/**
* Mode must extend cr.EventTarget.
*/
ThumbnailMode.prototype.__proto__ = cr.EventTarget.prototype;
/**
* Returns name of this mode.
* @return {string} Mode name.
*/
ThumbnailMode.prototype.getName = function() { return 'thumbnail'; };
/**
* Returns title of this mode.
* @return {string} Mode title.
*/
ThumbnailMode.prototype.getTitle = function() { return 'GALLERY_THUMBNAIL'; };
/**
* Returns current sub mode.
* @return {Gallery.SubMode}
*/
ThumbnailMode.prototype.getSubMode = function() {
return Gallery.SubMode.BROWSE;
};
/**
* Executes an action. An action is executed immediately since this mode does
* not have busy state.
*/
ThumbnailMode.prototype.executeWhenReady = function(action) { action(); };
/**
* @return {boolean} Always true. Toolbar is always visible.
*/
ThumbnailMode.prototype.hasActiveTool = function() { return true; };
/**
* Handles key down event.
* @param {!Event} event An event.
* @return {boolean} True when an event is handled.
*/
ThumbnailMode.prototype.onKeyDown = function(event) {
switch (event.key) {
case 'Enter':
if (event.target.matches('li.thumbnail')) {
this.changeToSlideModeCallback_();
return true;
}
break;
}
return false;
};
/**
* Handles splice event of data model.
*/
ThumbnailMode.prototype.onSplice_ = function() {
if (this.dataModel_.length === 0)
this.errorBanner_.show('GALLERY_NO_IMAGES');
else
this.errorBanner_.clear();
};
/**
* Handles thumbnail double click event of Thumbnail View.
* @param {!Event} event An event.
* @private
*/
ThumbnailMode.prototype.onThumbnailDoubleClick_ = function(event) {
this.changeToSlideModeCallback_();
};
/**
* Shows thumbnail view.
*/
ThumbnailMode.prototype.show = function() {
this.thumbnailView_.show();
};
/**
* Hides thumbnail view.
*/
ThumbnailMode.prototype.hide = function() {
this.thumbnailView_.hide();
};
/**
* Performs thumbnail mode enter animation.
* @param {number} index Selected thumbnail index.
* @param {!ImageRect} rect A rect from which the transformation starts.
*/
ThumbnailMode.prototype.performEnterAnimation = function(index, rect) {
this.thumbnailView_.performEnterAnimation(index, rect);
};
/**
* Focus to thumbnail mode.
*/
ThumbnailMode.prototype.focus = function() {
this.thumbnailView_.focus();
};
/**
* Returns thumbnail rect of the index.
* @param {number} index An index of thumbnail.
* @return {!ClientRect} A rect of thumbnail.
*/
ThumbnailMode.prototype.getThumbnailRect = function(index) {
return this.thumbnailView_.getThumbnailRect(index);
};
/**
* Thumbnail view.
* @param {!HTMLElement} container A container.
* @param {!GalleryDataModel} dataModel Gallery data model.
* @param {!cr.ui.ListSelectionModel} selectionModel List selection model.
* @constructor
* @extends {cr.EventTarget}
* @struct
*
* TODO(yawano): Optimization. Remove DOMs outside of viewport, reuse them.
* TODO(yawano): Extract ThumbnailView as a polymer element.
*/
function ThumbnailView(container, dataModel, selectionModel) {
cr.EventTarget.call(this);
/**
* @private {!HTMLElement}
*/
this.container_ = container;
/**
* @private {!GalleryDataModel}
*/
this.dataModel_ = dataModel;
/**
* @private {!cr.ui.ListSelectionModel}
*/
this.selectionModel_ = selectionModel;
/**
* @private {!Object}
*/
this.thumbnails_ = {};
/**
* @private {boolean}
*/
this.scrolling_ = false;
/**
* @private {number}
*/
this.initialScreenY_ = 0;
/**
* @private {number}
*/
this.initialScrollTop_ = 0;
/**
* @private {number}
*/
this.scrollbarTimeoutId_ = 0;
/**
* @private {!HTMLElement}
*/
this.list_ = assertInstanceof(document.createElement('ul'), HTMLElement);
this.container_.appendChild(this.list_);
/**
* @private {!HTMLElement}
*/
this.scrollbar_ = assertInstanceof(
document.createElement('div'), HTMLElement);
this.scrollbar_.classList.add('scrollbar');
/**
* @private {!HTMLElement}
*/
this.scrollbarThumb_ = assertInstanceof(
document.createElement('div'), HTMLElement);
this.scrollbarThumb_.classList.add('thumb');
this.scrollbar_.appendChild(this.scrollbarThumb_);
this.container_.appendChild(this.scrollbar_);
/**
* @private {!HTMLElement}
*/
this.animationThumbnail_ = assertInstanceof(
document.createElement('div'), HTMLElement);
this.animationThumbnail_.classList.add('animation-thumbnail');
this.container_.appendChild(this.animationThumbnail_);
this.container_.addEventListener('scroll', this.onScroll_.bind(this));
this.container_.addEventListener('click', this.onClick_.bind(this));
this.container_.addEventListener('dblclick', this.onDblClick_.bind(this));
// Set tabIndex to -1 as the container can capture keydown events.
this.container_.tabIndex = -1;
this.container_.addEventListener('keydown', this.onKeydown_.bind(this));
this.scrollbarThumb_.addEventListener(
'mousedown', this.onScrollbarThumbMouseDown_.bind(this));
window.addEventListener('mousemove', this.onWindowMouseMove_.bind(this));
window.addEventListener('mouseup', this.onWindowMouseUp_.bind(this));
this.dataModel_.addEventListener('splice', this.onSplice_.bind(this));
this.dataModel_.addEventListener('content', this.onContent_.bind(this));
this.selectionModel_.addEventListener(
'change', this.onSelectionChange_.bind(this));
}
ThumbnailView.prototype.__proto__ = cr.EventTarget.prototype;
/**
* Row height.
* @const {number}
*
* TODO(yawano): Change so that Gallery adjust row height depending on image
* collection and window size to cover viewport as much as possible.
*/
ThumbnailView.ROW_HEIGHT = 160; // px
/**
* Margins between thumbnails. This should be synced with CSS.
* @const {number}
*/
ThumbnailView.MARGIN = 4; // px
/**
* Timeout to fade out scrollbar.
* @const {number}
*/
ThumbnailView.SCROLLBAR_TIMEOUT = 1500; // ms
/**
* Selection mode.
* @enum {string}
*/
ThumbnailView.SelectionMode = {
SINGLE: 'single',
MULTIPLE: 'multiple',
RANGE: 'range'
};
/**
* Shows thumbnail view.
*/
ThumbnailView.prototype.show = function() {
this.container_.hidden = false;
};
/**
* Hides thumbnail view.
*/
ThumbnailView.prototype.hide = function() {
this.container_.hidden = true;
};
/**
* Handles scroll bar thumb mouse down event.
* @param {!Event} event An event.
* @private
*/
ThumbnailView.prototype.onScrollbarThumbMouseDown_ = function(event) {
this.scrolling_ = true;
this.initialScreenY_ = event.screenY;
this.initialScrollTop_ = this.container_.scrollTop;
};
/**
* Handles mouse move event of window.
* @param {!Event} event An event.
* @private
*/
ThumbnailView.prototype.onWindowMouseMove_ = function(event) {
if (this.scrolling_) {
var diff = event.screenY - this.initialScreenY_;
var scrollTop = this.initialScrollTop_ +
(diff * this.container_.scrollHeight / this.scrollbar_.clientHeight);
this.container_.scrollTop = scrollTop;
}
this.resetTimerOfScrollbar_();
};
/**
* Handles mouse up event of window.
* @param {!Event} event An event.
* @private
*/
ThumbnailView.prototype.onWindowMouseUp_ = function(event) {
this.scrolling_ = false;
this.resetTimerOfScrollbar_();
};
/**
* Handles scroll of viewport.
* @param {!Event} event An event.
* @private
*/
ThumbnailView.prototype.onScroll_ = function(event) {
this.updateScrollBar_();
};
/**
* Moves selection to specified direction.
* @param {string} direction Direction. Should be 'ArrowLeft', 'ArrowRight',
* 'ArrowUp', or 'ArrowDown'.
* @param {boolean} selectRange True to perform range selection.
* @private
*/
ThumbnailView.prototype.moveSelection_ = function(direction, selectRange) {
var step;
if ((direction === 'ArrowLeft' && !isRTL()) ||
(direction === 'ArrowRight' && isRTL()) ||
(direction === 'ArrowUp')) {
step = -1;
} else if ((direction === 'ArrowRight' && !isRTL()) ||
(direction === 'ArrowLeft' && isRTL()) ||
(direction === 'ArrowDown')) {
step = 1;
} else {
assertNotReached();
}
var vertical = direction === 'ArrowUp' || direction === 'ArrowDown';
var baseIndex = this.selectionModel_.leadIndex !== -1 ?
this.selectionModel_.leadIndex :
this.selectionModel_.selectedIndex;
var baseRect = this.getThumbnailRect(baseIndex);
var baseCenter = baseRect.left + baseRect.width / 2;
var minHorizontalGap = Number.MAX_VALUE;
var index = null;
for (var i = baseIndex + step;
0 <= i && i < this.dataModel_.length;
i += step) {
// Skip error thumbnail.
var thumbnail = this.getThumbnailAt_(i);
if (thumbnail.isError())
continue;
// Look for the horizontally nearest item if it is vertical move. Otherwise
// it just use the current i.
if (vertical) {
var rect = this.getThumbnailRect(i);
var verticalGap = Math.abs(baseRect.top - rect.top);
if (verticalGap === 0)
continue;
else if (verticalGap >= ThumbnailView.ROW_HEIGHT * 2)
break;
// If centerGap - rect.width / 2 < 0, the image is located just
// above the center point of base image since baseCenter is in the range
// (rect.left, rect.right). In this case we use 0 as distance. Otherwise
// centerGap - rect.width / 2 equals to the distance between baseCenter
// and either of rect.left or rect.right that is closer to centerGap.
var centerGap = Math.abs(baseCenter - (rect.left + rect.width / 2));
var horizontalGap = Math.max(centerGap - rect.width / 2, 0);
if (horizontalGap < minHorizontalGap) {
minHorizontalGap = horizontalGap;
index = i;
}
} else {
index = i;
break;
}
}
if (index !== null) {
// Move selection.
if (selectRange && this.selectionModel_.anchorIndex !== -1) {
// Since anchorIndex will be set to 0 by unselectAll, copy the value.
var anchorIndex = this.selectionModel_.anchorIndex;
this.selectionModel_.unselectAll();
this.selectionModel_.selectRange(anchorIndex, index);
this.selectionModel_.anchorIndex = anchorIndex;
} else {
this.selectionModel_.selectedIndex = index;
this.selectionModel_.anchorIndex = index;
}
this.selectionModel_.leadIndex = index;
this.scrollTo_(index);
}
};
/**
* Scrolls viewport to show the thumbnail of the index.
* @param {number} index Index of a thumbnail which becomes to appear in the
* viewport.
* @private
*
* TODO(yawano): Add scroll animation.
*/
ThumbnailView.prototype.scrollTo_ = function(index) {
var thumbnailRect = this.getThumbnailRect(index);
if (thumbnailRect.top - ThumbnailView.MARGIN < ImageEditor.Toolbar.HEIGHT) {
this.container_.scrollTop -=
ImageEditor.Toolbar.HEIGHT - thumbnailRect.top + ThumbnailView.MARGIN;
} else if (thumbnailRect.bottom + ThumbnailView.MARGIN >
this.container_.clientHeight) {
this.container_.scrollTop += thumbnailRect.bottom + ThumbnailView.MARGIN -
this.container_.clientHeight;
}
};
/**
* Updates scroll bar.
* @private
*/
ThumbnailView.prototype.updateScrollBar_ = function() {
var scrollTop = this.container_.scrollTop;
var scrollHeight = this.container_.scrollHeight;
var clientHeight = this.container_.clientHeight;
// If viewport is not long enough to scroll, do not show scrollbar.
if (scrollHeight <= clientHeight) {
this.scrollbar_.hidden = true;
return;
}
this.scrollbar_.hidden = false;
var thumbHeight =
~~(this.scrollbar_.clientHeight * clientHeight / scrollHeight);
var thumbTop = ~~(scrollTop * this.scrollbar_.clientHeight / scrollHeight);
this.scrollbarThumb_.style.height = thumbHeight + 'px';
this.scrollbarThumb_.style.marginTop = thumbTop + 'px';
this.resetTimerOfScrollbar_();
};
/**
* Resets timer to fade out scrollbar. If scrollbar is already faded-out, this
* method makes it visible and set timeout. If user is scrolling, this method
* just clears existing timer.
* @private
*/
ThumbnailView.prototype.resetTimerOfScrollbar_ = function() {
this.scrollbar_.classList.toggle('transparent', false);
if (this.scrollbarTimeoutId_) {
clearTimeout(this.scrollbarTimeoutId_);
this.scrollbarTimeoutId_ = 0;
}
// If user is scrolling, do not set timeout.
if (this.scrolling_)
return;
this.scrollbarTimeoutId_ = setTimeout(function() {
this.scrollbarTimeoutId_ = 0;
this.scrollbar_.classList.toggle('transparent', true);
}.bind(this), ThumbnailView.SCROLLBAR_TIMEOUT);
};
/**
* Handles splice event of data model.
* @param {!Event} event An event.
* @private
*/
ThumbnailView.prototype.onSplice_ = function(event) {
if (event.removed) {
for (var i = 0; i < event.removed.length; i++) {
this.remove_(event.removed[i]);
}
}
if (event.added && event.added.length > 0) {
// Get a thumbnail before which new thumbnail is inserted.
var insertBefore = null;
var galleryItem = this.dataModel_.item(event.index + event.added.length);
if (galleryItem)
insertBefore = this.thumbnails_[galleryItem.getEntry().toURL()];
for (var i = 0; i < event.added.length; i++) {
this.insert_(event.added[i], insertBefore);
}
}
};
/**
* Handles content event of data model.
* @param {!Event} event An event.
* @private
*/
ThumbnailView.prototype.onContent_ = function(event) {
var galleryItem = event.item;
var oldEntry = event.oldEntry;
var thumbnail = this.thumbnails_[oldEntry.toURL()];
if (thumbnail) {
// Update map.
delete this.thumbnails_[oldEntry.toURL()];
this.thumbnails_[galleryItem.getEntry().toURL()] = thumbnail;
thumbnail.update();
}
};
/**
* Handles selection change event.
* @param {!Event} event An event.
* @private
*/
ThumbnailView.prototype.onSelectionChange_ = function(event) {
var changes = event.changes;
var lastSelectedThumbnail = null;
for (var i = 0; i < changes.length; i++) {
var change = changes[i];
var galleryItem = this.dataModel_.item(change.index);
if (!galleryItem)
continue;
var thumbnail = this.thumbnails_[galleryItem.getEntry().toURL()];
if (!thumbnail)
continue;
thumbnail.setSelected(change.selected);
// We should not focus to error thumbnail.
if (change.selected && !thumbnail.isError())
lastSelectedThumbnail = thumbnail;
}
// If new item is selected, focus to it. If multiple thumbnails are selected,
// focus to the last one.
if (lastSelectedThumbnail)
lastSelectedThumbnail.getContainer().focus();
};
/**
* Handles click event.
* @param {!Event} event An event.
* @private
*/
ThumbnailView.prototype.onClick_ = function(event) {
var target = event.target;
if (target.matches('.selection.frame')) {
var selectionMode = ThumbnailView.SelectionMode.SINGLE;
if (event.ctrlKey)
selectionMode = ThumbnailView.SelectionMode.MULTIPLE;
if (event.shiftKey)
selectionMode = ThumbnailView.SelectionMode.RANGE;
this.selectByThumbnail_(target.parentNode.getThumbnail(), selectionMode);
return;
}
// If empty space is clicked, unselect current selection.
this.selectionModel_.unselectAll();
};
/**
* Handles double click event.
* @param {!Event} event An event.
* @private
*/
ThumbnailView.prototype.onDblClick_ = function(event) {
var target = event.target;
if (target.matches('.selection.frame')) {
this.selectByThumbnail_(target.parentNode.getThumbnail(),
ThumbnailView.SelectionMode.SINGLE);
var thumbnailDoubleClickEvent = new Event('thumbnail-double-click');
this.dispatchEvent(thumbnailDoubleClickEvent);
}
};
/**
* Handles keydown event.
* @param {!Event} event
* @private
*/
ThumbnailView.prototype.onKeydown_ = function(event) {
var keyString = util.getKeyModifiers(event) + event.key;
switch (keyString) {
case 'ArrowRight':
case 'ArrowLeft':
case 'ArrowUp':
case 'ArrowDown':
case 'Shift-ArrowRight':
case 'Shift-ArrowLeft':
case 'Shift-ArrowUp':
case 'Shift-ArrowDown':
this.moveSelection_(event.key, event.shiftKey);
event.stopPropagation();
break;
case 'Ctrl-a': // Crtl+A
this.selectionModel_.selectAll();
event.stopPropagation();
break;
}
};
/**
* Selects a thumbnail.
* @param {!ThumbnailView.Thumbnail} thumbnail Thumbnail to be selected.
* @param {ThumbnailView.SelectionMode} selectionMode
* @private
*/
ThumbnailView.prototype.selectByThumbnail_ = function(
thumbnail, selectionMode) {
var index = this.dataModel_.indexOf(thumbnail.getGalleryItem());
if (selectionMode === ThumbnailView.SelectionMode.SINGLE) {
this.selectionModel_.unselectAll();
this.selectionModel_.setIndexSelected(index, true);
this.selectionModel_.anchorIndex = index;
} else if (selectionMode === ThumbnailView.SelectionMode.MULTIPLE) {
this.selectionModel_.setIndexSelected(index,
this.selectionModel_.selectedIndexes.indexOf(index) === -1);
} else if (selectionMode === ThumbnailView.SelectionMode.RANGE) {
var leadIndex = this.selectionModel_.leadIndex;
this.selectionModel_.unselectAll();
this.selectionModel_.selectRange(leadIndex, index);
} else {
assertNotReached();
}
this.selectionModel_.leadIndex = index;
};
/**
* Inserts an item.
* @param {!GalleryItem} galleryItem A gallery item.
* @param {!ThumbnailView.Thumbnail} insertBefore A thumbnail before which new
* thumbnail is inserted. Set null for adding at the end of the list.
* @private
*/
ThumbnailView.prototype.insert_ = function(galleryItem, insertBefore) {
var thumbnail = new ThumbnailView.Thumbnail(galleryItem);
this.thumbnails_[galleryItem.getEntry().toURL()] = thumbnail;
if (insertBefore) {
this.list_.insertBefore(
thumbnail.getContainer(), insertBefore.getContainer());
} else {
this.list_.appendChild(thumbnail.getContainer());
}
// Set selection state.
var index = this.dataModel_.indexOf(galleryItem);
thumbnail.setSelected(this.selectionModel_.getIndexSelected(index));
this.updateScrollBar_();
};
/**
* Removes an item.
* @param {!GalleryItem} galleryItem A gallery item.
* @private
*/
ThumbnailView.prototype.remove_ = function(galleryItem) {
var thumbnail = this.thumbnails_[galleryItem.getEntry().toURL()];
this.list_.removeChild(thumbnail.getContainer());
delete this.thumbnails_[galleryItem.getEntry().toURL()];
};
/**
* Returns thumbnail instance at specified index.
* @param {number} index Index of the thumbnail.
* @return {!ThumbnailView.Thumbnail} Thumbnail at the index.
* @private
*/
ThumbnailView.prototype.getThumbnailAt_ = function(index) {
var galleryItem = this.dataModel_.item(index);
return this.thumbnails_[galleryItem.getEntry().toURL()];
};
/**
* Returns a rect of the specified thumbnail.
* @param {number} index An index of the thumbnail.
* @return {!ClientRect} Rect of the thumbnail.
*/
ThumbnailView.prototype.getThumbnailRect = function(index) {
var thumbnail = this.getThumbnailAt_(index);
return thumbnail.getContainer().getBoundingClientRect();
};
/**
* Performs enter animation.
* @param {number} index Index of the thumbnail which is animated.
* @param {!ImageRect} rect A rect from which the transformation starts.
*
* TODO(yawano): Consider to move this logic to thumbnail mode.
*/
ThumbnailView.prototype.performEnterAnimation = function(index, rect) {
this.scrollTo_(index);
this.updateScrollBar_();
var thumbnailRect = this.getThumbnailRect(index);
var thumbnail = this.getThumbnailAt_(index);
// If thumbnail is not loaded yet or failed to load, do not perform animation.
if (!thumbnail.getBackgroundImage() || thumbnail.isError())
return;
// Hide animating thumbnail.
thumbnail.setTransparent(true);
this.animationThumbnail_.style.backgroundImage =
thumbnail.getBackgroundImage();
this.animationThumbnail_.classList.add('animating');
this.animationThumbnail_.width = thumbnail.getWidth();
this.animationThumbnail_.height = ThumbnailView.ROW_HEIGHT;
var animationPlayer = this.animationThumbnail_.animate([{
height: rect.height + 'px',
left: rect.left + 'px',
top: rect.top + 'px',
width: rect.width + 'px',
offset: 0,
easing: 'linear'
}, {
height: thumbnailRect.height + 'px',
left: thumbnailRect.left + 'px',
top: thumbnailRect.top + 'px',
width: thumbnailRect.width + 'px',
offset: 1
}], 250);
animationPlayer.addEventListener('finish', function() {
this.animationThumbnail_.classList.remove('animating');
thumbnail.setTransparent(false);
}.bind(this));
};
/**
* Focus to thumbnail view. If an item is selected, focus to it.
*/
ThumbnailView.prototype.focus = function() {
if (this.selectionModel_.selectedIndexes.length === 0) {
this.container_.focus();
return;
}
var index = this.selectionModel_.leadIndex !== -1 ?
this.selectionModel_.leadIndex : this.selectionModel_.selectedIndex;
var thumbnail = this.getThumbnailAt_(index);
thumbnail.getContainer().focus();
};
/**
* Thumbnail.
* @param {!GalleryItem} galleryItem A gallery item.
* @constructor
* @struct
*/
ThumbnailView.Thumbnail = function(galleryItem) {
/**
* @private {!GalleryItem}
*/
this.galleryItem_ = galleryItem;
/**
* @private {boolean}
*/
this.selected_ = false;
/**
* @private {ThumbnailLoader}
*/
this.thumbnailLoader_ = null;
/**
* @private {number}
*/
this.thumbnailLoadRequestId_ = 0;
/**
* @private {number}
*/
this.width_ = 0;
/**
* @private {*}
*/
this.error_ = null;
/**
* @private {!HTMLElement}
*/
this.container_ = assertInstanceof(document.createElement('li'), HTMLElement);
this.container_.tabIndex = 1;
this.container_.classList.add('thumbnail');
/**
* @private {!HTMLElement}
*/
this.imageFrame_ = assertInstanceof(
document.createElement('div'), HTMLElement);
this.imageFrame_.classList.add('image', 'frame');
this.container_.appendChild(this.imageFrame_);
/**
* @private {!HTMLElement}
*/
this.selectionFrame_ = assertInstanceof(
document.createElement('div'), HTMLElement);
this.selectionFrame_.classList.add('selection', 'frame');
this.container_.appendChild(this.selectionFrame_);
this.container_.style.height = ThumbnailView.ROW_HEIGHT + 'px';
this.container_.getThumbnail =
function(thumbnail) { return thumbnail; }.bind(null, this);
this.update();
};
/**
* Returns a gallery item.
* @return {!GalleryItem} A gallery item.
*/
ThumbnailView.Thumbnail.prototype.getGalleryItem = function() {
return this.galleryItem_;
};
/**
* Change selection state of this thumbnail.
* @param {boolean} selected True to make this thumbnail selected.
*/
ThumbnailView.Thumbnail.prototype.setSelected = function(selected) {
this.selected_ = selected;
this.container_.classList.toggle('selected', selected);
};
/**
* Returns a container.
* @return {!HTMLElement} A container.
*/
ThumbnailView.Thumbnail.prototype.getContainer = function() {
return this.container_;
};
/**
* Sets this thumbnail as transparent.
* @param {boolean} transparent True to make this thumbnail transparent.
*/
ThumbnailView.Thumbnail.prototype.setTransparent = function(transparent) {
this.container_.classList.toggle('transparent', transparent);
};
/**
* Returns width of this thumbnail.
* @return {number} Width of this thumbnail.
*/
ThumbnailView.Thumbnail.prototype.getWidth = function() {
return this.width_;
};
/**
* Returns whether this has failed to load thumbnail or not.
* @return {boolean} True if thumbnail load has failed.
*/
ThumbnailView.Thumbnail.prototype.isError = function() {
return !!this.error_;
};
/**
* Sets error.
* @param {*} error Error object. Set null to clear error.
* @private
*/
ThumbnailView.Thumbnail.prototype.setError_ = function(error) {
this.error_ = error;
this.container_.classList.toggle('error', !!this.error_);
};
/**
* Sets width of this thumbnail.
* @param {number} width Width.
* @private
*/
ThumbnailView.Thumbnail.prototype.setWidth_ = function(width) {
if (this.width_ === width)
return;
this.width_ = width;
this.container_.style.width = this.width_ + 'px';
};
/**
* Returns background image style of this thumbnail.
* @return {string} Background image.
*/
ThumbnailView.Thumbnail.prototype.getBackgroundImage = function() {
return this.imageFrame_.style.backgroundImage;
};
/**
* Updates thumbnail.
*/
ThumbnailView.Thumbnail.prototype.update = function() {
// Update title.
this.container_.setAttribute('title', this.galleryItem_.getFileName());
// Calculate and set width.
var metadata = this.galleryItem_.getMetadataItem();
if (!metadata) {
this.setWidth_(ThumbnailView.ROW_HEIGHT);
return;
}
var rotated = metadata.imageRotation % 2 === 1;
var imageWidth = rotated ? metadata.imageHeight : metadata.imageWidth;
var imageHeight = rotated ? metadata.imageWidth : metadata.imageHeight;
this.setWidth_(~~(imageWidth * ThumbnailView.ROW_HEIGHT / imageHeight));
// Set thumbnail.
var thumbnailMetadata = this.galleryItem_.getThumbnailMetadataItem();
if (!thumbnailMetadata)
return;
this.loadAndSetThumbnail_(thumbnailMetadata,
false /* do not force to generate thumbnail */).then(function(result) {
if (!result ||
result.height >= ThumbnailView.ROW_HEIGHT ||
result.loadTarget === ThumbnailLoader.LoadTarget.FILE_ENTRY ||
metadata.imageHeight <= ThumbnailView.ROW_HEIGHT ||
(thumbnailMetadata.external && !thumbnailMetadata.external.present)) {
return;
}
// If thumbnail height is lower than ThumbnailView.ROW_HEIGHT, generate
// thumbnail from image content.
this.loadAndSetThumbnail_(
thumbnailMetadata, true /* force to generate thumbnail */);
}.bind(this));
};
/**
* Loads thumbnail and sets it.
* @param {!ThumbnailMetadataItem} thumbnailMetadata
* @param {boolean} forceToGenerate True to force generating thumbnail from
* image content.
* @return {!Promise<?{height:number, loadTarget:?ThumbnailLoader.LoadTarget}>}
* null is returned for error case.
* @private
*/
ThumbnailView.Thumbnail.prototype.loadAndSetThumbnail_ = function(
thumbnailMetadata, forceToGenerate) {
this.thumbnailLoadRequestId_++;
var loadTargets = forceToGenerate ?
[ThumbnailLoader.LoadTarget.FILE_ENTRY] :
undefined /* default value */;
this.thumbnailLoader_ = new ThumbnailLoader(this.galleryItem_.getEntry(),
undefined /* opt_loaderType */, thumbnailMetadata,
undefined /* opt_mediaType */, loadTargets);
return this.thumbnailLoader_.loadAsDataUrl(
ThumbnailLoader.FillMode.FIT).then(function(requestId, result) {
// Discard the result of old request.
if (requestId !== this.thumbnailLoadRequestId_)
return null;
// Update width by using the width of actual data.
this.setWidth_(
~~(result.width * ThumbnailView.ROW_HEIGHT / result.height));
this.imageFrame_.style.backgroundImage = 'url(' + result.data + ')';
this.setError_(null);
return {
height: result.height,
loadTarget: this.thumbnailLoader_.getLoadTarget()
};
}.bind(this, this.thumbnailLoadRequestId_))
.catch(function(requestId, error) {
if (requestId !== this.thumbnailLoadRequestId_)
return null;
this.setError_(error);
return null;
}.bind(this, this.thumbnailLoadRequestId_));
};