| // 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. |
| |
| cr.define('options', function() { |
| /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel; |
| /** @const */ var Grid = cr.ui.Grid; |
| /** @const */ var GridItem = cr.ui.GridItem; |
| /** @const */ var GridSelectionController = cr.ui.GridSelectionController; |
| /** @const */ var ListSingleSelectionModel = cr.ui.ListSingleSelectionModel; |
| |
| /** |
| * Interval between consecutive camera presence checks in msec. |
| * @const |
| */ |
| var CAMERA_CHECK_INTERVAL_MS = 3000; |
| |
| /** |
| * Interval between consecutive camera liveness checks in msec. |
| * @const |
| */ |
| var CAMERA_LIVENESS_CHECK_MS = 3000; |
| |
| /** |
| * Number of frames recorded by takeVideo(). |
| * @const |
| */ |
| var RECORD_FRAMES = 48; |
| |
| /** |
| * FPS at which camera stream is recorded. |
| * @const |
| */ |
| var RECORD_FPS = 16; |
| |
| /** |
| * Dimensions for camera capture. |
| * @const |
| */ |
| var CAPTURE_SIZE = { |
| height: 480, |
| width: 480 |
| }; |
| |
| /** |
| * Path for internal URLs. |
| * @const |
| */ |
| var CHROME_THEME_PATH = 'chrome://theme'; |
| |
| /** |
| * Creates a new user images grid item. |
| * @param {{url: string, title: string=, decorateFn: function=, |
| * clickHandler: function=}} imageInfo User image URL, optional title, |
| * decorator callback and click handler. |
| * @constructor |
| * @extends {cr.ui.GridItem} |
| */ |
| function UserImagesGridItem(imageInfo) { |
| var el = new GridItem(imageInfo); |
| el.__proto__ = UserImagesGridItem.prototype; |
| return el; |
| } |
| |
| UserImagesGridItem.prototype = { |
| __proto__: GridItem.prototype, |
| |
| /** @override */ |
| decorate: function() { |
| GridItem.prototype.decorate.call(this); |
| var imageEl = cr.doc.createElement('img'); |
| // Force 1x scale for chrome://theme URLs. Grid elements are much smaller |
| // than actual images so there is no need in full scale on HDPI. |
| var url = this.dataItem.url; |
| if (url.slice(0, CHROME_THEME_PATH.length) == CHROME_THEME_PATH) |
| imageEl.src = this.dataItem.url + '@1x'; |
| else |
| imageEl.src = this.dataItem.url; |
| imageEl.title = this.dataItem.title || ''; |
| if (typeof this.dataItem.clickHandler == 'function') |
| imageEl.addEventListener('mousedown', this.dataItem.clickHandler); |
| // Remove any garbage added by GridItem and ListItem decorators. |
| this.textContent = ''; |
| this.appendChild(imageEl); |
| if (typeof this.dataItem.decorateFn == 'function') |
| this.dataItem.decorateFn(this); |
| this.setAttribute('role', 'option'); |
| this.oncontextmenu = function(e) { e.preventDefault(); }; |
| } |
| }; |
| |
| /** |
| * Creates a selection controller that wraps selection on grid ends |
| * and translates Enter presses into 'activate' events. |
| * @param {cr.ui.ListSelectionModel} selectionModel The selection model to |
| * interact with. |
| * @param {cr.ui.Grid} grid The grid to interact with. |
| * @constructor |
| * @extends {cr.ui.GridSelectionController} |
| */ |
| function UserImagesGridSelectionController(selectionModel, grid) { |
| GridSelectionController.call(this, selectionModel, grid); |
| } |
| |
| UserImagesGridSelectionController.prototype = { |
| __proto__: GridSelectionController.prototype, |
| |
| /** @override */ |
| getIndexBefore: function(index) { |
| var result = |
| GridSelectionController.prototype.getIndexBefore.call(this, index); |
| return result == -1 ? this.getLastIndex() : result; |
| }, |
| |
| /** @override */ |
| getIndexAfter: function(index) { |
| var result = |
| GridSelectionController.prototype.getIndexAfter.call(this, index); |
| return result == -1 ? this.getFirstIndex() : result; |
| }, |
| |
| /** @override */ |
| handleKeyDown: function(e) { |
| if (e.keyIdentifier == 'Enter') |
| cr.dispatchSimpleEvent(this.grid_, 'activate'); |
| else |
| GridSelectionController.prototype.handleKeyDown.call(this, e); |
| } |
| }; |
| |
| /** |
| * Creates a new user images grid element. |
| * @param {Object=} opt_propertyBag Optional properties. |
| * @constructor |
| * @extends {cr.ui.Grid} |
| */ |
| var UserImagesGrid = cr.ui.define('grid'); |
| |
| UserImagesGrid.prototype = { |
| __proto__: Grid.prototype, |
| |
| /** @override */ |
| createSelectionController: function(sm) { |
| return new UserImagesGridSelectionController(sm, this); |
| }, |
| |
| /** @override */ |
| decorate: function() { |
| Grid.prototype.decorate.call(this); |
| this.dataModel = new ArrayDataModel([]); |
| this.itemConstructor = UserImagesGridItem; |
| this.selectionModel = new ListSingleSelectionModel(); |
| this.inProgramSelection_ = false; |
| this.addEventListener('dblclick', this.handleDblClick_.bind(this)); |
| this.addEventListener('change', this.handleChange_.bind(this)); |
| this.setAttribute('role', 'listbox'); |
| this.autoExpands = true; |
| }, |
| |
| /** |
| * Handles double click on the image grid. |
| * @param {Event} e Double click Event. |
| * @private |
| */ |
| handleDblClick_: function(e) { |
| // If a child element is double-clicked and not the grid itself, handle |
| // this as 'Enter' keypress. |
| if (e.target != this) |
| cr.dispatchSimpleEvent(this, 'activate'); |
| }, |
| |
| /** |
| * Handles selection change. |
| * @param {Event} e Double click Event. |
| * @private |
| */ |
| handleChange_: function(e) { |
| if (this.selectedItem === null) |
| return; |
| |
| var oldSelectionType = this.selectionType; |
| |
| // Update current selection type. |
| this.selectionType = this.selectedItem.type; |
| |
| // Show grey silhouette with the same border as stock images. |
| if (/^chrome:\/\/theme\//.test(this.selectedItemUrl)) |
| this.previewElement.classList.add('default-image'); |
| |
| this.updatePreview_(); |
| |
| var e = new cr.Event('select', false, false); |
| e.oldSelectionType = oldSelectionType; |
| this.dispatchEvent(e); |
| }, |
| |
| /** |
| * Updates the preview image, if present. |
| * @private |
| */ |
| updatePreview_: function() { |
| var url = this.selectedItemUrl; |
| if (url && this.previewImage_) { |
| if (url.slice(0, CHROME_THEME_PATH.length) == CHROME_THEME_PATH) |
| this.previewImage_.src = url + '@' + window.devicePixelRatio + 'x'; |
| else |
| this.previewImage_.src = url; |
| } |
| }, |
| |
| /** |
| * Start camera presence check. |
| * @private |
| */ |
| checkCameraPresence_: function() { |
| if (this.cameraPresentCheckTimer_) { |
| window.clearTimeout(this.cameraPresentCheckTimer_); |
| this.cameraPresentCheckTimer_ = null; |
| } |
| if (!this.cameraVideo_) |
| return; |
| chrome.send('checkCameraPresence'); |
| }, |
| |
| /** |
| * Whether a camera is present or not. |
| * @type {boolean} |
| */ |
| get cameraPresent() { |
| return this.cameraPresent_; |
| }, |
| set cameraPresent(value) { |
| this.cameraPresent_ = value; |
| if (this.cameraLive) |
| this.cameraImage = null; |
| // Repeat the check after some time. |
| this.cameraPresentCheckTimer_ = window.setTimeout( |
| this.checkCameraPresence_.bind(this), |
| CAMERA_CHECK_INTERVAL_MS); |
| }, |
| |
| /** |
| * Whether camera is actually streaming video. May be |false| even when |
| * camera is present and shown but still initializing. |
| * @type {boolean} |
| */ |
| get cameraOnline() { |
| return this.previewElement.classList.contains('online'); |
| }, |
| set cameraOnline(value) { |
| this.previewElement.classList[value ? 'add' : 'remove']('online'); |
| if (value) { |
| this.cameraLiveCheckTimer_ = window.setInterval( |
| this.checkCameraLive_.bind(this), CAMERA_LIVENESS_CHECK_MS); |
| } else if (this.cameraLiveCheckTimer_) { |
| window.clearInterval(this.cameraLiveCheckTimer_); |
| this.cameraLiveCheckTimer_ = null; |
| } |
| }, |
| |
| /** |
| * Tries to starts camera stream capture. |
| * @param {function(): boolean} onAvailable Callback that is called if |
| * camera is available. If it returns |true|, capture is started |
| * immediately. |
| */ |
| startCamera: function(onAvailable, onAbsent) { |
| this.stopCamera(); |
| this.cameraStartInProgress_ = true; |
| navigator.webkitGetUserMedia( |
| {video: true}, |
| this.handleCameraAvailable_.bind(this, onAvailable), |
| this.handleCameraAbsent_.bind(this)); |
| }, |
| |
| /** |
| * Stops camera capture, if it's currently active. |
| */ |
| stopCamera: function() { |
| this.cameraOnline = false; |
| if (this.cameraVideo_) |
| this.cameraVideo_.src = ''; |
| if (this.cameraStream_) |
| this.cameraStream_.stop(); |
| // Cancel any pending getUserMedia() checks. |
| this.cameraStartInProgress_ = false; |
| }, |
| |
| /** |
| * Handles successful camera check. |
| * @param {function(): boolean} onAvailable Callback to call. If it returns |
| * |true|, capture is started immediately. |
| * @param {MediaStream} stream Stream object as returned by getUserMedia. |
| * @private |
| */ |
| handleCameraAvailable_: function(onAvailable, stream) { |
| if (this.cameraStartInProgress_ && onAvailable()) { |
| this.cameraVideo_.src = window.webkitURL.createObjectURL(stream); |
| this.cameraStream_ = stream; |
| } else { |
| stream.stop(); |
| } |
| this.cameraStartInProgress_ = false; |
| }, |
| |
| /** |
| * Handles camera check failure. |
| * @param {NavigatorUserMediaError=} err Error object. |
| * @private |
| */ |
| handleCameraAbsent_: function(err) { |
| this.cameraPresent = false; |
| this.cameraOnline = false; |
| this.cameraStartInProgress_ = false; |
| }, |
| |
| /** |
| * Handles successful camera capture start. |
| * @private |
| */ |
| handleVideoStarted_: function() { |
| this.cameraOnline = true; |
| this.handleVideoUpdate_(); |
| }, |
| |
| /** |
| * Handles camera stream update. Called regularly (at rate no greater then |
| * 4/sec) while camera stream is live. |
| * @private |
| */ |
| handleVideoUpdate_: function() { |
| this.lastFrameTime_ = new Date().getTime(); |
| }, |
| |
| /** |
| * Checks if camera is still live by comparing the timestamp of the last |
| * 'timeupdate' event with the current time. |
| * @private |
| */ |
| checkCameraLive_: function() { |
| if (new Date().getTime() - this.lastFrameTime_ > |
| CAMERA_LIVENESS_CHECK_MS) { |
| this.cameraPresent = false; |
| } |
| }, |
| |
| /** |
| * Type of the selected image (one of 'default', 'profile', 'camera'). |
| * Setting it will update class list of |previewElement|. |
| * @type {string} |
| */ |
| get selectionType() { |
| return this.selectionType_; |
| }, |
| set selectionType(value) { |
| this.selectionType_ = value; |
| var previewClassList = this.previewElement.classList; |
| previewClassList[value == 'default' ? 'add' : 'remove']('default-image'); |
| previewClassList[value == 'profile' ? 'add' : 'remove']('profile-image'); |
| previewClassList[value == 'camera' ? 'add' : 'remove']('camera'); |
| }, |
| |
| /** |
| * Current image captured from camera as data URL. Setting to null will |
| * return to the live camera stream. |
| * @type {string=} |
| */ |
| get cameraImage() { |
| return this.cameraImage_; |
| }, |
| set cameraImage(imageUrl) { |
| this.cameraLive = !imageUrl; |
| if (this.cameraPresent && !imageUrl) |
| imageUrl = UserImagesGrid.ButtonImages.TAKE_PHOTO; |
| if (imageUrl) { |
| this.cameraImage_ = this.cameraImage_ ? |
| this.updateItem(this.cameraImage_, imageUrl, this.cameraTitle_) : |
| this.addItem(imageUrl, this.cameraTitle_, undefined, 0); |
| this.cameraImage_.type = 'camera'; |
| } else { |
| this.removeItem(this.cameraImage_); |
| this.cameraImage_ = null; |
| } |
| |
| // Set focus to the grid, unless focus is on the OK button. |
| if ((!document.activeElement || |
| document.activeElement.tagName != 'BUTTON') && imageUrl) { |
| this.focus(); |
| } |
| }, |
| |
| /** |
| * Updates the titles for the camera element. |
| * @param {string} placeholderTitle Title when showing a placeholder. |
| * @param {string} capturedImageTitle Title when showing a captured photo. |
| */ |
| setCameraTitles: function(placeholderTitle, capturedImageTitle) { |
| this.placeholderTitle_ = placeholderTitle; |
| this.capturedImageTitle_ = capturedImageTitle; |
| this.cameraTitle_ = this.placeholderTitle_; |
| }, |
| |
| /** |
| * True when camera is in live mode (i.e. no still photo selected). |
| * @type {boolean} |
| */ |
| get cameraLive() { |
| return this.cameraLive_; |
| }, |
| set cameraLive(value) { |
| this.cameraLive_ = value; |
| this.previewElement.classList[value ? 'add' : 'remove']('live'); |
| }, |
| |
| /** |
| * Should only be queried from the 'change' event listener, true if the |
| * change event was triggered by a programmatical selection change. |
| * @type {boolean} |
| */ |
| get inProgramSelection() { |
| return this.inProgramSelection_; |
| }, |
| |
| /** |
| * URL of the image selected. |
| * @type {string?} |
| */ |
| get selectedItemUrl() { |
| var selectedItem = this.selectedItem; |
| return selectedItem ? selectedItem.url : null; |
| }, |
| set selectedItemUrl(url) { |
| for (var i = 0, el; el = this.dataModel.item(i); i++) { |
| if (el.url === url) |
| this.selectedItemIndex = i; |
| } |
| }, |
| |
| /** |
| * Set index to the image selected. |
| * @type {number} index The index of selected image. |
| */ |
| set selectedItemIndex(index) { |
| this.inProgramSelection_ = true; |
| this.selectionModel.selectedIndex = index; |
| this.inProgramSelection_ = false; |
| }, |
| |
| /** @override */ |
| get selectedItem() { |
| var index = this.selectionModel.selectedIndex; |
| return index != -1 ? this.dataModel.item(index) : null; |
| }, |
| set selectedItem(selectedItem) { |
| var index = this.indexOf(selectedItem); |
| this.inProgramSelection_ = true; |
| this.selectionModel.selectedIndex = index; |
| this.selectionModel.leadIndex = index; |
| this.inProgramSelection_ = false; |
| }, |
| |
| /** |
| * Element containing the preview image (the first IMG element) and the |
| * camera live stream (the first VIDEO element). |
| * @type {HTMLElement} |
| */ |
| get previewElement() { |
| // TODO(ivankr): temporary hack for non-HTML5 version. |
| return this.previewElement_ || this; |
| }, |
| set previewElement(value) { |
| this.previewElement_ = value; |
| this.previewImage_ = value.querySelector('img'); |
| this.cameraVideo_ = value.querySelector('video'); |
| this.cameraVideo_.addEventListener('canplay', |
| this.handleVideoStarted_.bind(this)); |
| this.cameraVideo_.addEventListener('timeupdate', |
| this.handleVideoUpdate_.bind(this)); |
| this.updatePreview_(); |
| // Initialize camera state and check for its presence. |
| this.cameraLive = true; |
| this.cameraPresent = false; |
| }, |
| |
| /** |
| * Whether the camera live stream and photo should be flipped horizontally. |
| * If setting this property results in photo update, 'photoupdated' event |
| * will be fired with 'dataURL' property containing the photo encoded as |
| * a data URL |
| * @type {boolean} |
| */ |
| get flipPhoto() { |
| return this.flipPhoto_ || false; |
| }, |
| set flipPhoto(value) { |
| if (this.flipPhoto_ == value) |
| return; |
| this.flipPhoto_ = value; |
| this.previewElement.classList.toggle('flip-x'); |
| if (!this.cameraLive) { |
| // Flip current still photo. |
| var e = new cr.Event('photoupdated'); |
| e.dataURL = this.flipPhoto ? |
| this.flipFrame_(this.previewImage_) : this.previewImage_.src; |
| this.dispatchEvent(e); |
| } |
| }, |
| |
| /** |
| * Performs photo capture from the live camera stream. 'phototaken' event |
| * will be fired as soon as captured photo is available, with 'dataURL' |
| * property containing the photo encoded as a data URL. |
| * @return {boolean} Whether photo capture was successful. |
| */ |
| takePhoto: function() { |
| if (!this.cameraOnline) |
| return false; |
| var canvas = document.createElement('canvas'); |
| canvas.width = CAPTURE_SIZE.width; |
| canvas.height = CAPTURE_SIZE.height; |
| this.captureFrame_( |
| this.cameraVideo_, canvas.getContext('2d'), CAPTURE_SIZE); |
| // Preload image before displaying it. |
| var previewImg = new Image(); |
| previewImg.addEventListener('load', function(e) { |
| this.cameraTitle_ = this.capturedImageTitle_; |
| this.cameraImage = previewImg.src; |
| }.bind(this)); |
| previewImg.src = canvas.toDataURL('image/png'); |
| var e = new cr.Event('phototaken'); |
| e.dataURL = this.flipPhoto ? this.flipFrame_(canvas) : previewImg.src; |
| this.dispatchEvent(e); |
| return true; |
| }, |
| |
| /** |
| * Performs video capture from the live camera stream. |
| * @param {function=} opt_callback Callback that receives taken video as |
| * data URL of a vertically stacked PNG sprite. |
| */ |
| takeVideo: function(opt_callback) { |
| var canvas = document.createElement('canvas'); |
| canvas.width = CAPTURE_SIZE.width; |
| canvas.height = CAPTURE_SIZE.height * RECORD_FRAMES; |
| var ctx = canvas.getContext('2d'); |
| // Force canvas initialization to prevent FPS lag on the first frame. |
| ctx.fillRect(0, 0, 1, 1); |
| var captureData = { |
| callback: opt_callback, |
| canvas: canvas, |
| ctx: ctx, |
| frameNo: 0, |
| lastTimestamp: new Date().getTime() |
| }; |
| captureData.timer = window.setInterval( |
| this.captureVideoFrame_.bind(this, captureData), 1000 / RECORD_FPS); |
| }, |
| |
| /** |
| * Discard current photo and return to the live camera stream. |
| */ |
| discardPhoto: function() { |
| this.cameraTitle_ = this.placeholderTitle_; |
| this.cameraImage = null; |
| }, |
| |
| /** |
| * Capture a single still frame from a <video> element, placing it at the |
| * current drawing origin of a canvas context. |
| * @param {HTMLVideoElement} video Video element to capture from. |
| * @param {CanvasRenderingContext2D} ctx Canvas context to draw onto. |
| * @param {{width: number, height: number}} destSize Capture size. |
| * @private |
| */ |
| captureFrame_: function(video, ctx, destSize) { |
| var width = video.videoWidth; |
| var height = video.videoHeight; |
| if (width < destSize.width || height < destSize.height) { |
| console.error('Video capture size too small: ' + |
| width + 'x' + height + '!'); |
| } |
| var src = {}; |
| if (width / destSize.width > height / destSize.height) { |
| // Full height, crop left/right. |
| src.height = height; |
| src.width = height * destSize.width / destSize.height; |
| } else { |
| // Full width, crop top/bottom. |
| src.width = width; |
| src.height = width * destSize.height / destSize.width; |
| } |
| src.x = (width - src.width) / 2; |
| src.y = (height - src.height) / 2; |
| ctx.drawImage(video, src.x, src.y, src.width, src.height, |
| 0, 0, destSize.width, destSize.height); |
| }, |
| |
| /** |
| * Flips frame horizontally. |
| * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} source |
| * Frame to flip. |
| * @return {string} Flipped frame as data URL. |
| */ |
| flipFrame_: function(source) { |
| var canvas = document.createElement('canvas'); |
| canvas.width = CAPTURE_SIZE.width; |
| canvas.height = CAPTURE_SIZE.height; |
| var ctx = canvas.getContext('2d'); |
| ctx.translate(CAPTURE_SIZE.width, 0); |
| ctx.scale(-1.0, 1.0); |
| ctx.drawImage(source, 0, 0); |
| return canvas.toDataURL('image/png'); |
| }, |
| |
| /** |
| * Capture next frame of the video being recorded after a takeVideo() call. |
| * @param {Object} data Property bag with the recorder details. |
| * @private |
| */ |
| captureVideoFrame_: function(data) { |
| var lastTimestamp = new Date().getTime(); |
| var delayMs = lastTimestamp - data.lastTimestamp; |
| console.error('Delay: ' + delayMs + ' (' + (1000 / delayMs + ' FPS)')); |
| data.lastTimestamp = lastTimestamp; |
| |
| this.captureFrame_(this.cameraVideo_, data.ctx, CAPTURE_SIZE); |
| data.ctx.translate(0, CAPTURE_SIZE.height); |
| |
| if (++data.frameNo == RECORD_FRAMES) { |
| window.clearTimeout(data.timer); |
| if (data.callback && typeof data.callback == 'function') |
| data.callback(data.canvas.toDataURL('image/png')); |
| } |
| }, |
| |
| /** |
| * Adds new image to the user image grid. |
| * @param {string} src Image URL. |
| * @param {string=} opt_title Image tooltip. |
| * @param {function=} opt_clickHandler Image click handler. |
| * @param {number=} opt_position If given, inserts new image into |
| * that position (0-based) in image list. |
| * @param {function=} opt_decorateFn Function called with the list element |
| * as argument to do any final decoration. |
| * @return {!Object} Image data inserted into the data model. |
| */ |
| // TODO(ivankr): this function needs some argument list refactoring. |
| addItem: function(url, opt_title, opt_clickHandler, opt_position, |
| opt_decorateFn) { |
| var imageInfo = { |
| url: url, |
| title: opt_title, |
| clickHandler: opt_clickHandler, |
| decorateFn: opt_decorateFn |
| }; |
| this.inProgramSelection_ = true; |
| if (opt_position !== undefined) |
| this.dataModel.splice(opt_position, 0, imageInfo); |
| else |
| this.dataModel.push(imageInfo); |
| this.inProgramSelection_ = false; |
| return imageInfo; |
| }, |
| |
| /** |
| * Returns index of an image in grid. |
| * @param {Object} imageInfo Image data returned from addItem() call. |
| * @return {number} Image index (0-based) or -1 if image was not found. |
| */ |
| indexOf: function(imageInfo) { |
| return this.dataModel.indexOf(imageInfo); |
| }, |
| |
| /** |
| * Replaces an image in the grid. |
| * @param {Object} imageInfo Image data returned from addItem() call. |
| * @param {string} imageUrl New image URL. |
| * @param {string=} opt_title New image tooltip (if undefined, tooltip |
| * is left unchanged). |
| * @return {!Object} Image data of the added or updated image. |
| */ |
| updateItem: function(imageInfo, imageUrl, opt_title) { |
| var imageIndex = this.indexOf(imageInfo); |
| var wasSelected = this.selectionModel.selectedIndex == imageIndex; |
| this.removeItem(imageInfo); |
| var newInfo = this.addItem( |
| imageUrl, |
| opt_title === undefined ? imageInfo.title : opt_title, |
| imageInfo.clickHandler, |
| imageIndex, |
| imageInfo.decorateFn); |
| // Update image data with the reset of the keys from the old data. |
| for (k in imageInfo) { |
| if (!(k in newInfo)) |
| newInfo[k] = imageInfo[k]; |
| } |
| if (wasSelected) |
| this.selectedItem = newInfo; |
| return newInfo; |
| }, |
| |
| /** |
| * Removes previously added image from the grid. |
| * @param {Object} imageInfo Image data returned from the addItem() call. |
| */ |
| removeItem: function(imageInfo) { |
| var index = this.indexOf(imageInfo); |
| if (index != -1) { |
| var wasSelected = this.selectionModel.selectedIndex == index; |
| this.inProgramSelection_ = true; |
| this.dataModel.splice(index, 1); |
| if (wasSelected) { |
| // If item removed was selected, select the item next to it. |
| this.selectedItem = this.dataModel.item( |
| Math.min(this.dataModel.length - 1, index)); |
| } |
| this.inProgramSelection_ = false; |
| } |
| }, |
| |
| /** |
| * Forces re-display, size re-calculation and focuses grid. |
| */ |
| updateAndFocus: function() { |
| // Recalculate the measured item size. |
| this.measured_ = null; |
| this.columns = 0; |
| this.redraw(); |
| this.focus(); |
| } |
| }; |
| |
| /** |
| * URLs of special button images. |
| * @enum {string} |
| */ |
| UserImagesGrid.ButtonImages = { |
| TAKE_PHOTO: 'chrome://theme/IDR_BUTTON_USER_IMAGE_TAKE_PHOTO', |
| CHOOSE_FILE: 'chrome://theme/IDR_BUTTON_USER_IMAGE_CHOOSE_FILE', |
| PROFILE_PICTURE: 'chrome://theme/IDR_PROFILE_PICTURE_LOADING' |
| }; |
| |
| return { |
| UserImagesGrid: UserImagesGrid |
| }; |
| }); |