| 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 'use strict'; | |
| 6 | |
| 7 document.addEventListener('DOMContentLoaded', function() { | |
| 8 PhotoImport.load(); | |
| 9 }); | |
| 10 | |
| 11 /** | |
| 12 * The main Photo App object. | |
| 13 * @param {HTMLElement} dom Container. | |
| 14 * @param {VolumeManagerWrapper} volumeManager The initialized | |
| 15 * VolumeManagerWrapper instance. | |
| 16 * @param {Object} params Parameters. | |
| 17 * @constructor | |
| 18 */ | |
| 19 function PhotoImport(dom, volumeManager, params) { | |
| 20 this.dom_ = dom; | |
| 21 this.document_ = this.dom_.ownerDocument; | |
| 22 this.metadataCache_ = params.metadataCache; | |
| 23 this.volumeManager_ = volumeManager; | |
| 24 this.fileOperationManager_ = FileOperationManagerWrapper.getInstance(); | |
| 25 this.mediaFilesList_ = null; | |
| 26 this.destination_ = null; | |
| 27 this.myPhotosDirectory_ = null; | |
| 28 this.parentWindowId_ = params.parentWindowId; | |
| 29 | |
| 30 this.initDom_(); | |
| 31 this.initMyPhotos_(); | |
| 32 this.loadSource_(params.source); | |
| 33 } | |
| 34 | |
| 35 PhotoImport.prototype = { __proto__: cr.EventTarget.prototype }; | |
| 36 | |
| 37 /** | |
| 38 * Single item width. | |
| 39 * Keep in sync with .grid-item rule in photo_import.css. | |
| 40 */ | |
| 41 PhotoImport.ITEM_WIDTH = 164 + 8; | |
| 42 | |
| 43 /** | |
| 44 * Number of tries in creating a destination directory. | |
| 45 */ | |
| 46 PhotoImport.CREATE_DESTINATION_TRIES = 100; | |
| 47 | |
| 48 /** | |
| 49 * Loads app in the document body. | |
| 50 * @param {Object=} opt_params Parameters. | |
| 51 */ | |
| 52 PhotoImport.load = function(opt_params) { | |
| 53 ImageUtil.metrics = metrics; | |
| 54 | |
| 55 var hash = location.hash ? location.hash.substr(1) : ''; | |
| 56 var query = location.search ? location.search.substr(1) : ''; | |
| 57 var params = opt_params || {}; | |
| 58 if (!params.source) params.source = hash; | |
| 59 if (!params.parentWindowId && query) params.parentWindowId = query; | |
| 60 if (!params.metadataCache) params.metadataCache = MetadataCache.createFull(); | |
| 61 | |
| 62 var api = chrome.fileBrowserPrivate || window.top.chrome.fileBrowserPrivate; | |
| 63 api.getStrings(function(strings) { | |
| 64 loadTimeData.data = strings; | |
| 65 var dom = document.querySelector('.photo-import'); | |
| 66 | |
| 67 var volumeManager = new VolumeManagerWrapper( | |
| 68 VolumeManagerWrapper.DriveEnabledStatus.DRIVE_ENABLED); | |
| 69 volumeManager.ensureInitialized(function() { | |
| 70 new PhotoImport(dom, volumeManager, params); | |
| 71 }); | |
| 72 }); | |
| 73 }; | |
| 74 | |
| 75 /** | |
| 76 * One-time initialization of dom elements. | |
| 77 * @private | |
| 78 */ | |
| 79 PhotoImport.prototype.initDom_ = function() { | |
| 80 this.dom_.setAttribute('loading', ''); | |
| 81 this.dom_.ownerDocument.defaultView.addEventListener( | |
| 82 'resize', this.onResize_.bind(this)); | |
| 83 | |
| 84 this.spinner_ = this.dom_.querySelector('.spinner'); | |
| 85 | |
| 86 this.document_.querySelector('title').textContent = | |
| 87 loadTimeData.getString('PHOTO_IMPORT_TITLE'); | |
| 88 this.dom_.querySelector('.caption').textContent = | |
| 89 loadTimeData.getString('PHOTO_IMPORT_CAPTION'); | |
| 90 this.selectAllNone_ = this.dom_.querySelector('.select'); | |
| 91 this.selectAllNone_.addEventListener('click', | |
| 92 this.onSelectAllNone_.bind(this)); | |
| 93 | |
| 94 this.dom_.querySelector('label[for=delete-after-checkbox]').textContent = | |
| 95 loadTimeData.getString('PHOTO_IMPORT_DELETE_AFTER'); | |
| 96 this.selectedCount_ = this.dom_.querySelector('.selected-count'); | |
| 97 | |
| 98 this.importButton_ = this.dom_.querySelector('button.import'); | |
| 99 this.importButton_.textContent = | |
| 100 loadTimeData.getString('PHOTO_IMPORT_IMPORT_BUTTON'); | |
| 101 this.importButton_.addEventListener('click', this.onImportClick_.bind(this)); | |
| 102 | |
| 103 this.cancelButton_ = this.dom_.querySelector('button.cancel'); | |
| 104 this.cancelButton_.textContent = str('CANCEL_LABEL'); | |
| 105 this.cancelButton_.addEventListener('click', this.onCancelClick_.bind(this)); | |
| 106 | |
| 107 this.grid_ = this.dom_.querySelector('grid'); | |
| 108 cr.ui.Grid.decorate(this.grid_); | |
| 109 this.grid_.itemConstructor = GridItem.bind(null, this); | |
| 110 this.fileList_ = new cr.ui.ArrayDataModel([]); | |
| 111 this.grid_.selectionModel = new cr.ui.ListSelectionModel(); | |
| 112 this.grid_.dataModel = this.fileList_; | |
| 113 this.grid_.selectionModel.addEventListener('change', | |
| 114 this.onSelectionChanged_.bind(this)); | |
| 115 this.onSelectionChanged_(); | |
| 116 | |
| 117 this.importingDialog_ = new ImportingDialog( | |
| 118 this.dom_, this.fileOperationManager_, | |
| 119 this.metadataCache_, this.parentWindowId_); | |
| 120 | |
| 121 var dialogs = cr.ui.dialogs; | |
| 122 dialogs.BaseDialog.OK_LABEL = str('OK_LABEL'); | |
| 123 dialogs.BaseDialog.CANCEL_LABEL = str('CANCEL_LABEL'); | |
| 124 this.alert_ = new dialogs.AlertDialog(this.dom_); | |
| 125 }; | |
| 126 | |
| 127 /** | |
| 128 * One-time initialization of the My Photos directory. | |
| 129 * @private | |
| 130 */ | |
| 131 PhotoImport.prototype.initMyPhotos_ = function() { | |
| 132 var driveVolume = this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE); | |
| 133 if (!driveVolume || driveVolume.error || !driveVolume.root) { | |
| 134 this.onError_(loadTimeData.getString('PHOTO_IMPORT_DRIVE_ERROR')); | |
| 135 return; | |
| 136 } | |
| 137 | |
| 138 util.getOrCreateDirectory( | |
| 139 driveVolume.root, | |
| 140 loadTimeData.getString('PHOTO_IMPORT_MY_PHOTOS_DIRECTORY_NAME'), | |
| 141 function(entry) { | |
| 142 // This may enable the import button, so check that. | |
| 143 this.myPhotosDirectory_ = entry; | |
| 144 this.onSelectionChanged_(); | |
| 145 }, | |
| 146 function(error) { | |
| 147 this.onError_(loadTimeData.getString('PHOTO_IMPORT_DRIVE_ERROR')); | |
| 148 }.bind(this)); | |
| 149 }; | |
| 150 | |
| 151 /** | |
| 152 * Creates the destination directory. | |
| 153 * @param {function} onSuccess Callback on success. | |
| 154 * @private | |
| 155 */ | |
| 156 PhotoImport.prototype.createDestination_ = function(onSuccess) { | |
| 157 var onError = this.onError_.bind( | |
| 158 this, loadTimeData.getString('PHOTO_IMPORT_DESTINATION_ERROR')); | |
| 159 | |
| 160 var dateFormatter = Intl.DateTimeFormat( | |
| 161 [] /* default locale */, | |
| 162 {year: 'numeric', month: 'short', day: 'numeric'}); | |
| 163 | |
| 164 var baseName = PathUtil.join( | |
| 165 RootDirectory.DRIVE, | |
| 166 loadTimeData.getString('PHOTO_IMPORT_MY_PHOTOS_DIRECTORY_NAME'), | |
| 167 dateFormatter.format(new Date())); | |
| 168 | |
| 169 var driveVolume = this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE); | |
| 170 if (!driveVolume || driveVolume.error || !driveVolume.root) { | |
| 171 onError(); | |
| 172 return; | |
| 173 } | |
| 174 | |
| 175 var tryNext = function(number) { | |
| 176 if (number > PhotoImport.CREATE_DESTINATION_TRIES) { | |
| 177 console.error('Too many directories with the same base name exist.'); | |
| 178 onError(); | |
| 179 return; | |
| 180 } | |
| 181 | |
| 182 var directoryName = baseName; | |
| 183 if (number > 1) | |
| 184 directoryName += ' (' + (tryNumber) + ')'; | |
| 185 driveVolume.root.getDirectory( | |
| 186 directoryName, | |
| 187 {create: true, exclusive: true}, | |
| 188 function(entry) { | |
| 189 this.destination_ = entry; | |
| 190 onSuccess(); | |
| 191 }.bind(this), | |
| 192 function(error) { | |
| 193 if (error.code === FileError.PATH_EXISTS_ERR) { | |
| 194 // If there already exists an entry, retry with incrementing the | |
| 195 // number. | |
| 196 tryNext(number + 1); | |
| 197 return; | |
| 198 } | |
| 199 onError(); | |
| 200 }.bind(this)); | |
| 201 }.bind(this); | |
| 202 | |
| 203 tryNext(1); | |
| 204 }; | |
| 205 | |
| 206 | |
| 207 /** | |
| 208 * Load the source contents. | |
| 209 * @param {string} source Path to source. | |
| 210 * @private | |
| 211 */ | |
| 212 PhotoImport.prototype.loadSource_ = function(source) { | |
| 213 var onError = this.onError_.bind( | |
| 214 this, loadTimeData.getString('PHOTO_IMPORT_SOURCE_ERROR')); | |
| 215 | |
| 216 var result = []; | |
| 217 this.volumeManager_.resolvePath( | |
| 218 source, | |
| 219 function(sourceEntry) { | |
| 220 util.traverseTree( | |
| 221 entry, | |
| 222 function(entry) { | |
| 223 if (!FileType.isVisible(entry)) | |
| 224 return false; | |
| 225 if (FileType.isImageOrVideo(entry)) | |
| 226 result.push(entry); | |
| 227 return true; | |
| 228 }, | |
| 229 function() { | |
| 230 this.dom_.removeAttribute('loading'); | |
| 231 this.mediaFilesList_ = result; | |
| 232 this.fillGrid_(); | |
| 233 }.bind(this), | |
| 234 onError); | |
| 235 }.bind(this), | |
| 236 onError); | |
| 237 }; | |
| 238 | |
| 239 /** | |
| 240 * Renders files into grid. | |
| 241 * @private | |
| 242 */ | |
| 243 PhotoImport.prototype.fillGrid_ = function() { | |
| 244 if (!this.mediaFilesList_) return; | |
| 245 this.fileList_.splice(0, this.fileList_.length); | |
| 246 this.fileList_.push.apply(this.fileList_, this.mediaFilesList_); | |
| 247 }; | |
| 248 | |
| 249 /** | |
| 250 * Creates groups for files based on modification date. | |
| 251 * @param {Array.<Entry>} files File list. | |
| 252 * @param {Object} filesystem Filesystem metadata. | |
| 253 * @return {Array.<Object>} List of grouped items. | |
| 254 * @private | |
| 255 */ | |
| 256 PhotoImport.prototype.createGroups_ = function(files, filesystem) { | |
| 257 var dateFormatter = Intl.DateTimeFormat( | |
| 258 [] /* default locale */, | |
| 259 {year: 'numeric', month: 'short', day: 'numeric'}); | |
| 260 | |
| 261 var columns = this.grid_.columns; | |
| 262 | |
| 263 var unknownGroup = { | |
| 264 type: 'group', | |
| 265 date: 0, | |
| 266 title: loadTimeData.getString('PHOTO_IMPORT_UNKNOWN_DATE'), | |
| 267 items: [] | |
| 268 }; | |
| 269 | |
| 270 var groupsMap = {}; | |
| 271 | |
| 272 for (var index = 0; index < files.length; index++) { | |
| 273 var props = filesystem[index]; | |
| 274 var item = { type: 'entry', entry: files[index] }; | |
| 275 | |
| 276 if (!props || !props.modificationTime) { | |
| 277 item.group = unknownGroup; | |
| 278 unknownGroup.items.push(item); | |
| 279 continue; | |
| 280 } | |
| 281 | |
| 282 var date = new Date(props.modificationTime); | |
| 283 date.setHours(0); | |
| 284 date.setMinutes(0); | |
| 285 date.setSeconds(0); | |
| 286 date.setMilliseconds(0); | |
| 287 | |
| 288 var time = date.getTime(); | |
| 289 if (!(time in groupsMap)) { | |
| 290 groupsMap[time] = { | |
| 291 type: 'group', | |
| 292 date: date, | |
| 293 title: dateFormatter.format(date), | |
| 294 items: [] | |
| 295 }; | |
| 296 } | |
| 297 | |
| 298 var group = groupsMap[time]; | |
| 299 group.items.push(item); | |
| 300 item.group = group; | |
| 301 } | |
| 302 | |
| 303 var groups = []; | |
| 304 for (var time in groupsMap) { | |
| 305 if (groupsMap.hasOwnProperty(time)) { | |
| 306 groups.push(groupsMap[time]); | |
| 307 } | |
| 308 } | |
| 309 if (unknownGroup.items.length > 0) | |
| 310 groups.push(unknownGroup); | |
| 311 | |
| 312 groups.sort(function(a, b) { | |
| 313 return b.date.getTime() - a.date.getTime(); | |
| 314 }); | |
| 315 | |
| 316 var list = []; | |
| 317 for (var index = 0; index < groups.length; index++) { | |
| 318 var group = groups[index]; | |
| 319 | |
| 320 list.push(group); | |
| 321 for (var t = 1; t < columns; t++) { | |
| 322 list.push({ type: 'empty' }); | |
| 323 } | |
| 324 | |
| 325 for (var j = 0; j < group.items.length; j++) { | |
| 326 list.push(group.items[j]); | |
| 327 } | |
| 328 | |
| 329 var count = group.items.length; | |
| 330 while (count % columns != 0) { | |
| 331 list.push({ type: 'empty' }); | |
| 332 count++; | |
| 333 } | |
| 334 } | |
| 335 | |
| 336 return list; | |
| 337 }; | |
| 338 | |
| 339 /** | |
| 340 * Decorates grid item. | |
| 341 * @param {HTMLLIElement} li The list item. | |
| 342 * @param {FileEntry} entry The file entry. | |
| 343 * @private | |
| 344 */ | |
| 345 PhotoImport.prototype.decorateGridItem_ = function(li, entry) { | |
| 346 li.className = 'grid-item'; | |
| 347 li.entry = entry; | |
| 348 | |
| 349 var frame = this.document_.createElement('div'); | |
| 350 frame.className = 'grid-frame'; | |
| 351 li.appendChild(frame); | |
| 352 | |
| 353 var box = this.document_.createElement('div'); | |
| 354 box.className = 'img-container'; | |
| 355 this.metadataCache_.get(entry, 'thumbnail|filesystem', | |
| 356 function(metadata) { | |
| 357 new ThumbnailLoader(entry.toURL(), | |
| 358 ThumbnailLoader.LoaderType.IMAGE, | |
| 359 metadata). | |
| 360 load(box, ThumbnailLoader.FillMode.FIT, | |
| 361 ThumbnailLoader.OptimizationMode.DISCARD_DETACHED); | |
| 362 }); | |
| 363 frame.appendChild(box); | |
| 364 | |
| 365 var check = this.document_.createElement('div'); | |
| 366 check.className = 'check'; | |
| 367 li.appendChild(check); | |
| 368 }; | |
| 369 | |
| 370 /** | |
| 371 * Handles the 'pick all/none' action. | |
| 372 * @private | |
| 373 */ | |
| 374 PhotoImport.prototype.onSelectAllNone_ = function() { | |
| 375 var sm = this.grid_.selectionModel; | |
| 376 if (sm.selectedIndexes.length == this.fileList_.length) { | |
| 377 sm.unselectAll(); | |
| 378 } else { | |
| 379 sm.selectAll(); | |
| 380 } | |
| 381 }; | |
| 382 | |
| 383 /** | |
| 384 * Show error message. | |
| 385 * @param {string} message Error message. | |
| 386 * @private | |
| 387 */ | |
| 388 PhotoImport.prototype.onError_ = function(message) { | |
| 389 this.importingDialog_.hide(function() { | |
| 390 this.alert_.show(message, | |
| 391 function() { | |
| 392 window.close(); | |
| 393 }); | |
| 394 }.bind(this)); | |
| 395 }; | |
| 396 | |
| 397 /** | |
| 398 * Resize event handler. | |
| 399 * @private | |
| 400 */ | |
| 401 PhotoImport.prototype.onResize_ = function() { | |
| 402 var g = this.grid_; | |
| 403 g.startBatchUpdates(); | |
| 404 setTimeout(function() { | |
| 405 g.columns = 0; | |
| 406 g.redraw(); | |
| 407 g.endBatchUpdates(); | |
| 408 }, 0); | |
| 409 }; | |
| 410 | |
| 411 /** | |
| 412 * @return {Array.<Object>} The list of selected entries. | |
| 413 * @private | |
| 414 */ | |
| 415 PhotoImport.prototype.getSelectedItems_ = function() { | |
| 416 var indexes = this.grid_.selectionModel.selectedIndexes; | |
| 417 var list = []; | |
| 418 for (var i = 0; i < indexes.length; i++) { | |
| 419 list.push(this.fileList_.item(indexes[i])); | |
| 420 } | |
| 421 return list; | |
| 422 }; | |
| 423 | |
| 424 /** | |
| 425 * Event handler for picked items change. | |
| 426 * @private | |
| 427 */ | |
| 428 PhotoImport.prototype.onSelectionChanged_ = function() { | |
| 429 var count = this.grid_.selectionModel.selectedIndexes.length; | |
| 430 this.selectedCount_.textContent = count == 0 ? '' : | |
| 431 count == 1 ? loadTimeData.getString('PHOTO_IMPORT_ONE_SELECTED') : | |
| 432 loadTimeData.getStringF('PHOTO_IMPORT_MANY_SELECTED', count); | |
| 433 this.importButton_.disabled = count == 0 || this.myPhotosDirectory_ == null; | |
| 434 this.selectAllNone_.textContent = loadTimeData.getString( | |
| 435 count == this.fileList_.length && count > 0 ? | |
| 436 'PHOTO_IMPORT_SELECT_NONE' : 'PHOTO_IMPORT_SELECT_ALL'); | |
| 437 }; | |
| 438 | |
| 439 /** | |
| 440 * Event handler for import button click. | |
| 441 * @param {Event} event The event. | |
| 442 * @private | |
| 443 */ | |
| 444 PhotoImport.prototype.onImportClick_ = function(event) { | |
| 445 var entries = this.getSelectedItems_(); | |
| 446 var move = this.dom_.querySelector('#delete-after-checkbox').checked; | |
| 447 this.importingDialog_.show(entries, move); | |
| 448 | |
| 449 this.createDestination_(function() { | |
| 450 var percentage = Math.round(entries.length / this.fileList_.length * 100); | |
| 451 metrics.recordMediumCount('PhotoImport.ImportCount', entries.length); | |
| 452 metrics.recordSmallCount('PhotoImport.ImportPercentage', percentage); | |
| 453 | |
| 454 this.importingDialog_.start(this.destination_); | |
| 455 }.bind(this)); | |
| 456 }; | |
| 457 | |
| 458 /** | |
| 459 * Click event handler for the cancel button. | |
| 460 * @param {Event} event The event. | |
| 461 * @private | |
| 462 */ | |
| 463 PhotoImport.prototype.onCancelClick_ = function(event) { | |
| 464 window.close(); | |
| 465 }; | |
| 466 | |
| 467 /** | |
| 468 * Item in the grid. | |
| 469 * @param {PhotoImport} app Application instance. | |
| 470 * @param {Entry} entry File entry. | |
| 471 * @constructor | |
| 472 */ | |
| 473 function GridItem(app, entry) { | |
| 474 var li = app.document_.createElement('li'); | |
| 475 li.__proto__ = GridItem.prototype; | |
| 476 app.decorateGridItem_(li, entry); | |
| 477 return li; | |
| 478 } | |
| 479 | |
| 480 GridItem.prototype = { | |
| 481 __proto__: cr.ui.ListItem.prototype, | |
| 482 get label() {}, | |
| 483 set label(value) {} | |
| 484 }; | |
| 485 | |
| 486 /** | |
| 487 * Creates a selection controller that is to be used with grid. | |
| 488 * @param {cr.ui.ListSelectionModel} selectionModel The selection model to | |
| 489 * interact with. | |
| 490 * @param {cr.ui.Grid} grid The grid to interact with. | |
| 491 * @constructor | |
| 492 * @extends {!cr.ui.ListSelectionController} | |
| 493 */ | |
| 494 function GridSelectionController(selectionModel, grid) { | |
| 495 this.selectionModel_ = selectionModel; | |
| 496 this.grid_ = grid; | |
| 497 } | |
| 498 | |
| 499 /** | |
| 500 * Extends cr.ui.ListSelectionController. | |
| 501 */ | |
| 502 GridSelectionController.prototype.__proto__ = | |
| 503 cr.ui.ListSelectionController.prototype; | |
| 504 | |
| 505 /** @override */ | |
| 506 GridSelectionController.prototype.getIndexBelow = function(index) { | |
| 507 if (index == this.getLastIndex()) { | |
| 508 return -1; | |
| 509 } | |
| 510 | |
| 511 var dm = this.grid_.dataModel; | |
| 512 var columns = this.grid_.columns; | |
| 513 var min = (Math.floor(index / columns) + 1) * columns; | |
| 514 | |
| 515 for (var row = 1; true; row++) { | |
| 516 var end = index + columns * row; | |
| 517 var start = Math.max(min, index + columns * (row - 1)); | |
| 518 if (start > dm.length) break; | |
| 519 | |
| 520 for (var i = end; i > start; i--) { | |
| 521 if (i < dm.length && dm.item(i).type == 'entry') | |
| 522 return i; | |
| 523 } | |
| 524 } | |
| 525 | |
| 526 return this.getLastIndex(); | |
| 527 }; | |
| 528 | |
| 529 /** @override */ | |
| 530 GridSelectionController.prototype.getIndexAbove = function(index) { | |
| 531 if (index == this.getFirstIndex()) { | |
| 532 return -1; | |
| 533 } | |
| 534 | |
| 535 var dm = this.grid_.dataModel; | |
| 536 index -= this.grid_.columns; | |
| 537 while (index >= 0 && dm.item(index).type != 'entry') { | |
| 538 index--; | |
| 539 } | |
| 540 | |
| 541 return index < 0 ? this.getFirstIndex() : index; | |
| 542 }; | |
| 543 | |
| 544 /** @override */ | |
| 545 GridSelectionController.prototype.getIndexBefore = function(index) { | |
| 546 var dm = this.grid_.dataModel; | |
| 547 index--; | |
| 548 while (index >= 0 && dm.item(index).type != 'entry') { | |
| 549 index--; | |
| 550 } | |
| 551 return index; | |
| 552 }; | |
| 553 | |
| 554 /** @override */ | |
| 555 GridSelectionController.prototype.getIndexAfter = function(index) { | |
| 556 var dm = this.grid_.dataModel; | |
| 557 index++; | |
| 558 while (index < dm.length && dm.item(index).type != 'entry') { | |
| 559 index++; | |
| 560 } | |
| 561 return index == dm.length ? -1 : index; | |
| 562 }; | |
| 563 | |
| 564 /** @override */ | |
| 565 GridSelectionController.prototype.getFirstIndex = function() { | |
| 566 var dm = this.grid_.dataModel; | |
| 567 for (var index = 0; index < dm.length; index++) { | |
| 568 if (dm.item(index).type == 'entry') | |
| 569 return index; | |
| 570 } | |
| 571 return -1; | |
| 572 }; | |
| 573 | |
| 574 /** @override */ | |
| 575 GridSelectionController.prototype.getLastIndex = function() { | |
| 576 var dm = this.grid_.dataModel; | |
| 577 for (var index = dm.length - 1; index >= 0; index--) { | |
| 578 if (dm.item(index).type == 'entry') | |
| 579 return index; | |
| 580 } | |
| 581 return -1; | |
| 582 }; | |
| OLD | NEW |