Home | History | Annotate | Download | only in js
      1 // Copyright (c) 2013 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 /**
      6  * Model for the folder shortcuts. This object is cr.ui.ArrayDataModel-like
      7  * object with additional methods for the folder shortcut feature.
      8  * This uses chrome.storage as backend. Items are always sorted by file path.
      9  *
     10  * @constructor
     11  * @extends {cr.EventTarget}
     12  */
     13 function FolderShortcutsDataModel() {
     14   this.array_ = [];
     15 
     16   /**
     17    * Eliminate unsupported folders from the list.
     18    *
     19    * @param {Array.<string>} array Folder array which may contain the
     20    *     unsupported folders.
     21    * @return {Array.<string>} Folder list without unsupported folder.
     22    */
     23   var filter = function(array) {
     24     return array.filter(PathUtil.isEligibleForFolderShortcut);
     25   };
     26 
     27   // Loads the contents from the storage to initialize the array.
     28   chrome.storage.sync.get(FolderShortcutsDataModel.NAME, function(value) {
     29     if (!(FolderShortcutsDataModel.NAME in value))
     30       return;
     31 
     32     // Since the value comes from outer resource, we have to check it just in
     33     // case.
     34     var list = value[FolderShortcutsDataModel.NAME];
     35     if (list instanceof Array) {
     36       list = filter(list);
     37 
     38       var permutation = this.calculatePermitation_(this.array_, list);
     39       this.array_ = list;
     40       this.firePermutedEvent_(permutation);
     41     }
     42   }.bind(this));
     43 
     44   // Listening for changes in the storage.
     45   chrome.storage.onChanged.addListener(function(changes, namespace) {
     46     if (!(FolderShortcutsDataModel.NAME in changes) || namespace != 'sync')
     47       return;
     48 
     49     var list = changes[FolderShortcutsDataModel.NAME].newValue;
     50     // Since the value comes from outer resource, we have to check it just in
     51     // case.
     52     if (list instanceof Array) {
     53       list = filter(list);
     54 
     55       // If the list is not changed, do nothing and just return.
     56       if (this.array_.length == list.length) {
     57         var changed = false;
     58         for (var i = 0; i < this.array_.length; i++) {
     59           // Same item check: must be exact match.
     60           if (this.array_[i] != list[i]) {
     61             changed = true;
     62             break;
     63           }
     64         }
     65         if (!changed)
     66           return;
     67       }
     68 
     69       var permutation = this.calculatePermitation_(this.array_, list);
     70       this.array_ = list;
     71       this.firePermutedEvent_(permutation);
     72     }
     73   }.bind(this));
     74 }
     75 
     76 /**
     77  * Key name in chrome.storage. The array are stored with this name.
     78  * @type {string}
     79  * @const
     80  */
     81 FolderShortcutsDataModel.NAME = 'folder-shortcuts-list';
     82 
     83 FolderShortcutsDataModel.prototype = {
     84   __proto__: cr.EventTarget.prototype,
     85 
     86   /**
     87    * @return {number} Number of elements in the array.
     88    */
     89   get length() {
     90     return this.array_.length;
     91   },
     92 
     93   /**
     94    * @param {number} index Index of the element to be retrieved.
     95    * @return {string} The value of the |index|-th element.
     96    */
     97   item: function(index) {
     98     return this.array_[index];
     99   },
    100 
    101   /**
    102    * @param {string} value Value of the element to be retrieved.
    103    * @return {number} Index of the element with the specified |value|.
    104    */
    105   getIndex: function(value) {
    106     for (var i = 0; i < this.length; i++) {
    107       // Same item check: must be exact match.
    108       if (this.array_[i] == value) {
    109         return i;
    110       }
    111     }
    112     return -1;
    113   },
    114 
    115   /**
    116    * Compares 2 strings and returns a number indicating one string comes before
    117    * or after or is the same as the other string in sort order.
    118    *
    119    * @param {string} a String1.
    120    * @param {string} b String2.
    121    * @return {boolean} Return -1, if String1 < String2. Return 0, if String1 ==
    122    *     String2. Otherwise, return 1.
    123    */
    124   compare: function(a, b) {
    125     return a.localeCompare(b,
    126                            undefined,  // locale parameter, use default locale.
    127                            {usage: 'sort', numeric: true});
    128   },
    129 
    130   /**
    131    * Adds the given item to the array. If there were already same item in the
    132    * list, return the index of the existing item without adding a duplicate
    133    * item.
    134    *
    135    * @param {string} value Value to be added into the array.
    136    * @return {number} Index in the list which the element added to.
    137    */
    138   add: function(value) {
    139     var oldArray = this.array_.slice(0);  // Shallow copy.
    140     var addedIndex = -1;
    141     for (var i = 0; i < this.length; i++) {
    142       // Same item check: must be exact match.
    143       if (this.array_[i] == value)
    144         return i;
    145 
    146       // Since the array is sorted, new item will be added just before the first
    147       // larger item.
    148       if (this.compare(this.array_[i], value) >= 0) {
    149         this.array_.splice(i, 0, value);
    150         addedIndex = i;
    151         break;
    152       }
    153     }
    154     // If value is not added yet, add it at the last.
    155     if (addedIndex == -1) {
    156       this.array_.push(value);
    157       addedIndex = this.length;
    158     }
    159 
    160     this.firePermutedEvent_(
    161         this.calculatePermitation_(oldArray, this.array_));
    162     this.save_();
    163     return addedIndex;
    164   },
    165 
    166   /**
    167    * Removes the given item from the array.
    168    * @param {string} value Value to be removed from the array.
    169    * @return {number} Index in the list which the element removed from.
    170    */
    171   remove: function(value) {
    172     var removedIndex = -1;
    173     var oldArray = this.array_.slice(0);  // Shallow copy.
    174     for (var i = 0; i < this.length; i++) {
    175       // Same item check: must be exact match.
    176       if (this.array_[i] == value) {
    177         this.array_.splice(i, 1);
    178         removedIndex = i;
    179         break;
    180       }
    181     }
    182 
    183     if (removedIndex != -1) {
    184       this.firePermutedEvent_(
    185           this.calculatePermitation_(oldArray, this.array_));
    186       this.save_();
    187       return removedIndex;
    188     }
    189 
    190     // No item is removed.
    191     return -1;
    192   },
    193 
    194   /**
    195    * @param {string} path Path to be checked.
    196    * @return {boolean} True if the given |path| exists in the array. False
    197    *     otherwise.
    198    */
    199   exists: function(path) {
    200     var index = this.getIndex(path);
    201     return (index >= 0);
    202   },
    203 
    204   /**
    205    * Saves the current array to chrome.storage.
    206    * @private
    207    */
    208   save_: function() {
    209     var obj = {};
    210     obj[FolderShortcutsDataModel.NAME] = this.array_;
    211     chrome.storage.sync.set(obj, function() {});
    212   },
    213 
    214   /**
    215    * Creates a permutation array for 'permuted' event, which is compatible with
    216    * a parmutation array used in cr/ui/array_data_model.js.
    217    *
    218    * @param {array} oldArray Previous array before changing.
    219    * @param {array} newArray New array after changing.
    220    * @return {Array.<number>} Created permutation array.
    221    * @private
    222    */
    223   calculatePermitation_: function(oldArray, newArray) {
    224     var oldIndex = 0;  // Index of oldArray.
    225     var newIndex = 0;  // Index of newArray.
    226 
    227     // Note that both new and old arrays are sorted.
    228     var permutation = [];
    229     for (; oldIndex < oldArray.length; oldIndex++) {
    230       if (newIndex >= newArray.length) {
    231         // oldArray[oldIndex] is deleted, which is not in the new array.
    232         permutation[oldIndex] = -1;
    233         continue;
    234       }
    235 
    236       while (newIndex < newArray.length) {
    237         // Unchanged item, which exists in both new and old array. But the
    238         // index may be changed.
    239         if (oldArray[oldIndex] == newArray[newIndex]) {
    240           permutation[oldIndex] = newIndex;
    241           newIndex++;
    242           break;
    243         }
    244 
    245         // oldArray[oldIndex] is deleted, which is not in the new array.
    246         if (this.compare(oldArray[oldIndex], newArray[newIndex]) < 0) {
    247           permutation[oldIndex] = -1;
    248           break;
    249         }
    250 
    251         // In the case of this.compare(oldArray[oldIndex]) > 0:
    252         // newArray[newIndex] is added, which is not in the old array.
    253         newIndex++;
    254       }
    255     }
    256     return permutation;
    257   },
    258 
    259   /**
    260    * Fires a 'permuted' event, which is compatible with cr.ui.ArrayDataModel.
    261    * @param {Array.<number>} Permutation array.
    262    */
    263   firePermutedEvent_: function(permutation) {
    264     var permutedEvent = new Event('permuted');
    265     permutedEvent.newLength = this.length;
    266     permutedEvent.permutation = permutation;
    267     this.dispatchEvent(permutedEvent);
    268 
    269     // Note: This model only fires 'permuted' event, because:
    270     // 1) 'change' event is not necessary to fire since it is covered by
    271     //    'permuted' event.
    272     // 2) 'splice' and 'sorted' events are not implemented. These events are
    273     //    not used in NavigationListModel. We have to implement them when
    274     //    necessary.
    275   }
    276 };
    277