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