OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 document.addEventListener('DOMContentLoaded', function() { |
| 6 if (document.location.hash) // File path passed after the #. |
| 7 Gallery.openStandalone(decodeURI(document.location.hash.substr(1))); |
| 8 }); |
| 9 |
| 10 /** |
| 11 * Gallery for viewing and editing image files. |
| 12 * |
| 13 * @param {Object} context Object containing the following: |
| 14 * {function(string)} onNameChange Called every time a selected |
| 15 * item name changes (on rename and on selection change). |
| 16 * {function} onClose |
| 17 * {MetadataCache} metadataCache |
| 18 * {Array.<Object>} shareActions |
| 19 * {string} readonlyDirName Directory name for readonly warning or null. |
| 20 * {DirEntry} saveDirEntry Directory to save to. |
| 21 * {function(string)} displayStringFunction |
| 22 * @class |
| 23 * @constructor |
| 24 */ |
| 25 function Gallery(context) { |
| 26 this.container_ = document.querySelector('.gallery'); |
| 27 this.document_ = document; |
| 28 this.context_ = context; |
| 29 this.metadataCache_ = context.metadataCache; |
| 30 |
| 31 var strf = context.displayStringFunction; |
| 32 this.displayStringFunction_ = function(id, formatArgs) { |
| 33 var args = Array.prototype.slice.call(arguments); |
| 34 args[0] = 'GALLERY_' + id.toUpperCase(); |
| 35 return strf.apply(null, args); |
| 36 }; |
| 37 |
| 38 this.initListeners_(); |
| 39 this.initDom_(); |
| 40 } |
| 41 |
| 42 /** |
| 43 * Create and initialize a Gallery object based on a context. |
| 44 * |
| 45 * @param {Object} context Gallery context. |
| 46 * @param {Array.<string>} urls Array of image urls. |
| 47 * @param {string} selectedUrl Selected url. |
| 48 */ |
| 49 Gallery.open = function(context, urls, selectedUrl) { |
| 50 Gallery.instance = new Gallery(context); |
| 51 Gallery.instance.load(urls, selectedUrl); |
| 52 }; |
| 53 |
| 54 /** |
| 55 * Create a Gallery object in a tab. |
| 56 * @param {string} path File system path to a selected file. |
| 57 */ |
| 58 Gallery.openStandalone = function(path) { |
| 59 ImageUtil.metrics = metrics; |
| 60 |
| 61 var currentDir; |
| 62 var urls = []; |
| 63 var selectedUrl; |
| 64 |
| 65 Gallery.getFileBrowserPrivate().requestLocalFileSystem(function(filesystem) { |
| 66 // If the path points to the directory scan it. |
| 67 filesystem.root.getDirectory(path, {create: false}, scanDirectory, |
| 68 function() { |
| 69 // Try to scan the parent directory. |
| 70 var pathParts = path.split('/'); |
| 71 pathParts.pop(); |
| 72 var parentPath = pathParts.join('/'); |
| 73 filesystem.root.getDirectory(parentPath, {create: false}, |
| 74 scanDirectory, open /* no data, just display an error */); |
| 75 }); |
| 76 }); |
| 77 |
| 78 function scanDirectory(dirEntry) { |
| 79 currentDir = dirEntry; |
| 80 util.forEachDirEntry(currentDir, function(entry) { |
| 81 if (entry == null) { |
| 82 open(); |
| 83 } else if (FileType.isImageOrVideo(entry)) { |
| 84 var url = entry.toURL(); |
| 85 urls.push(url); |
| 86 if (entry.fullPath == path) |
| 87 selectedUrl = url; |
| 88 } |
| 89 }); |
| 90 } |
| 91 |
| 92 function onNameChange(name) { |
| 93 window.top.document.title = name; |
| 94 |
| 95 var newPath = currentDir.fullPath + '/' + name; |
| 96 var location = document.location.origin + document.location.pathname + |
| 97 '#' + encodeURI(newPath); |
| 98 history.replaceState(undefined, newPath, location); |
| 99 } |
| 100 |
| 101 function onClose() { |
| 102 document.location = 'main.html?' + |
| 103 JSON.stringify({defaultPath: document.location.hash.substr(1)}); |
| 104 } |
| 105 |
| 106 function open() { |
| 107 Gallery.getFileBrowserPrivate().getStrings(function(strings) { |
| 108 loadTimeData.data = strings; |
| 109 var context = { |
| 110 readonlyDirName: null, |
| 111 saveDirEntry: currentDir, |
| 112 metadataCache: MetadataCache.createFull(), |
| 113 onNameChange: onNameChange, |
| 114 onClose: onClose, |
| 115 displayStringFunction: strf |
| 116 }; |
| 117 Gallery.open(context, urls, selectedUrl || urls[0]); |
| 118 }); |
| 119 } |
| 120 }; |
| 121 |
| 122 /** |
| 123 * Tools fade-out timeout im milliseconds. |
| 124 * @type {Number} |
| 125 */ |
| 126 Gallery.FADE_TIMEOUT = 3000; |
| 127 |
| 128 /** |
| 129 * First time tools fade-out timeout im milliseconds. |
| 130 * @type {Number} |
| 131 */ |
| 132 Gallery.FIRST_FADE_TIMEOUT = 1000; |
| 133 |
| 134 /** |
| 135 * Types of metadata Gallery uses (to query the metadata cache). |
| 136 */ |
| 137 Gallery.METADATA_TYPE = 'thumbnail|filesystem|media|streaming'; |
| 138 |
| 139 /** |
| 140 * Initialize listeners. |
| 141 * @private |
| 142 */ |
| 143 |
| 144 Gallery.prototype.initListeners_ = function() { |
| 145 this.document_.oncontextmenu = function(e) { e.preventDefault(); }; |
| 146 |
| 147 this.document_.body.addEventListener('keydown', this.onKeyDown_.bind(this)); |
| 148 |
| 149 this.inactivityWatcher_ = new MouseInactivityWatcher( |
| 150 this.container_, Gallery.FADE_TIMEOUT, this.hasActiveTool.bind(this)); |
| 151 |
| 152 // Show tools when the user touches the screen. |
| 153 this.document_.body.addEventListener('touchstart', |
| 154 this.inactivityWatcher_.startActivity.bind(this.inactivityWatcher_)); |
| 155 var initiateFading = |
| 156 this.inactivityWatcher_.stopActivity.bind(this.inactivityWatcher_, |
| 157 Gallery.FADE_TIMEOUT); |
| 158 this.document_.body.addEventListener('touchend', initiateFading); |
| 159 this.document_.body.addEventListener('touchcancel', initiateFading); |
| 160 }; |
| 161 |
| 162 /** |
| 163 * Initializes DOM UI |
| 164 * @private |
| 165 */ |
| 166 Gallery.prototype.initDom_ = function() { |
| 167 var content = util.createChild(this.container_, 'content'); |
| 168 content.addEventListener('click', this.onContentClick_.bind(this)); |
| 169 |
| 170 var closeButton = util.createChild(this.container_, 'close tool dimmable'); |
| 171 util.createChild(closeButton); |
| 172 closeButton.addEventListener('click', this.onClose_.bind(this)); |
| 173 |
| 174 this.toolbar_ = util.createChild(this.container_, 'toolbar tool dimmable'); |
| 175 |
| 176 this.filenameSpacer_ = util.createChild(this.toolbar_, 'filename-spacer'); |
| 177 |
| 178 var nameBox = util.createChild(this.filenameSpacer_, 'namebox'); |
| 179 |
| 180 this.filenameText_ = util.createChild(nameBox); |
| 181 this.filenameText_.addEventListener('click', |
| 182 this.onFilenameClick_.bind(this)); |
| 183 |
| 184 this.filenameEdit_ = this.document_.createElement('input'); |
| 185 this.filenameEdit_.setAttribute('type', 'text'); |
| 186 this.filenameEdit_.addEventListener('blur', |
| 187 this.onFilenameEditBlur_.bind(this)); |
| 188 this.filenameEdit_.addEventListener('keydown', |
| 189 this.onFilenameEditKeydown_.bind(this)); |
| 190 nameBox.appendChild(this.filenameEdit_); |
| 191 |
| 192 util.createChild(this.toolbar_, 'button-spacer'); |
| 193 |
| 194 this.prompt_ = new ImageEditor.Prompt( |
| 195 this.container_, this.displayStringFunction_); |
| 196 |
| 197 this.slideMode_ = new SlideMode(this.container_, this.toolbar_, this.prompt_, |
| 198 this.context_, this.displayStringFunction_); |
| 199 this.slideMode_.addEventListener('edit', this.onEdit_.bind(this)); |
| 200 this.slideMode_.addEventListener('selection', this.onSelection_.bind(this)); |
| 201 |
| 202 this.shareMode_ = new ShareMode( |
| 203 this.slideMode_.editor_, this.container_, this.toolbar_, |
| 204 this.onShare_.bind(this), this.executeWhenReady.bind(this), |
| 205 this.displayStringFunction_); |
| 206 |
| 207 Gallery.getFileBrowserPrivate().isFullscreen(function(fullscreen) { |
| 208 this.originalFullscreen_ = fullscreen; |
| 209 }.bind(this)); |
| 210 }; |
| 211 |
| 212 /** |
| 213 * Load the content. |
| 214 * |
| 215 * @param {Array.<string>} urls Array of urls. |
| 216 * @param {string} selectedUrl Selected url. |
| 217 */ |
| 218 Gallery.prototype.load = function(urls, selectedUrl) { |
| 219 this.items_ = []; |
| 220 for (var index = 0; index < urls.length; ++index) { |
| 221 this.items_.push(new Gallery.Item(urls[index])); |
| 222 } |
| 223 |
| 224 var selectedIndex = urls.indexOf(selectedUrl); |
| 225 this.slideMode_.load(this.items_, selectedIndex, function() { |
| 226 // Flash the toolbar briefly to show it is there. |
| 227 this.inactivityWatcher_.startActivity(); |
| 228 this.inactivityWatcher_.stopActivity(Gallery.FIRST_FADE_TIMEOUT); |
| 229 }.bind(this)); |
| 230 }; |
| 231 |
| 232 /** |
| 233 * Close the Gallery. |
| 234 * @private |
| 235 */ |
| 236 Gallery.prototype.close_ = function() { |
| 237 Gallery.getFileBrowserPrivate().isFullscreen(function(fullscreen) { |
| 238 if (this.originalFullscreen_ != fullscreen) { |
| 239 Gallery.toggleFullscreen(); |
| 240 } |
| 241 this.context_.onClose(); |
| 242 }.bind(this)); |
| 243 }; |
| 244 |
| 245 /** |
| 246 * Handle user's 'Close' action (Escape or a click on the X icon). |
| 247 * @private |
| 248 */ |
| 249 Gallery.prototype.onClose_ = function() { |
| 250 this.executeWhenReady(this.close_.bind(this)); |
| 251 }; |
| 252 |
| 253 /** |
| 254 * Execute a function when the editor is done with the modifications. |
| 255 * @param {function} callback Function to execute. |
| 256 */ |
| 257 Gallery.prototype.executeWhenReady = function(callback) { |
| 258 //TODO(kaznacheev): Execute directly when in grid mode. |
| 259 this.slideMode_.editor_.executeWhenReady(callback); |
| 260 }; |
| 261 |
| 262 /** |
| 263 * @return {Object} File browser private API. |
| 264 */ |
| 265 Gallery.getFileBrowserPrivate = function() { |
| 266 return chrome.fileBrowserPrivate || window.top.chrome.fileBrowserPrivate; |
| 267 }; |
| 268 |
| 269 /** |
| 270 * Switches gallery to fullscreen mode and back. |
| 271 */ |
| 272 Gallery.toggleFullscreen = function() { |
| 273 Gallery.getFileBrowserPrivate().toggleFullscreen(); |
| 274 }; |
| 275 |
| 276 /** |
| 277 * @return {boolean} True if some tool is currently active. |
| 278 */ |
| 279 Gallery.prototype.hasActiveTool = function() { |
| 280 return this.slideMode_.isEditing() || this.isSharing_() || this.isRenaming_(); |
| 281 }; |
| 282 |
| 283 /** |
| 284 * Check if the tools are active and notify the inactivity watcher. |
| 285 * @private |
| 286 */ |
| 287 Gallery.prototype.checkActivity_ = function() { |
| 288 if (this.hasActiveTool()) |
| 289 this.inactivityWatcher_.startActivity(); |
| 290 else |
| 291 this.inactivityWatcher_.stopActivity(); |
| 292 }; |
| 293 |
| 294 /** |
| 295 * Edit toggle event handler. |
| 296 * @private |
| 297 */ |
| 298 Gallery.prototype.onEdit_ = function() { |
| 299 // The user has just clicked on the Edit button. Dismiss the Share menu. |
| 300 if (this.isSharing_()) |
| 301 this.onShare_(); |
| 302 this.checkActivity_(); |
| 303 }; |
| 304 |
| 305 /** |
| 306 * @return {Array.<Gallery.Item>} Current selection. |
| 307 */ |
| 308 Gallery.prototype.getSelectedItems = function() { |
| 309 // TODO(kaznacheev) support multiple selection grid/mosaic mode. |
| 310 return [this.slideMode_.getSelectedItem()]; |
| 311 }; |
| 312 |
| 313 /** |
| 314 * @return {Gallery.Item} Current single selection. |
| 315 */ |
| 316 Gallery.prototype.getSingleSelectedItem = function() { |
| 317 var items = this.getSelectedItems(); |
| 318 if (items.length > 1) |
| 319 throw new Error('Unexpected multiple selection'); |
| 320 return items[0]; |
| 321 }; |
| 322 |
| 323 /** |
| 324 * Selection change event handler. |
| 325 * @private |
| 326 */ |
| 327 Gallery.prototype.onSelection_ = function() { |
| 328 this.updateFilename_(); |
| 329 this.shareMode_.updateMenu( |
| 330 this.getSelectedItems().map(function(item) { return item.getUrl() })); |
| 331 }; |
| 332 |
| 333 /** |
| 334 * Keydown handler. |
| 335 * @param {Event} event Event. |
| 336 * @private |
| 337 */ |
| 338 Gallery.prototype.onKeyDown_ = function(event) { |
| 339 if (this.slideMode_.onKeyDown(event)) |
| 340 return; |
| 341 |
| 342 switch (util.getKeyModifiers(event) + event.keyIdentifier) { |
| 343 case 'U+0008': // Backspace. |
| 344 // The default handler would call history.back and close the Gallery. |
| 345 event.preventDefault(); |
| 346 break; |
| 347 |
| 348 case 'U+001B': // Escape |
| 349 if (this.isSharing_()) |
| 350 this.onShare_(); |
| 351 else |
| 352 this.onClose_(); |
| 353 break; |
| 354 } |
| 355 }; |
| 356 |
| 357 // Name box and rename support. |
| 358 |
| 359 /** |
| 360 * Update the displayed current item file name. |
| 361 * |
| 362 * @private |
| 363 */ |
| 364 Gallery.prototype.updateFilename_ = function() { |
| 365 var fullName = this.getSingleSelectedItem().getFileName(); |
| 366 |
| 367 this.context_.onNameChange(fullName); |
| 368 |
| 369 var displayName = ImageUtil.getFileNameFromFullName(fullName); |
| 370 this.filenameEdit_.value = displayName; |
| 371 this.filenameText_.textContent = displayName; |
| 372 }; |
| 373 |
| 374 /** |
| 375 * Click event handler on filename edit box |
| 376 * @private |
| 377 */ |
| 378 Gallery.prototype.onFilenameClick_ = function() { |
| 379 // We can't rename files in readonly directory. |
| 380 if (this.context_.readonlyDirName) |
| 381 return; |
| 382 |
| 383 ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', true); |
| 384 setTimeout(this.filenameEdit_.select.bind(this.filenameEdit_), 0); |
| 385 this.inactivityWatcher_.startActivity(); |
| 386 }; |
| 387 |
| 388 /** |
| 389 * Blur event handler on filename edit box |
| 390 * @private |
| 391 */ |
| 392 Gallery.prototype.onFilenameEditBlur_ = function() { |
| 393 if (this.filenameEdit_.value && this.filenameEdit_.value[0] == '.') { |
| 394 this.prompt_.show('file_hidden_name', 5000); |
| 395 this.filenameEdit_.focus(); |
| 396 return; |
| 397 } |
| 398 |
| 399 var item = this.getSingleSelectedItem(); |
| 400 var oldUrl = item.getUrl(); |
| 401 |
| 402 var onFileExists = function() { |
| 403 this.prompt_.show('file_exists', 3000); |
| 404 this.filenameEdit_.value = name; |
| 405 this.onFilenameClick_(); |
| 406 }.bind(this); |
| 407 |
| 408 var onSuccess = function() { |
| 409 this.slideMode_.updateSelectedUrl_(oldUrl, item.getUrl()); |
| 410 }.bind(this); |
| 411 |
| 412 if (this.filenameEdit_.value) { |
| 413 this.getSingleSelectedItem().rename(this.context_.saveDirEntry, |
| 414 this.filenameEdit_.value, onSuccess, onFileExists); |
| 415 } |
| 416 |
| 417 ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', false); |
| 418 this.checkActivity_(); |
| 419 }; |
| 420 |
| 421 /** |
| 422 * Keydown event handler on filename edit box |
| 423 * @private |
| 424 */ |
| 425 Gallery.prototype.onFilenameEditKeydown_ = function() { |
| 426 switch (event.keyCode) { |
| 427 case 27: // Escape |
| 428 this.updateFilename_(); |
| 429 this.filenameEdit_.blur(); |
| 430 break; |
| 431 |
| 432 case 13: // Enter |
| 433 this.filenameEdit_.blur(); |
| 434 break; |
| 435 } |
| 436 event.stopPropagation(); |
| 437 }; |
| 438 |
| 439 /** |
| 440 * @return {Boolean} True if file renaming is currently in progress |
| 441 * @private |
| 442 */ |
| 443 Gallery.prototype.isRenaming_ = function() { |
| 444 return this.filenameSpacer_.hasAttribute('renaming'); |
| 445 }; |
| 446 |
| 447 /** |
| 448 * Content area click handler. |
| 449 * @private |
| 450 */ |
| 451 Gallery.prototype.onContentClick_ = function() { |
| 452 this.filenameEdit_.blur(); |
| 453 }; |
| 454 |
| 455 // Share button support. |
| 456 |
| 457 /** |
| 458 * @return {boolean} True if the Share mode is active. |
| 459 * @private |
| 460 */ |
| 461 Gallery.prototype.isSharing_ = function() { |
| 462 return this.shareMode_.isActive(); |
| 463 }; |
| 464 |
| 465 /** |
| 466 * Share button handler. |
| 467 * @param {Event} event Event. |
| 468 * @private |
| 469 */ |
| 470 Gallery.prototype.onShare_ = function(event) { |
| 471 this.shareMode_.toggle(event); |
| 472 this.checkActivity_(); |
| 473 }; |
| 474 |
| 475 /** |
| 476 * |
| 477 * @param {ImageEditor} editor Editor. |
| 478 * @param {Element} container Container element. |
| 479 * @param {Element} toolbar Toolbar element. |
| 480 * @param {function} onClick Click handler. |
| 481 * @param {function(function())} actionCallback Function to execute the action. |
| 482 * @param {function(string):string} displayStringFunction String formatting |
| 483 * function. |
| 484 * @constructor |
| 485 */ |
| 486 function ShareMode(editor, container, toolbar, |
| 487 onClick, actionCallback, displayStringFunction) { |
| 488 ImageEditor.Mode.call(this, 'share'); |
| 489 |
| 490 this.message_ = null; |
| 491 |
| 492 var button = util.createChild(toolbar, 'button share'); |
| 493 button.textContent = displayStringFunction('share'); |
| 494 button.addEventListener('click', onClick); |
| 495 this.bind(editor, button); |
| 496 |
| 497 this.actionCallback_ = actionCallback; |
| 498 |
| 499 this.menu_ = util.createChild(container, 'share-menu'); |
| 500 this.menu_.hidden = true; |
| 501 |
| 502 util.createChild(this.menu_, 'bubble-point'); |
| 503 } |
| 504 |
| 505 ShareMode.prototype = { __proto__: ImageEditor.Mode.prototype }; |
| 506 |
| 507 /** |
| 508 * Shows share mode UI. |
| 509 */ |
| 510 ShareMode.prototype.setUp = function() { |
| 511 ImageEditor.Mode.prototype.setUp.apply(this, arguments); |
| 512 this.menu_.hidden = false; |
| 513 ImageUtil.setAttribute(this.button_, 'pressed', false); |
| 514 }; |
| 515 |
| 516 /** |
| 517 * Hides share mode UI. |
| 518 */ |
| 519 ShareMode.prototype.cleanUpUI = function() { |
| 520 ImageEditor.Mode.prototype.cleanUpUI.apply(this, arguments); |
| 521 this.menu_.hidden = true; |
| 522 }; |
| 523 |
| 524 /** |
| 525 * @return {boolean} True if the menu is currently open. |
| 526 */ |
| 527 ShareMode.prototype.isActive = function() { |
| 528 return !this.menu_.hidden; |
| 529 }; |
| 530 |
| 531 /** |
| 532 * Show/hide the menu. |
| 533 * @param {Event} event Event. |
| 534 */ |
| 535 ShareMode.prototype.toggle = function(event) { |
| 536 this.editor_.enterMode(this, event); |
| 537 }; |
| 538 |
| 539 /** |
| 540 * Update available actions list based on the currently selected urls. |
| 541 * |
| 542 * @param {Array.<string>} urls Array of urls. |
| 543 */ |
| 544 ShareMode.prototype.updateMenu = function(urls) { |
| 545 var internalId = util.getExtensionId(); |
| 546 function isShareAction(task) { |
| 547 var task_parts = task.taskId.split('|'); |
| 548 return task_parts[0] != internalId; |
| 549 } |
| 550 |
| 551 var items = this.menu_.querySelectorAll('.item'); |
| 552 for (var i = 0; i != items.length; i++) { |
| 553 items[i].parentNode.removeChild(items[i]); |
| 554 } |
| 555 |
| 556 var api = Gallery.getFileBrowserPrivate(); |
| 557 api.getFileTasks(urls, function(tasks) { |
| 558 for (var i = 0; i != tasks.length; i++) { |
| 559 var task = tasks[i]; |
| 560 if (!isShareAction(task)) continue; |
| 561 |
| 562 var item = document.createElement('div'); |
| 563 item.className = 'item'; |
| 564 this.menu_.appendChild(item); |
| 565 |
| 566 item.textContent = task.title; |
| 567 item.style.backgroundImage = 'url(' + task.iconUrl + ')'; |
| 568 item.addEventListener('click', this.actionCallback_.bind(null, |
| 569 api.executeTask.bind(api, task.taskId, urls))); |
| 570 } |
| 571 |
| 572 if (this.menu_.firstChild) |
| 573 this.button_.removeAttribute('disabled'); |
| 574 else |
| 575 this.button_.setAttribute('disabled', 'true'); |
| 576 }.bind(this)); |
| 577 }; |
OLD | NEW |