Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | "use strict"; |
michael@0 | 6 | |
michael@0 | 7 | this.EXPORTED_SYMBOLS = ["PlacesTransactions"]; |
michael@0 | 8 | |
michael@0 | 9 | /** |
michael@0 | 10 | * Overview |
michael@0 | 11 | * -------- |
michael@0 | 12 | * This modules serves as the transactions manager for Places, and implements |
michael@0 | 13 | * all the standard transactions for its UI commands (creating items, editing |
michael@0 | 14 | * various properties, etc.). It shares most of its semantics with common |
michael@0 | 15 | * command pattern implementations, the HTML5 Undo Manager in particular. |
michael@0 | 16 | * However, the asynchronous design of [future] Places APIs, combined with the |
michael@0 | 17 | * commitment to serialize all UI operations, makes things a little bit |
michael@0 | 18 | * different. For example, when |undo| is called in order to undo the top undo |
michael@0 | 19 | * entry, the caller cannot tell for sure what entry would it be because the |
michael@0 | 20 | * execution of some transaction is either in process, or queued. |
michael@0 | 21 | * |
michael@0 | 22 | * GUIDs and item-ids |
michael@0 | 23 | * ------------------- |
michael@0 | 24 | * The Bookmarks API still relies heavily on item-ids, but since those do not |
michael@0 | 25 | * play nicely with the concept of undo and redo (especially not in an |
michael@0 | 26 | * asynchronous environment), this API only accepts bookmark GUIDs, both for |
michael@0 | 27 | * input (e.g. for specifying the parent folder for a new bookmark) and for |
michael@0 | 28 | * output (when the GUID for such a bookmark is propagated). |
michael@0 | 29 | * |
michael@0 | 30 | * GUIDs are readily available when dealing with the "output" of this API and |
michael@0 | 31 | * when result nodes are used (see nsINavHistoryResultNode::bookmarkGUID). |
michael@0 | 32 | * If you only have item-ids in hand, use PlacesUtils.promiseItemGUID for |
michael@0 | 33 | * converting them. Should you need to convert them back into itemIds, use |
michael@0 | 34 | * PlacesUtils.promiseItemId. |
michael@0 | 35 | * |
michael@0 | 36 | * The Standard Transactions |
michael@0 | 37 | * ------------------------- |
michael@0 | 38 | * At the bottom of this module you will find implementations for all Places UI |
michael@0 | 39 | * commands (One should almost never fallback to raw Places APIs. Please file |
michael@0 | 40 | * a bug if you find anything uncovered). The transactions' constructors are |
michael@0 | 41 | * set on the PlacesTransactions object (e.g. PlacesTransactions.NewFolder). |
michael@0 | 42 | * The input for this constructors is taken in the form of a single argument |
michael@0 | 43 | * plain object. Input properties may be either required (e.g. the |keyword| |
michael@0 | 44 | * property for the EditKeyword transaction) or optional (e.g. the |keyword| |
michael@0 | 45 | * property for NewBookmark). Once a transaction is created, you may pass it |
michael@0 | 46 | * to |transact| or use it in the for batching (see next section). |
michael@0 | 47 | * |
michael@0 | 48 | * The constructors throw right away when any required input is missing or when |
michael@0 | 49 | * some input is invalid "on the surface" (e.g. GUID values are validated to be |
michael@0 | 50 | * 12-characters strings, but are not validated to point to existing item. Such |
michael@0 | 51 | * an error will reveal when the transaction is executed). |
michael@0 | 52 | * |
michael@0 | 53 | * To make things simple, a given input property has the same basic meaning and |
michael@0 | 54 | * valid values across all transactions which accept it in the input object. |
michael@0 | 55 | * Here is a list of all supported input properties along with their expected |
michael@0 | 56 | * values: |
michael@0 | 57 | * - uri: an nsIURI object. |
michael@0 | 58 | * - feedURI: an nsIURI object, holding the url for a live bookmark. |
michael@0 | 59 | * - siteURI: an nsIURI object, holding the url for the site with which |
michael@0 | 60 | * a live bookmark is associated. |
michael@0 | 61 | * - GUID, parentGUID, newParentGUID: a valid places GUID string. |
michael@0 | 62 | * - title: a string |
michael@0 | 63 | * - index, newIndex: the position of an item in its containing folder, |
michael@0 | 64 | * starting from 0. |
michael@0 | 65 | * integer and PlacesUtils.bookmarks.DEFAULT_INDEX |
michael@0 | 66 | * - annotationObject: see PlacesUtils.setAnnotationsForItem |
michael@0 | 67 | * - annotations: an array of annotation objects as above. |
michael@0 | 68 | * - tags: an array of strings. |
michael@0 | 69 | * |
michael@0 | 70 | * Batching transactions |
michael@0 | 71 | * --------------------- |
michael@0 | 72 | * Sometimes it is useful to "batch" or "merge" transactions. For example, |
michael@0 | 73 | * "Bookmark All Tabs" may be implemented as one NewFolder transaction followed |
michael@0 | 74 | * by numerous NewBookmark transactions - all to be undone or redone in a single |
michael@0 | 75 | * command. The |transact| method makes this possible using a generator |
michael@0 | 76 | * function as an input. These generators have the same semantics as in |
michael@0 | 77 | * Task.jsm except that when you yield a transaction, it's executed, and the |
michael@0 | 78 | * resolution (e.g. the new bookmark GUID) is sent to the generator so you can |
michael@0 | 79 | * use it as the input for another transaction. See |transact| for the details. |
michael@0 | 80 | * |
michael@0 | 81 | * "Custom" transactions |
michael@0 | 82 | * --------------------- |
michael@0 | 83 | * In the legacy transactions API it was possible to pass-in transactions |
michael@0 | 84 | * implemented "externally". For various reason this isn't allowed anymore: |
michael@0 | 85 | * transact throws right away if one attempts to pass a transaction that was not |
michael@0 | 86 | * created by this module. However, it's almost always possible to achieve the |
michael@0 | 87 | * same functionality with the batching technique described above. |
michael@0 | 88 | * |
michael@0 | 89 | * The transactions-history structure |
michael@0 | 90 | * ---------------------------------- |
michael@0 | 91 | * The transactions-history is a two-dimensional stack of transactions: the |
michael@0 | 92 | * transactions are ordered in reverse to the order they were committed. |
michael@0 | 93 | * It's two-dimensional because the undo manager allows batching transactions |
michael@0 | 94 | * together for the purpose of undo or redo (batched transactions can never be |
michael@0 | 95 | * undone or redone partially). |
michael@0 | 96 | * |
michael@0 | 97 | * The undoPosition property is set to the index of the top entry. If there is |
michael@0 | 98 | * no entry at that index, there is nothing to undo. |
michael@0 | 99 | * Entries prior to undoPosition, if any, are redo entries, the first one being |
michael@0 | 100 | * the top redo entry. |
michael@0 | 101 | * |
michael@0 | 102 | * [ [2nd redo txn, 1st redo txn], <= 2nd redo entry |
michael@0 | 103 | * [2nd redo txn, 1st redo txn], <= 1st redo entry |
michael@0 | 104 | * [1st undo txn, 2nd undo txn], <= 1st undo entry |
michael@0 | 105 | * [1st undo txn, 2nd undo txn] <= 2nd undo entry ] |
michael@0 | 106 | * undoPostion: 2. |
michael@0 | 107 | * |
michael@0 | 108 | * Note that when a new entry is created, all redo entries are removed. |
michael@0 | 109 | */ |
michael@0 | 110 | |
michael@0 | 111 | Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 112 | Components.utils.import("resource://gre/modules/Services.jsm"); |
michael@0 | 113 | XPCOMUtils.defineLazyModuleGetter(this, "Promise", |
michael@0 | 114 | "resource://gre/modules/Promise.jsm"); |
michael@0 | 115 | XPCOMUtils.defineLazyModuleGetter(this, "Task", |
michael@0 | 116 | "resource://gre/modules/Task.jsm"); |
michael@0 | 117 | XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", |
michael@0 | 118 | "resource://gre/modules/NetUtil.jsm"); |
michael@0 | 119 | XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", |
michael@0 | 120 | "resource://gre/modules/PlacesUtils.jsm"); |
michael@0 | 121 | XPCOMUtils.defineLazyModuleGetter(this, "console", |
michael@0 | 122 | "resource://gre/modules/devtools/Console.jsm"); |
michael@0 | 123 | |
michael@0 | 124 | // Updates commands in the undo group of the active window commands. |
michael@0 | 125 | // Inactive windows commands will be updated on focus. |
michael@0 | 126 | function updateCommandsOnActiveWindow() { |
michael@0 | 127 | // Updating "undo" will cause a group update including "redo". |
michael@0 | 128 | try { |
michael@0 | 129 | let win = Services.focus.activeWindow; |
michael@0 | 130 | if (win) |
michael@0 | 131 | win.updateCommands("undo"); |
michael@0 | 132 | } |
michael@0 | 133 | catch(ex) { console.error(ex, "Couldn't update undo commands"); } |
michael@0 | 134 | } |
michael@0 | 135 | |
michael@0 | 136 | // The internal object for managing the transactions history. |
michael@0 | 137 | // The public API is included in PlacesTransactions. |
michael@0 | 138 | // TODO bug 982099: extending the array "properly" makes it painful to implement |
michael@0 | 139 | // getters. If/when ES6 gets proper array subclassing we can revise this. |
michael@0 | 140 | let TransactionsHistory = []; |
michael@0 | 141 | TransactionsHistory.__proto__ = { |
michael@0 | 142 | __proto__: Array.prototype, |
michael@0 | 143 | |
michael@0 | 144 | // The index of the first undo entry (if any) - See the documentation |
michael@0 | 145 | // at the top of this file. |
michael@0 | 146 | _undoPosition: 0, |
michael@0 | 147 | get undoPosition() this._undoPosition, |
michael@0 | 148 | |
michael@0 | 149 | // Handy shortcuts |
michael@0 | 150 | get topUndoEntry() this.undoPosition < this.length ? |
michael@0 | 151 | this[this.undoPosition] : null, |
michael@0 | 152 | get topRedoEntry() this.undoPosition > 0 ? |
michael@0 | 153 | this[this.undoPosition - 1] : null, |
michael@0 | 154 | |
michael@0 | 155 | // Outside of this module, the API of transactions is inaccessible, and so |
michael@0 | 156 | // are any internal properties. To achieve that, transactions are proxified |
michael@0 | 157 | // in their constructors. This maps the proxies to their respective raw |
michael@0 | 158 | // objects. |
michael@0 | 159 | proxifiedToRaw: new WeakMap(), |
michael@0 | 160 | |
michael@0 | 161 | /** |
michael@0 | 162 | * Proxify a transaction object for consumers. |
michael@0 | 163 | * @param aRawTransaction |
michael@0 | 164 | * the raw transaction object. |
michael@0 | 165 | * @return the proxified transaction object. |
michael@0 | 166 | * @see getRawTransaction for retrieving the raw transaction. |
michael@0 | 167 | */ |
michael@0 | 168 | proxifyTransaction: function (aRawTransaction) { |
michael@0 | 169 | let proxy = Object.freeze({}); |
michael@0 | 170 | this.proxifiedToRaw.set(proxy, aRawTransaction); |
michael@0 | 171 | return proxy; |
michael@0 | 172 | }, |
michael@0 | 173 | |
michael@0 | 174 | /** |
michael@0 | 175 | * Check if the given object is a the proxy object for some transaction. |
michael@0 | 176 | * @param aValue |
michael@0 | 177 | * any JS value. |
michael@0 | 178 | * @return true if aValue is the proxy object for some transaction, false |
michael@0 | 179 | * otherwise. |
michael@0 | 180 | */ |
michael@0 | 181 | isProxifiedTransactionObject: |
michael@0 | 182 | function (aValue) this.proxifiedToRaw.has(aValue), |
michael@0 | 183 | |
michael@0 | 184 | /** |
michael@0 | 185 | * Get the raw transaction for the given proxy. |
michael@0 | 186 | * @param aProxy |
michael@0 | 187 | * the proxy object |
michael@0 | 188 | * @return the transaction proxified by aProxy; |undefined| is returned if |
michael@0 | 189 | * aProxy is not a proxified transaction. |
michael@0 | 190 | */ |
michael@0 | 191 | getRawTransaction: function (aProxy) this.proxifiedToRaw.get(aProxy), |
michael@0 | 192 | |
michael@0 | 193 | /** |
michael@0 | 194 | * Undo the top undo entry, if any, and update the undo position accordingly. |
michael@0 | 195 | */ |
michael@0 | 196 | undo: function* () { |
michael@0 | 197 | let entry = this.topUndoEntry; |
michael@0 | 198 | if (!entry) |
michael@0 | 199 | return; |
michael@0 | 200 | |
michael@0 | 201 | for (let transaction of entry) { |
michael@0 | 202 | try { |
michael@0 | 203 | yield TransactionsHistory.getRawTransaction(transaction).undo(); |
michael@0 | 204 | } |
michael@0 | 205 | catch(ex) { |
michael@0 | 206 | // If one transaction is broken, it's not safe to work with any other |
michael@0 | 207 | // undo entry. Report the error and clear the undo history. |
michael@0 | 208 | console.error(ex, |
michael@0 | 209 | "Couldn't undo a transaction, clearing all undo entries."); |
michael@0 | 210 | this.clearUndoEntries(); |
michael@0 | 211 | return; |
michael@0 | 212 | } |
michael@0 | 213 | } |
michael@0 | 214 | this._undoPosition++; |
michael@0 | 215 | updateCommandsOnActiveWindow(); |
michael@0 | 216 | }, |
michael@0 | 217 | |
michael@0 | 218 | /** |
michael@0 | 219 | * Redo the top redo entry, if any, and update the undo position accordingly. |
michael@0 | 220 | */ |
michael@0 | 221 | redo: function* () { |
michael@0 | 222 | let entry = this.topRedoEntry; |
michael@0 | 223 | if (!entry) |
michael@0 | 224 | return; |
michael@0 | 225 | |
michael@0 | 226 | for (let i = entry.length - 1; i >= 0; i--) { |
michael@0 | 227 | let transaction = TransactionsHistory.getRawTransaction(entry[i]); |
michael@0 | 228 | try { |
michael@0 | 229 | if (transaction.redo) |
michael@0 | 230 | yield transaction.redo(); |
michael@0 | 231 | else |
michael@0 | 232 | yield transaction.execute(); |
michael@0 | 233 | } |
michael@0 | 234 | catch(ex) { |
michael@0 | 235 | // If one transaction is broken, it's not safe to work with any other |
michael@0 | 236 | // redo entry. Report the error and clear the undo history. |
michael@0 | 237 | console.error(ex, |
michael@0 | 238 | "Couldn't redo a transaction, clearing all redo entries."); |
michael@0 | 239 | this.clearRedoEntries(); |
michael@0 | 240 | return; |
michael@0 | 241 | } |
michael@0 | 242 | } |
michael@0 | 243 | this._undoPosition--; |
michael@0 | 244 | updateCommandsOnActiveWindow(); |
michael@0 | 245 | }, |
michael@0 | 246 | |
michael@0 | 247 | /** |
michael@0 | 248 | * Add a transaction either as a new entry, if forced or if there are no undo |
michael@0 | 249 | * entries, or to the top undo entry. |
michael@0 | 250 | * |
michael@0 | 251 | * @param aProxifiedTransaction |
michael@0 | 252 | * the proxified transaction object to be added to the transaction |
michael@0 | 253 | * history. |
michael@0 | 254 | * @param [optional] aForceNewEntry |
michael@0 | 255 | * Force a new entry for the transaction. Default: false. |
michael@0 | 256 | * If false, an entry will we created only if there's no undo entry |
michael@0 | 257 | * to extend. |
michael@0 | 258 | */ |
michael@0 | 259 | add: function (aProxifiedTransaction, aForceNewEntry = false) { |
michael@0 | 260 | if (!this.isProxifiedTransactionObject(aProxifiedTransaction)) |
michael@0 | 261 | throw new Error("aProxifiedTransaction is not a proxified transaction"); |
michael@0 | 262 | |
michael@0 | 263 | if (this.length == 0 || aForceNewEntry) { |
michael@0 | 264 | this.clearRedoEntries(); |
michael@0 | 265 | this.unshift([aProxifiedTransaction]); |
michael@0 | 266 | } |
michael@0 | 267 | else { |
michael@0 | 268 | this[this.undoPosition].unshift(aProxifiedTransaction); |
michael@0 | 269 | } |
michael@0 | 270 | updateCommandsOnActiveWindow(); |
michael@0 | 271 | }, |
michael@0 | 272 | |
michael@0 | 273 | /** |
michael@0 | 274 | * Clear all undo entries. |
michael@0 | 275 | */ |
michael@0 | 276 | clearUndoEntries: function () { |
michael@0 | 277 | if (this.undoPosition < this.length) |
michael@0 | 278 | this.splice(this.undoPosition); |
michael@0 | 279 | }, |
michael@0 | 280 | |
michael@0 | 281 | /** |
michael@0 | 282 | * Clear all redo entries. |
michael@0 | 283 | */ |
michael@0 | 284 | clearRedoEntries: function () { |
michael@0 | 285 | if (this.undoPosition > 0) { |
michael@0 | 286 | this.splice(0, this.undoPosition); |
michael@0 | 287 | this._undoPosition = 0; |
michael@0 | 288 | } |
michael@0 | 289 | }, |
michael@0 | 290 | |
michael@0 | 291 | /** |
michael@0 | 292 | * Clear all entries. |
michael@0 | 293 | */ |
michael@0 | 294 | clearAllEntries: function () { |
michael@0 | 295 | if (this.length > 0) { |
michael@0 | 296 | this.splice(0); |
michael@0 | 297 | this._undoPosition = 0; |
michael@0 | 298 | } |
michael@0 | 299 | } |
michael@0 | 300 | }; |
michael@0 | 301 | |
michael@0 | 302 | |
michael@0 | 303 | // Our transaction manager is asynchronous in the sense that all of its methods |
michael@0 | 304 | // don't execute synchronously. However, all actions must be serialized. |
michael@0 | 305 | let currentTask = Promise.resolve(); |
michael@0 | 306 | function Serialize(aTask) { |
michael@0 | 307 | // Ignore failures. |
michael@0 | 308 | return currentTask = currentTask.then( () => Task.spawn(aTask) ) |
michael@0 | 309 | .then(null, Components.utils.reportError); |
michael@0 | 310 | } |
michael@0 | 311 | |
michael@0 | 312 | // Transactions object should never be recycled (that is, |execute| should |
michael@0 | 313 | // only be called once, or not at all, after they're constructed. |
michael@0 | 314 | // This keeps track of all transactions which were executed. |
michael@0 | 315 | let executedTransactions = new WeakMap(); // TODO: use WeakSet (bug 792439) |
michael@0 | 316 | executedTransactions.add = k => executedTransactions.set(k, null); |
michael@0 | 317 | |
michael@0 | 318 | let PlacesTransactions = { |
michael@0 | 319 | /** |
michael@0 | 320 | * Asynchronously transact either a single transaction, or a sequence of |
michael@0 | 321 | * transactions that would be treated as a single entry in the transactions |
michael@0 | 322 | * history. |
michael@0 | 323 | * |
michael@0 | 324 | * @param aToTransact |
michael@0 | 325 | * Either a transaction object or a generator function (ES6-style only) |
michael@0 | 326 | * that yields transaction objects. |
michael@0 | 327 | * |
michael@0 | 328 | * Generator mode how-to: when a transaction is yielded, it's executed. |
michael@0 | 329 | * Then, if it was executed successfully, the resolution of |execute| |
michael@0 | 330 | * is sent to the generator. If |execute| threw or rejected, the |
michael@0 | 331 | * exception is propagated to the generator. |
michael@0 | 332 | * Any other value yielded by a generator function is handled the |
michael@0 | 333 | * same way as in a Task (see Task.jsm). |
michael@0 | 334 | * |
michael@0 | 335 | * @return {Promise} |
michael@0 | 336 | * @resolves either to the resolution of |execute|, in single-transaction mode, |
michael@0 | 337 | * or to the return value of the generator, in generator-mode. |
michael@0 | 338 | * @rejects either if |execute| threw, in single-transaction mode, or if |
michael@0 | 339 | * the generator function threw (or didn't handle) an exception, in generator |
michael@0 | 340 | * mode. |
michael@0 | 341 | * @throws if aTransactionOrGeneratorFunction is neither a transaction object |
michael@0 | 342 | * created by this module or a generator function. |
michael@0 | 343 | * @note If no transaction was executed successfully, the transactions history |
michael@0 | 344 | * is not affected. |
michael@0 | 345 | * |
michael@0 | 346 | * @note All PlacesTransactions operations are serialized. This means that the |
michael@0 | 347 | * transactions history state may change by the time the transaction/generator |
michael@0 | 348 | * is processed. It's guaranteed, however, that a generator function "blocks" |
michael@0 | 349 | * the queue: that is, it is assured that no other operations are performed |
michael@0 | 350 | * by or on PlacesTransactions until the generator returns. Keep in mind you |
michael@0 | 351 | * are not protected from consumers who use the raw places APIs directly. |
michael@0 | 352 | */ |
michael@0 | 353 | transact: function (aToTransact) { |
michael@0 | 354 | let isGeneratorObj = |
michael@0 | 355 | o => Object.prototype.toString.call(o) == "[object Generator]"; |
michael@0 | 356 | |
michael@0 | 357 | let generator = null; |
michael@0 | 358 | if (typeof(aToTransact) == "function") { |
michael@0 | 359 | generator = aToTransact(); |
michael@0 | 360 | if (!isGeneratorObj(generator)) |
michael@0 | 361 | throw new Error("aToTransact is not a generator function"); |
michael@0 | 362 | } |
michael@0 | 363 | else if (!TransactionsHistory.isProxifiedTransactionObject(aToTransact)) { |
michael@0 | 364 | throw new Error("aToTransact is not a valid transaction object"); |
michael@0 | 365 | } |
michael@0 | 366 | else if (executedTransactions.has(aToTransact)) { |
michael@0 | 367 | throw new Error("Transactions objects may not be recycled."); |
michael@0 | 368 | } |
michael@0 | 369 | |
michael@0 | 370 | return Serialize(function* () { |
michael@0 | 371 | // The entry in the transactions history is created once the first |
michael@0 | 372 | // transaction is committed. This means that if |transact| is called |
michael@0 | 373 | // in its "generator mode" and no transactions are committed by the |
michael@0 | 374 | // generator, the transactions history is left unchanged. |
michael@0 | 375 | // Bug 982115: Depending on how this API is actually used we may revise |
michael@0 | 376 | // this decision and make it so |transact| always forces a new entry. |
michael@0 | 377 | let forceNewEntry = true; |
michael@0 | 378 | function* transactOneTransaction(aTransaction) { |
michael@0 | 379 | let retval = |
michael@0 | 380 | yield TransactionsHistory.getRawTransaction(aTransaction).execute(); |
michael@0 | 381 | executedTransactions.add(aTransaction); |
michael@0 | 382 | TransactionsHistory.add(aTransaction, forceNewEntry); |
michael@0 | 383 | forceNewEntry = false; |
michael@0 | 384 | return retval; |
michael@0 | 385 | } |
michael@0 | 386 | |
michael@0 | 387 | function* transactBatch(aGenerator) { |
michael@0 | 388 | let error = false; |
michael@0 | 389 | let sendValue = undefined; |
michael@0 | 390 | while (true) { |
michael@0 | 391 | let next = error ? |
michael@0 | 392 | aGenerator.throw(sendValue) : aGenerator.next(sendValue); |
michael@0 | 393 | sendValue = next.value; |
michael@0 | 394 | if (isGeneratorObj(sendValue)) { |
michael@0 | 395 | sendValue = yield transactBatch(sendValue); |
michael@0 | 396 | } |
michael@0 | 397 | else if (typeof(sendValue) == "object" && sendValue) { |
michael@0 | 398 | if (TransactionsHistory.isProxifiedTransactionObject(sendValue)) { |
michael@0 | 399 | if (executedTransactions.has(sendValue)) { |
michael@0 | 400 | sendValue = new Error("Transactions may not be recycled."); |
michael@0 | 401 | error = true; |
michael@0 | 402 | } |
michael@0 | 403 | else { |
michael@0 | 404 | sendValue = yield transactOneTransaction(sendValue); |
michael@0 | 405 | } |
michael@0 | 406 | } |
michael@0 | 407 | else if ("then" in sendValue) { |
michael@0 | 408 | sendValue = yield sendValue; |
michael@0 | 409 | } |
michael@0 | 410 | } |
michael@0 | 411 | if (next.done) |
michael@0 | 412 | break; |
michael@0 | 413 | } |
michael@0 | 414 | return sendValue; |
michael@0 | 415 | } |
michael@0 | 416 | |
michael@0 | 417 | if (generator) |
michael@0 | 418 | return yield transactBatch(generator); |
michael@0 | 419 | else |
michael@0 | 420 | return yield transactOneTransaction(aToTransact); |
michael@0 | 421 | }.bind(this)); |
michael@0 | 422 | }, |
michael@0 | 423 | |
michael@0 | 424 | /** |
michael@0 | 425 | * Asynchronously undo the transaction immediately after the current undo |
michael@0 | 426 | * position in the transactions history in the reverse order, if any, and |
michael@0 | 427 | * adjusts the undo position. |
michael@0 | 428 | * |
michael@0 | 429 | * @return {Promises). The promise always resolves. |
michael@0 | 430 | * @note All undo manager operations are queued. This means that transactions |
michael@0 | 431 | * history may change by the time your request is fulfilled. |
michael@0 | 432 | */ |
michael@0 | 433 | undo: function () Serialize(() => TransactionsHistory.undo()), |
michael@0 | 434 | |
michael@0 | 435 | /** |
michael@0 | 436 | * Asynchronously redo the transaction immediately before the current undo |
michael@0 | 437 | * position in the transactions history, if any, and adjusts the undo |
michael@0 | 438 | * position. |
michael@0 | 439 | * |
michael@0 | 440 | * @return {Promises). The promise always resolves. |
michael@0 | 441 | * @note All undo manager operations are queued. This means that transactions |
michael@0 | 442 | * history may change by the time your request is fulfilled. |
michael@0 | 443 | */ |
michael@0 | 444 | redo: function () Serialize(() => TransactionsHistory.redo()), |
michael@0 | 445 | |
michael@0 | 446 | /** |
michael@0 | 447 | * Asynchronously clear the undo, redo, or all entries from the transactions |
michael@0 | 448 | * history. |
michael@0 | 449 | * |
michael@0 | 450 | * @param [optional] aUndoEntries |
michael@0 | 451 | * Whether or not to clear undo entries. Default: true. |
michael@0 | 452 | * @param [optional] aRedoEntries |
michael@0 | 453 | * Whether or not to clear undo entries. Default: true. |
michael@0 | 454 | * |
michael@0 | 455 | * @return {Promises). The promise always resolves. |
michael@0 | 456 | * @throws if both aUndoEntries and aRedoEntries are false. |
michael@0 | 457 | * @note All undo manager operations are queued. This means that transactions |
michael@0 | 458 | * history may change by the time your request is fulfilled. |
michael@0 | 459 | */ |
michael@0 | 460 | clearTransactionsHistory: |
michael@0 | 461 | function (aUndoEntries = true, aRedoEntries = true) { |
michael@0 | 462 | return Serialize(function* () { |
michael@0 | 463 | if (aUndoEntries && aRedoEntries) |
michael@0 | 464 | TransactionsHistory.clearAllEntries(); |
michael@0 | 465 | else if (aUndoEntries) |
michael@0 | 466 | TransactionsHistory.clearUndoEntries(); |
michael@0 | 467 | else if (aRedoEntries) |
michael@0 | 468 | TransactionsHistory.clearRedoEntries(); |
michael@0 | 469 | else |
michael@0 | 470 | throw new Error("either aUndoEntries or aRedoEntries should be true"); |
michael@0 | 471 | }); |
michael@0 | 472 | }, |
michael@0 | 473 | |
michael@0 | 474 | /** |
michael@0 | 475 | * The numbers of entries in the transactions history. |
michael@0 | 476 | */ |
michael@0 | 477 | get length() TransactionsHistory.length, |
michael@0 | 478 | |
michael@0 | 479 | /** |
michael@0 | 480 | * Get the transaction history entry at a given index. Each entry consists |
michael@0 | 481 | * of one or more transaction objects. |
michael@0 | 482 | * |
michael@0 | 483 | * @param aIndex |
michael@0 | 484 | * the index of the entry to retrieve. |
michael@0 | 485 | * @return an array of transaction objects in their undo order (that is, |
michael@0 | 486 | * reversely to the order they were executed). |
michael@0 | 487 | * @throw if aIndex is invalid (< 0 or >= length). |
michael@0 | 488 | * @note the returned array is a clone of the history entry and is not |
michael@0 | 489 | * kept in sync with the original entry if it changes. |
michael@0 | 490 | */ |
michael@0 | 491 | entry: function (aIndex) { |
michael@0 | 492 | if (!Number.isInteger(aIndex) || aIndex < 0 || aIndex >= this.length) |
michael@0 | 493 | throw new Error("Invalid index"); |
michael@0 | 494 | |
michael@0 | 495 | return TransactionsHistory[aIndex]; |
michael@0 | 496 | }, |
michael@0 | 497 | |
michael@0 | 498 | /** |
michael@0 | 499 | * The index of the top undo entry in the transactions history. |
michael@0 | 500 | * If there are no undo entries, it equals to |length|. |
michael@0 | 501 | * Entries past this point |
michael@0 | 502 | * Entries at and past this point are redo entries. |
michael@0 | 503 | */ |
michael@0 | 504 | get undoPosition() TransactionsHistory.undoPosition, |
michael@0 | 505 | |
michael@0 | 506 | /** |
michael@0 | 507 | * Shortcut for accessing the top undo entry in the transaction history. |
michael@0 | 508 | */ |
michael@0 | 509 | get topUndoEntry() TransactionsHistory.topUndoEntry, |
michael@0 | 510 | |
michael@0 | 511 | /** |
michael@0 | 512 | * Shortcut for accessing the top redo entry in the transaction history. |
michael@0 | 513 | */ |
michael@0 | 514 | get topRedoEntry() TransactionsHistory.topRedoEntry |
michael@0 | 515 | }; |
michael@0 | 516 | |
michael@0 | 517 | /** |
michael@0 | 518 | * Internal helper for defining the standard transactions and their input. |
michael@0 | 519 | * It takes the required and optional properties, and generates the public |
michael@0 | 520 | * constructor (which takes the input in the form of a plain object) which, |
michael@0 | 521 | * when called, creates the argument-less "public" |execute| method by binding |
michael@0 | 522 | * the input properties to the function arguments (required properties first, |
michael@0 | 523 | * then the optional properties). |
michael@0 | 524 | * |
michael@0 | 525 | * If this seems confusing, look at the consumers. |
michael@0 | 526 | * |
michael@0 | 527 | * This magic serves two purposes: |
michael@0 | 528 | * (1) It completely hides the transactions' internals from the module |
michael@0 | 529 | * consumers. |
michael@0 | 530 | * (2) It keeps each transaction implementation to what is about, bypassing |
michael@0 | 531 | * all this bureaucracy while still validating input appropriately. |
michael@0 | 532 | */ |
michael@0 | 533 | function DefineTransaction(aRequiredProps = [], aOptionalProps = []) { |
michael@0 | 534 | for (let prop of [...aRequiredProps, ...aOptionalProps]) { |
michael@0 | 535 | if (!DefineTransaction.inputProps.has(prop)) |
michael@0 | 536 | throw new Error("Property '" + prop + "' is not defined"); |
michael@0 | 537 | } |
michael@0 | 538 | |
michael@0 | 539 | let ctor = function (aInput) { |
michael@0 | 540 | // We want to support both syntaxes: |
michael@0 | 541 | // let t = new PlacesTransactions.NewBookmark(), |
michael@0 | 542 | // let t = PlacesTransactions.NewBookmark() |
michael@0 | 543 | if (this == PlacesTransactions) |
michael@0 | 544 | return new ctor(aInput); |
michael@0 | 545 | |
michael@0 | 546 | if (aRequiredProps.length > 0 || aOptionalProps.length > 0) { |
michael@0 | 547 | // Bind the input properties to the arguments of execute. |
michael@0 | 548 | let input = DefineTransaction.verifyInput(aInput, aRequiredProps, |
michael@0 | 549 | aOptionalProps); |
michael@0 | 550 | let executeArgs = [this, |
michael@0 | 551 | ...[input[prop] for (prop of aRequiredProps)], |
michael@0 | 552 | ...[input[prop] for (prop of aOptionalProps)]]; |
michael@0 | 553 | this.execute = Function.bind.apply(this.execute, executeArgs); |
michael@0 | 554 | } |
michael@0 | 555 | return TransactionsHistory.proxifyTransaction(this); |
michael@0 | 556 | }; |
michael@0 | 557 | return ctor; |
michael@0 | 558 | } |
michael@0 | 559 | |
michael@0 | 560 | DefineTransaction.isStr = v => typeof(v) == "string"; |
michael@0 | 561 | DefineTransaction.isURI = v => v instanceof Components.interfaces.nsIURI; |
michael@0 | 562 | DefineTransaction.isIndex = v => Number.isInteger(v) && |
michael@0 | 563 | v >= PlacesUtils.bookmarks.DEFAULT_INDEX; |
michael@0 | 564 | DefineTransaction.isGUID = v => /^[a-zA-Z0-9\-_]{12}$/.test(v); |
michael@0 | 565 | DefineTransaction.isPrimitive = v => v === null || (typeof(v) != "object" && |
michael@0 | 566 | typeof(v) != "function"); |
michael@0 | 567 | DefineTransaction.isAnnotationObject = function (obj) { |
michael@0 | 568 | let checkProperty = (aPropName, aRequired, aCheckFunc) => { |
michael@0 | 569 | if (aPropName in obj) |
michael@0 | 570 | return aCheckFunc(obj[aPropName]); |
michael@0 | 571 | |
michael@0 | 572 | return !aRequired; |
michael@0 | 573 | }; |
michael@0 | 574 | |
michael@0 | 575 | if (obj && |
michael@0 | 576 | checkProperty("name", true, DefineTransaction.isStr) && |
michael@0 | 577 | checkProperty("expires", false, Number.isInteger) && |
michael@0 | 578 | checkProperty("flags", false, Number.isInteger) && |
michael@0 | 579 | checkProperty("value", false, DefineTransaction.isPrimitive) ) { |
michael@0 | 580 | // Nothing else should be set |
michael@0 | 581 | let validKeys = ["name", "value", "flags", "expires"]; |
michael@0 | 582 | if (Object.keys(obj).every( (k) => validKeys.indexOf(k) != -1 )) |
michael@0 | 583 | return true; |
michael@0 | 584 | } |
michael@0 | 585 | return false; |
michael@0 | 586 | }; |
michael@0 | 587 | |
michael@0 | 588 | DefineTransaction.inputProps = new Map(); |
michael@0 | 589 | DefineTransaction.defineInputProps = |
michael@0 | 590 | function (aNames, aValidationFunction, aDefaultValue) { |
michael@0 | 591 | for (let name of aNames) { |
michael@0 | 592 | this.inputProps.set(name, { |
michael@0 | 593 | validate: aValidationFunction, |
michael@0 | 594 | defaultValue: aDefaultValue, |
michael@0 | 595 | isGUIDProp: false |
michael@0 | 596 | }); |
michael@0 | 597 | } |
michael@0 | 598 | }; |
michael@0 | 599 | |
michael@0 | 600 | DefineTransaction.defineArrayInputProp = |
michael@0 | 601 | function (aName, aValidationFunction, aDefaultValue) { |
michael@0 | 602 | this.inputProps.set(aName, { |
michael@0 | 603 | validate: (v) => Array.isArray(v) && v.every(aValidationFunction), |
michael@0 | 604 | defaultValue: aDefaultValue, |
michael@0 | 605 | isGUIDProp: false |
michael@0 | 606 | }); |
michael@0 | 607 | }; |
michael@0 | 608 | |
michael@0 | 609 | DefineTransaction.verifyPropertyValue = |
michael@0 | 610 | function (aProp, aValue, aRequired) { |
michael@0 | 611 | if (aValue === undefined) { |
michael@0 | 612 | if (aRequired) |
michael@0 | 613 | throw new Error("Required property is missing: " + aProp); |
michael@0 | 614 | return this.inputProps.get(aProp).defaultValue; |
michael@0 | 615 | } |
michael@0 | 616 | |
michael@0 | 617 | if (!this.inputProps.get(aProp).validate(aValue)) |
michael@0 | 618 | throw new Error("Invalid value for property: " + aProp); |
michael@0 | 619 | |
michael@0 | 620 | if (Array.isArray(aValue)) { |
michael@0 | 621 | // The original array cannot be referenced by this module because it would |
michael@0 | 622 | // then implicitly reference its global as well. |
michael@0 | 623 | return Components.utils.cloneInto(aValue, {}); |
michael@0 | 624 | } |
michael@0 | 625 | |
michael@0 | 626 | return aValue; |
michael@0 | 627 | }; |
michael@0 | 628 | |
michael@0 | 629 | DefineTransaction.verifyInput = |
michael@0 | 630 | function (aInput, aRequired = [], aOptional = []) { |
michael@0 | 631 | if (aRequired.length == 0 && aOptional.length == 0) |
michael@0 | 632 | return {}; |
michael@0 | 633 | |
michael@0 | 634 | // If there's just a single required/optional property, we allow passing it |
michael@0 | 635 | // as is, so, for example, one could do PlacesTransactions.RemoveItem(myGUID) |
michael@0 | 636 | // rather than PlacesTransactions.RemoveItem({ GUID: myGUID}). |
michael@0 | 637 | // This shortcut isn't supported for "complex" properties - e.g. one cannot |
michael@0 | 638 | // pass an annotation object this way (note there is no use case for this at |
michael@0 | 639 | // the moment anyway). |
michael@0 | 640 | let isSinglePropertyInput = |
michael@0 | 641 | this.isPrimitive(aInput) || |
michael@0 | 642 | (aInput instanceof Components.interfaces.nsISupports); |
michael@0 | 643 | let fixedInput = { }; |
michael@0 | 644 | if (aRequired.length > 0) { |
michael@0 | 645 | if (isSinglePropertyInput) { |
michael@0 | 646 | if (aRequired.length == 1) { |
michael@0 | 647 | let prop = aRequired[0], value = aInput; |
michael@0 | 648 | value = this.verifyPropertyValue(prop, value, true); |
michael@0 | 649 | fixedInput[prop] = value; |
michael@0 | 650 | } |
michael@0 | 651 | else { |
michael@0 | 652 | throw new Error("Transaction input isn't an object"); |
michael@0 | 653 | } |
michael@0 | 654 | } |
michael@0 | 655 | else { |
michael@0 | 656 | for (let prop of aRequired) { |
michael@0 | 657 | let value = this.verifyPropertyValue(prop, aInput[prop], true); |
michael@0 | 658 | fixedInput[prop] = value; |
michael@0 | 659 | } |
michael@0 | 660 | } |
michael@0 | 661 | } |
michael@0 | 662 | |
michael@0 | 663 | if (aOptional.length > 0) { |
michael@0 | 664 | if (isSinglePropertyInput && !aRequired.length > 0) { |
michael@0 | 665 | if (aOptional.length == 1) { |
michael@0 | 666 | let prop = aOptional[0], value = aInput; |
michael@0 | 667 | value = this.verifyPropertyValue(prop, value, true); |
michael@0 | 668 | fixedInput[prop] = value; |
michael@0 | 669 | } |
michael@0 | 670 | else if (aInput !== null && aInput !== undefined) { |
michael@0 | 671 | throw new Error("Transaction input isn't an object"); |
michael@0 | 672 | } |
michael@0 | 673 | } |
michael@0 | 674 | else { |
michael@0 | 675 | for (let prop of aOptional) { |
michael@0 | 676 | let value = this.verifyPropertyValue(prop, aInput[prop], false); |
michael@0 | 677 | if (value !== undefined) |
michael@0 | 678 | fixedInput[prop] = value; |
michael@0 | 679 | else |
michael@0 | 680 | fixedInput[prop] = this.defaultValues[prop]; |
michael@0 | 681 | } |
michael@0 | 682 | } |
michael@0 | 683 | } |
michael@0 | 684 | |
michael@0 | 685 | return fixedInput; |
michael@0 | 686 | }; |
michael@0 | 687 | |
michael@0 | 688 | // Update the documentation at the top of this module if you add or |
michael@0 | 689 | // remove properties. |
michael@0 | 690 | DefineTransaction.defineInputProps(["uri", "feedURI", "siteURI"], |
michael@0 | 691 | DefineTransaction.isURI, null); |
michael@0 | 692 | DefineTransaction.defineInputProps(["GUID", "parentGUID", "newParentGUID"], |
michael@0 | 693 | DefineTransaction.isGUID); |
michael@0 | 694 | DefineTransaction.defineInputProps(["title", "keyword", "postData"], |
michael@0 | 695 | DefineTransaction.isStr, ""); |
michael@0 | 696 | DefineTransaction.defineInputProps(["index", "newIndex"], |
michael@0 | 697 | DefineTransaction.isIndex, |
michael@0 | 698 | PlacesUtils.bookmarks.DEFAULT_INDEX); |
michael@0 | 699 | DefineTransaction.defineInputProps(["annotationObject"], |
michael@0 | 700 | DefineTransaction.isAnnotationObject); |
michael@0 | 701 | DefineTransaction.defineArrayInputProp("tags", |
michael@0 | 702 | DefineTransaction.isStr, null); |
michael@0 | 703 | DefineTransaction.defineArrayInputProp("annotations", |
michael@0 | 704 | DefineTransaction.isAnnotationObject, |
michael@0 | 705 | null); |
michael@0 | 706 | |
michael@0 | 707 | /** |
michael@0 | 708 | * Internal helper for implementing the execute method of NewBookmark, NewFolder |
michael@0 | 709 | * and NewSeparator. |
michael@0 | 710 | * |
michael@0 | 711 | * @param aTransaction |
michael@0 | 712 | * The transaction object |
michael@0 | 713 | * @param aParentGUID |
michael@0 | 714 | * The guid of the parent folder |
michael@0 | 715 | * @param aCreateItemFunction(aParentId, aGUIDToRestore) |
michael@0 | 716 | * The function to be called for creating the item on execute and redo. |
michael@0 | 717 | * It should return the itemId for the new item |
michael@0 | 718 | * - aGUIDToRestore - the GUID to set for the item (used for redo). |
michael@0 | 719 | * @param [optional] aOnUndo |
michael@0 | 720 | * an additional function to call after undo |
michael@0 | 721 | * @param [optional] aOnRedo |
michael@0 | 722 | * an additional function to call after redo |
michael@0 | 723 | */ |
michael@0 | 724 | function* ExecuteCreateItem(aTransaction, aParentGUID, aCreateItemFunction, |
michael@0 | 725 | aOnUndo = null, aOnRedo = null) { |
michael@0 | 726 | let parentId = yield PlacesUtils.promiseItemId(aParentGUID), |
michael@0 | 727 | itemId = yield aCreateItemFunction(parentId, ""), |
michael@0 | 728 | guid = yield PlacesUtils.promiseItemGUID(itemId); |
michael@0 | 729 | |
michael@0 | 730 | // On redo, we'll restore the date-added and last-modified properties. |
michael@0 | 731 | let dateAdded = 0, lastModified = 0; |
michael@0 | 732 | aTransaction.undo = function* () { |
michael@0 | 733 | if (dateAdded == 0) { |
michael@0 | 734 | dateAdded = PlacesUtils.bookmarks.getItemDateAdded(itemId); |
michael@0 | 735 | lastModified = PlacesUtils.bookmarks.getItemLastModified(itemId); |
michael@0 | 736 | } |
michael@0 | 737 | PlacesUtils.bookmarks.removeItem(itemId); |
michael@0 | 738 | if (aOnUndo) { |
michael@0 | 739 | yield aOnUndo(); |
michael@0 | 740 | } |
michael@0 | 741 | }; |
michael@0 | 742 | aTransaction.redo = function* () { |
michael@0 | 743 | parentId = yield PlacesUtils.promiseItemId(aParentGUID); |
michael@0 | 744 | itemId = yield aCreateItemFunction(parentId, guid); |
michael@0 | 745 | if (aOnRedo) |
michael@0 | 746 | yield aOnRedo(); |
michael@0 | 747 | |
michael@0 | 748 | // aOnRedo is called first to make sure it doesn't override |
michael@0 | 749 | // lastModified. |
michael@0 | 750 | PlacesUtils.bookmarks.setItemDateAdded(itemId, dateAdded); |
michael@0 | 751 | PlacesUtils.bookmarks.setItemLastModified(itemId, lastModified); |
michael@0 | 752 | }; |
michael@0 | 753 | return guid; |
michael@0 | 754 | } |
michael@0 | 755 | |
michael@0 | 756 | /***************************************************************************** |
michael@0 | 757 | * The Standard Places Transactions. |
michael@0 | 758 | * |
michael@0 | 759 | * See the documentation at the top of this file. The valid values for input |
michael@0 | 760 | * are also documented there. |
michael@0 | 761 | *****************************************************************************/ |
michael@0 | 762 | |
michael@0 | 763 | let PT = PlacesTransactions; |
michael@0 | 764 | |
michael@0 | 765 | /** |
michael@0 | 766 | * Transaction for creating a bookmark. |
michael@0 | 767 | * |
michael@0 | 768 | * Required Input Properties: uri, parentGUID. |
michael@0 | 769 | * Optional Input Properties: index, title, keyword, annotations, tags. |
michael@0 | 770 | * |
michael@0 | 771 | * When this transaction is executed, it's resolved to the new bookmark's GUID. |
michael@0 | 772 | */ |
michael@0 | 773 | PT.NewBookmark = DefineTransaction(["parentGUID", "uri"], |
michael@0 | 774 | ["index", "title", "keyword", "postData", |
michael@0 | 775 | "annotations", "tags"]); |
michael@0 | 776 | PT.NewBookmark.prototype = Object.seal({ |
michael@0 | 777 | execute: function (aParentGUID, aURI, aIndex, aTitle, |
michael@0 | 778 | aKeyword, aPostData, aAnnos, aTags) { |
michael@0 | 779 | return ExecuteCreateItem(this, aParentGUID, |
michael@0 | 780 | function (parentId, guidToRestore = "") { |
michael@0 | 781 | let itemId = PlacesUtils.bookmarks.insertBookmark( |
michael@0 | 782 | parentId, aURI, aIndex, aTitle, guidToRestore); |
michael@0 | 783 | if (aKeyword) |
michael@0 | 784 | PlacesUtils.bookmarks.setKeywordForBookmark(itemId, aKeyword); |
michael@0 | 785 | if (aPostData) |
michael@0 | 786 | PlacesUtils.setPostDataForBookmark(itemId, aPostData); |
michael@0 | 787 | if (aAnnos) |
michael@0 | 788 | PlacesUtils.setAnnotationsForItem(itemId, aAnnos); |
michael@0 | 789 | if (aTags && aTags.length > 0) { |
michael@0 | 790 | let currentTags = PlacesUtils.tagging.getTagsForURI(aURI); |
michael@0 | 791 | aTags = [t for (t of aTags) if (currentTags.indexOf(t) == -1)]; |
michael@0 | 792 | PlacesUtils.tagging.tagURI(aURI, aTags); |
michael@0 | 793 | } |
michael@0 | 794 | |
michael@0 | 795 | return itemId; |
michael@0 | 796 | }, |
michael@0 | 797 | function _additionalOnUndo() { |
michael@0 | 798 | if (aTags && aTags.length > 0) |
michael@0 | 799 | PlacesUtils.tagging.untagURI(aURI, aTags); |
michael@0 | 800 | }); |
michael@0 | 801 | } |
michael@0 | 802 | }); |
michael@0 | 803 | |
michael@0 | 804 | /** |
michael@0 | 805 | * Transaction for creating a folder. |
michael@0 | 806 | * |
michael@0 | 807 | * Required Input Properties: title, parentGUID. |
michael@0 | 808 | * Optional Input Properties: index, annotations. |
michael@0 | 809 | * |
michael@0 | 810 | * When this transaction is executed, it's resolved to the new folder's GUID. |
michael@0 | 811 | */ |
michael@0 | 812 | PT.NewFolder = DefineTransaction(["parentGUID", "title"], |
michael@0 | 813 | ["index", "annotations"]); |
michael@0 | 814 | PT.NewFolder.prototype = Object.seal({ |
michael@0 | 815 | execute: function (aParentGUID, aTitle, aIndex, aAnnos) { |
michael@0 | 816 | return ExecuteCreateItem(this, aParentGUID, |
michael@0 | 817 | function(parentId, guidToRestore = "") { |
michael@0 | 818 | let itemId = PlacesUtils.bookmarks.createFolder( |
michael@0 | 819 | parentId, aTitle, aIndex, guidToRestore); |
michael@0 | 820 | if (aAnnos) |
michael@0 | 821 | PlacesUtils.setAnnotationsForItem(itemId, aAnnos); |
michael@0 | 822 | return itemId; |
michael@0 | 823 | }); |
michael@0 | 824 | } |
michael@0 | 825 | }); |
michael@0 | 826 | |
michael@0 | 827 | /** |
michael@0 | 828 | * Transaction for creating a separator. |
michael@0 | 829 | * |
michael@0 | 830 | * Required Input Properties: parentGUID. |
michael@0 | 831 | * Optional Input Properties: index. |
michael@0 | 832 | * |
michael@0 | 833 | * When this transaction is executed, it's resolved to the new separator's |
michael@0 | 834 | * GUID. |
michael@0 | 835 | */ |
michael@0 | 836 | PT.NewSeparator = DefineTransaction(["parentGUID"], ["index"]); |
michael@0 | 837 | PT.NewSeparator.prototype = Object.seal({ |
michael@0 | 838 | execute: function (aParentGUID, aIndex) { |
michael@0 | 839 | return ExecuteCreateItem(this, aParentGUID, |
michael@0 | 840 | function (parentId, guidToRestore = "") { |
michael@0 | 841 | let itemId = PlacesUtils.bookmarks.insertSeparator( |
michael@0 | 842 | parentId, aIndex, guidToRestore); |
michael@0 | 843 | return itemId; |
michael@0 | 844 | }); |
michael@0 | 845 | } |
michael@0 | 846 | }); |
michael@0 | 847 | |
michael@0 | 848 | /** |
michael@0 | 849 | * Transaction for creating a live bookmark (see mozIAsyncLivemarks for the |
michael@0 | 850 | * semantics). |
michael@0 | 851 | * |
michael@0 | 852 | * Required Input Properties: feedURI, title, parentGUID. |
michael@0 | 853 | * Optional Input Properties: siteURI, index, annotations. |
michael@0 | 854 | * |
michael@0 | 855 | * When this transaction is executed, it's resolved to the new separators's |
michael@0 | 856 | * GUID. |
michael@0 | 857 | */ |
michael@0 | 858 | PT.NewLivemark = DefineTransaction(["feedURI", "title", "parentGUID"], |
michael@0 | 859 | ["siteURI", "index", "annotations"]); |
michael@0 | 860 | PT.NewLivemark.prototype = Object.seal({ |
michael@0 | 861 | execute: function* (aFeedURI, aTitle, aParentGUID, aSiteURI, aIndex, aAnnos) { |
michael@0 | 862 | let createItem = function* (aGUID = "") { |
michael@0 | 863 | let parentId = yield PlacesUtils.promiseItemId(aParentGUID); |
michael@0 | 864 | let livemarkInfo = { |
michael@0 | 865 | title: aTitle |
michael@0 | 866 | , feedURI: aFeedURI |
michael@0 | 867 | , parentId: parentId |
michael@0 | 868 | , index: aIndex |
michael@0 | 869 | , siteURI: aSiteURI }; |
michael@0 | 870 | if (aGUID) |
michael@0 | 871 | livemarkInfo.guid = aGUID; |
michael@0 | 872 | |
michael@0 | 873 | let livemark = yield PlacesUtils.livemarks.addLivemark(livemarkInfo); |
michael@0 | 874 | if (aAnnos) |
michael@0 | 875 | PlacesUtils.setAnnotationsForItem(livemark.id, aAnnos); |
michael@0 | 876 | |
michael@0 | 877 | return livemark; |
michael@0 | 878 | }; |
michael@0 | 879 | |
michael@0 | 880 | let guid = (yield createItem()).guid; |
michael@0 | 881 | this.undo = function* () { |
michael@0 | 882 | yield PlacesUtils.livemarks.removeLivemark({ guid: guid }); |
michael@0 | 883 | }; |
michael@0 | 884 | this.redo = function* () { |
michael@0 | 885 | yield createItem(guid); |
michael@0 | 886 | }; |
michael@0 | 887 | return guid; |
michael@0 | 888 | } |
michael@0 | 889 | }); |
michael@0 | 890 | |
michael@0 | 891 | /** |
michael@0 | 892 | * Transaction for moving an item. |
michael@0 | 893 | * |
michael@0 | 894 | * Required Input Properties: GUID, newParentGUID. |
michael@0 | 895 | * Optional Input Properties newIndex. |
michael@0 | 896 | */ |
michael@0 | 897 | PT.MoveItem = DefineTransaction(["GUID", "newParentGUID"], ["newIndex"]); |
michael@0 | 898 | PT.MoveItem.prototype = Object.seal({ |
michael@0 | 899 | execute: function* (aGUID, aNewParentGUID, aNewIndex) { |
michael@0 | 900 | let itemId = yield PlacesUtils.promiseItemId(aGUID), |
michael@0 | 901 | oldParentId = PlacesUtils.bookmarks.getFolderIdForItem(itemId), |
michael@0 | 902 | oldIndex = PlacesUtils.bookmarks.getItemIndex(itemId), |
michael@0 | 903 | newParentId = yield PlacesUtils.promiseItemId(aNewParentGUID); |
michael@0 | 904 | |
michael@0 | 905 | PlacesUtils.bookmarks.moveItem(itemId, newParentId, aNewIndex); |
michael@0 | 906 | |
michael@0 | 907 | let undoIndex = PlacesUtils.bookmarks.getItemIndex(itemId); |
michael@0 | 908 | this.undo = () => { |
michael@0 | 909 | // Moving down in the same parent takes in count removal of the item |
michael@0 | 910 | // so to revert positions we must move to oldIndex + 1 |
michael@0 | 911 | if (newParentId == oldParentId && oldIndex > undoIndex) |
michael@0 | 912 | PlacesUtils.bookmarks.moveItem(itemId, oldParentId, oldIndex + 1); |
michael@0 | 913 | else |
michael@0 | 914 | PlacesUtils.bookmarks.moveItem(itemId, oldParentId, oldIndex); |
michael@0 | 915 | }; |
michael@0 | 916 | } |
michael@0 | 917 | }); |
michael@0 | 918 | |
michael@0 | 919 | /** |
michael@0 | 920 | * Transaction for setting the title for an item. |
michael@0 | 921 | * |
michael@0 | 922 | * Required Input Properties: GUID, title. |
michael@0 | 923 | */ |
michael@0 | 924 | PT.EditTitle = DefineTransaction(["GUID", "title"]); |
michael@0 | 925 | PT.EditTitle.prototype = Object.seal({ |
michael@0 | 926 | execute: function* (aGUID, aTitle) { |
michael@0 | 927 | let itemId = yield PlacesUtils.promiseItemId(aGUID), |
michael@0 | 928 | oldTitle = PlacesUtils.bookmarks.getItemTitle(itemId); |
michael@0 | 929 | PlacesUtils.bookmarks.setItemTitle(itemId, aTitle); |
michael@0 | 930 | this.undo = () => { PlacesUtils.bookmarks.setItemTitle(itemId, oldTitle); }; |
michael@0 | 931 | } |
michael@0 | 932 | }); |
michael@0 | 933 | |
michael@0 | 934 | /** |
michael@0 | 935 | * Transaction for setting the URI for an item. |
michael@0 | 936 | * |
michael@0 | 937 | * Required Input Properties: GUID, uri. |
michael@0 | 938 | */ |
michael@0 | 939 | PT.EditURI = DefineTransaction(["GUID", "uri"]); |
michael@0 | 940 | PT.EditURI.prototype = Object.seal({ |
michael@0 | 941 | execute: function* (aGUID, aURI) { |
michael@0 | 942 | let itemId = yield PlacesUtils.promiseItemId(aGUID), |
michael@0 | 943 | oldURI = PlacesUtils.bookmarks.getBookmarkURI(itemId), |
michael@0 | 944 | oldURITags = PlacesUtils.tagging.getTagsForURI(oldURI), |
michael@0 | 945 | newURIAdditionalTags = null; |
michael@0 | 946 | PlacesUtils.bookmarks.changeBookmarkURI(itemId, aURI); |
michael@0 | 947 | |
michael@0 | 948 | // Move tags from old URI to new URI. |
michael@0 | 949 | if (oldURITags.length > 0) { |
michael@0 | 950 | // Only untag the old URI if this is the only bookmark. |
michael@0 | 951 | if (PlacesUtils.getBookmarksForURI(oldURI, {}).length == 0) |
michael@0 | 952 | PlacesUtils.tagging.untagURI(oldURI, oldURITags); |
michael@0 | 953 | |
michael@0 | 954 | let currentNewURITags = PlacesUtils.tagging.getTagsForURI(aURI); |
michael@0 | 955 | newURIAdditionalTags = [t for (t of oldURITags) |
michael@0 | 956 | if (currentNewURITags.indexOf(t) == -1)]; |
michael@0 | 957 | if (newURIAdditionalTags) |
michael@0 | 958 | PlacesUtils.tagging.tagURI(aURI, newURIAdditionalTags); |
michael@0 | 959 | } |
michael@0 | 960 | |
michael@0 | 961 | this.undo = () => { |
michael@0 | 962 | PlacesUtils.bookmarks.changeBookmarkURI(itemId, oldURI); |
michael@0 | 963 | // Move tags from new URI to old URI. |
michael@0 | 964 | if (oldURITags.length > 0) { |
michael@0 | 965 | // Only untag the new URI if this is the only bookmark. |
michael@0 | 966 | if (newURIAdditionalTags && newURIAdditionalTags.length > 0 && |
michael@0 | 967 | PlacesUtils.getBookmarksForURI(aURI, {}).length == 0) { |
michael@0 | 968 | PlacesUtils.tagging.untagURI(aURI, newURIAdditionalTags); |
michael@0 | 969 | } |
michael@0 | 970 | |
michael@0 | 971 | PlacesUtils.tagging.tagURI(oldURI, oldURITags); |
michael@0 | 972 | } |
michael@0 | 973 | }; |
michael@0 | 974 | } |
michael@0 | 975 | }); |
michael@0 | 976 | |
michael@0 | 977 | /** |
michael@0 | 978 | * Transaction for setting an annotation for an item. |
michael@0 | 979 | * |
michael@0 | 980 | * Required Input Properties: GUID, annotationObject |
michael@0 | 981 | */ |
michael@0 | 982 | PT.SetItemAnnotation = DefineTransaction(["GUID", "annotationObject"]); |
michael@0 | 983 | PT.SetItemAnnotation.prototype = { |
michael@0 | 984 | execute: function* (aGUID, aAnno) { |
michael@0 | 985 | let itemId = yield PlacesUtils.promiseItemId(aGUID), oldAnno; |
michael@0 | 986 | if (PlacesUtils.annotations.itemHasAnnotation(itemId, aAnno.name)) { |
michael@0 | 987 | // Fill the old anno if it is set. |
michael@0 | 988 | let flags = {}, expires = {}; |
michael@0 | 989 | PlacesUtils.annotations.getItemAnnotationInfo(itemId, aAnno.name, flags, |
michael@0 | 990 | expires, { }); |
michael@0 | 991 | let value = PlacesUtils.annotations.getItemAnnotation(itemId, aAnno.name); |
michael@0 | 992 | oldAnno = { name: aAnno.name, flags: flags.value, |
michael@0 | 993 | value: value, expires: expires.value }; |
michael@0 | 994 | } |
michael@0 | 995 | else { |
michael@0 | 996 | // An unset value removes the annoation. |
michael@0 | 997 | oldAnno = { name: aAnno.name }; |
michael@0 | 998 | } |
michael@0 | 999 | |
michael@0 | 1000 | PlacesUtils.setAnnotationsForItem(itemId, [aAnno]); |
michael@0 | 1001 | this.undo = () => { PlacesUtils.setAnnotationsForItem(itemId, [oldAnno]); }; |
michael@0 | 1002 | } |
michael@0 | 1003 | }; |
michael@0 | 1004 | |
michael@0 | 1005 | /** |
michael@0 | 1006 | * Transaction for setting the keyword for a bookmark. |
michael@0 | 1007 | * |
michael@0 | 1008 | * Required Input Properties: GUID, keyword. |
michael@0 | 1009 | */ |
michael@0 | 1010 | PT.EditKeyword = DefineTransaction(["GUID", "keyword"]); |
michael@0 | 1011 | PT.EditKeyword.prototype = Object.seal({ |
michael@0 | 1012 | execute: function* (aGUID, aKeyword) { |
michael@0 | 1013 | let itemId = yield PlacesUtils.promiseItemId(aGUID), |
michael@0 | 1014 | oldKeyword = PlacesUtils.bookmarks.getKeywordForBookmark(itemId); |
michael@0 | 1015 | PlacesUtils.bookmarks.setKeywordForBookmark(itemId, aKeyword); |
michael@0 | 1016 | this.undo = () => { |
michael@0 | 1017 | PlacesUtils.bookmarks.setKeywordForBookmark(itemId, oldKeyword); |
michael@0 | 1018 | }; |
michael@0 | 1019 | } |
michael@0 | 1020 | }); |
michael@0 | 1021 | |
michael@0 | 1022 | /** |
michael@0 | 1023 | * Transaction for sorting a folder by name. |
michael@0 | 1024 | * |
michael@0 | 1025 | * Required Input Properties: GUID. |
michael@0 | 1026 | */ |
michael@0 | 1027 | PT.SortByName = DefineTransaction(["GUID"]); |
michael@0 | 1028 | PT.SortByName.prototype = { |
michael@0 | 1029 | execute: function* (aGUID) { |
michael@0 | 1030 | let itemId = yield PlacesUtils.promiseItemId(aGUID), |
michael@0 | 1031 | oldOrder = [], // [itemId] = old index |
michael@0 | 1032 | contents = PlacesUtils.getFolderContents(itemId, false, false).root, |
michael@0 | 1033 | count = contents.childCount; |
michael@0 | 1034 | |
michael@0 | 1035 | // Sort between separators. |
michael@0 | 1036 | let newOrder = [], // nodes, in the new order. |
michael@0 | 1037 | preSep = []; // Temporary array for sorting each group of nodes. |
michael@0 | 1038 | let sortingMethod = (a, b) => { |
michael@0 | 1039 | if (PlacesUtils.nodeIsContainer(a) && !PlacesUtils.nodeIsContainer(b)) |
michael@0 | 1040 | return -1; |
michael@0 | 1041 | if (!PlacesUtils.nodeIsContainer(a) && PlacesUtils.nodeIsContainer(b)) |
michael@0 | 1042 | return 1; |
michael@0 | 1043 | return a.title.localeCompare(b.title); |
michael@0 | 1044 | }; |
michael@0 | 1045 | |
michael@0 | 1046 | for (let i = 0; i < count; ++i) { |
michael@0 | 1047 | let node = contents.getChild(i); |
michael@0 | 1048 | oldOrder[node.itemId] = i; |
michael@0 | 1049 | if (PlacesUtils.nodeIsSeparator(node)) { |
michael@0 | 1050 | if (preSep.length > 0) { |
michael@0 | 1051 | preSep.sort(sortingMethod); |
michael@0 | 1052 | newOrder = newOrder.concat(preSep); |
michael@0 | 1053 | preSep.splice(0, preSep.length); |
michael@0 | 1054 | } |
michael@0 | 1055 | newOrder.push(node); |
michael@0 | 1056 | } |
michael@0 | 1057 | else |
michael@0 | 1058 | preSep.push(node); |
michael@0 | 1059 | } |
michael@0 | 1060 | contents.containerOpen = false; |
michael@0 | 1061 | |
michael@0 | 1062 | if (preSep.length > 0) { |
michael@0 | 1063 | preSep.sort(sortingMethod); |
michael@0 | 1064 | newOrder = newOrder.concat(preSep); |
michael@0 | 1065 | } |
michael@0 | 1066 | |
michael@0 | 1067 | // Set the nex indexes. |
michael@0 | 1068 | let callback = { |
michael@0 | 1069 | runBatched: function() { |
michael@0 | 1070 | for (let i = 0; i < newOrder.length; ++i) { |
michael@0 | 1071 | PlacesUtils.bookmarks.setItemIndex(newOrder[i].itemId, i); |
michael@0 | 1072 | } |
michael@0 | 1073 | } |
michael@0 | 1074 | }; |
michael@0 | 1075 | PlacesUtils.bookmarks.runInBatchMode(callback, null); |
michael@0 | 1076 | |
michael@0 | 1077 | this.undo = () => { |
michael@0 | 1078 | let callback = { |
michael@0 | 1079 | runBatched: function() { |
michael@0 | 1080 | for (let item in oldOrder) { |
michael@0 | 1081 | PlacesUtils.bookmarks.setItemIndex(item, oldOrder[item]); |
michael@0 | 1082 | } |
michael@0 | 1083 | } |
michael@0 | 1084 | }; |
michael@0 | 1085 | PlacesUtils.bookmarks.runInBatchMode(callback, null); |
michael@0 | 1086 | }; |
michael@0 | 1087 | } |
michael@0 | 1088 | }; |
michael@0 | 1089 | |
michael@0 | 1090 | /** |
michael@0 | 1091 | * Transaction for removing an item (any type). |
michael@0 | 1092 | * |
michael@0 | 1093 | * Required Input Properties: GUID. |
michael@0 | 1094 | */ |
michael@0 | 1095 | PT.RemoveItem = DefineTransaction(["GUID"]); |
michael@0 | 1096 | PT.RemoveItem.prototype = { |
michael@0 | 1097 | execute: function* (aGUID) { |
michael@0 | 1098 | const bms = PlacesUtils.bookmarks; |
michael@0 | 1099 | |
michael@0 | 1100 | let itemsToRestoreOnUndo = []; |
michael@0 | 1101 | function* saveItemRestoreData(aItem, aNode = null) { |
michael@0 | 1102 | if (!aItem || !aItem.GUID) |
michael@0 | 1103 | throw new Error("invalid item object"); |
michael@0 | 1104 | |
michael@0 | 1105 | let itemId = aNode ? |
michael@0 | 1106 | aNode.itemId : yield PlacesUtils.promiseItemId(aItem.GUID); |
michael@0 | 1107 | if (itemId == -1) |
michael@0 | 1108 | throw new Error("Unexpected non-bookmarks node"); |
michael@0 | 1109 | |
michael@0 | 1110 | aItem.itemType = function() { |
michael@0 | 1111 | if (aNode) { |
michael@0 | 1112 | switch (aNode.type) { |
michael@0 | 1113 | case aNode.RESULT_TYPE_SEPARATOR: |
michael@0 | 1114 | return bms.TYPE_SEPARATOR; |
michael@0 | 1115 | case aNode.RESULT_TYPE_URI: // regular bookmarks |
michael@0 | 1116 | case aNode.RESULT_TYPE_FOLDER_SHORTCUT: // place:folder= bookmarks |
michael@0 | 1117 | case aNode.RESULT_TYPE_QUERY: // smart bookmarks |
michael@0 | 1118 | return bms.TYPE_BOOKMARK; |
michael@0 | 1119 | case aNode.RESULT_TYPE_FOLDER: |
michael@0 | 1120 | return bms.TYPE_FOLDER; |
michael@0 | 1121 | default: |
michael@0 | 1122 | throw new Error("Unexpected node type"); |
michael@0 | 1123 | } |
michael@0 | 1124 | } |
michael@0 | 1125 | return bms.getItemType(itemId); |
michael@0 | 1126 | }(); |
michael@0 | 1127 | |
michael@0 | 1128 | let node = aNode; |
michael@0 | 1129 | if (!node && aItem.itemType == bms.TYPE_FOLDER) |
michael@0 | 1130 | node = PlacesUtils.getFolderContents(itemId).root; |
michael@0 | 1131 | |
michael@0 | 1132 | // dateAdded, lastModified and annotations apply to all types. |
michael@0 | 1133 | aItem.dateAdded = node ? node.dateAdded : bms.getItemDateAdded(itemId); |
michael@0 | 1134 | aItem.lastModified = node ? |
michael@0 | 1135 | node.lastModified : bms.getItemLastModified(itemId); |
michael@0 | 1136 | aItem.annotations = PlacesUtils.getAnnotationsForItem(itemId); |
michael@0 | 1137 | |
michael@0 | 1138 | // For the first-level item, we don't have the parent. |
michael@0 | 1139 | if (!aItem.parentGUID) { |
michael@0 | 1140 | let parentId = PlacesUtils.bookmarks.getFolderIdForItem(itemId); |
michael@0 | 1141 | aItem.parentGUID = yield PlacesUtils.promiseItemGUID(parentId); |
michael@0 | 1142 | // For the first-level item, we also need the index. |
michael@0 | 1143 | // Note: node.bookmarkIndex doesn't work for root nodes. |
michael@0 | 1144 | aItem.index = bms.getItemIndex(itemId); |
michael@0 | 1145 | } |
michael@0 | 1146 | |
michael@0 | 1147 | // Separators don't have titles. |
michael@0 | 1148 | if (aItem.itemType != bms.TYPE_SEPARATOR) { |
michael@0 | 1149 | aItem.title = node ? node.title : bms.getItemTitle(itemId); |
michael@0 | 1150 | |
michael@0 | 1151 | if (aItem.itemType == bms.TYPE_BOOKMARK) { |
michael@0 | 1152 | aItem.uri = |
michael@0 | 1153 | node ? NetUtil.newURI(node.uri) : bms.getBookmarkURI(itemId); |
michael@0 | 1154 | aItem.keyword = PlacesUtils.bookmarks.getKeywordForBookmark(itemId); |
michael@0 | 1155 | |
michael@0 | 1156 | // This may be the last bookmark (excluding the tag-items themselves) |
michael@0 | 1157 | // for the URI, so we need to preserve the tags. |
michael@0 | 1158 | let tags = PlacesUtils.tagging.getTagsForURI(aItem.uri);; |
michael@0 | 1159 | if (tags.length > 0) |
michael@0 | 1160 | aItem.tags = tags; |
michael@0 | 1161 | } |
michael@0 | 1162 | else { // folder |
michael@0 | 1163 | // We always have the node for folders |
michael@0 | 1164 | aItem.readOnly = node.childrenReadOnly; |
michael@0 | 1165 | for (let i = 0; i < node.childCount; i++) { |
michael@0 | 1166 | let childNode = node.getChild(i); |
michael@0 | 1167 | let childItem = |
michael@0 | 1168 | { GUID: yield PlacesUtils.promiseItemGUID(childNode.itemId) |
michael@0 | 1169 | , parentGUID: aItem.GUID }; |
michael@0 | 1170 | itemsToRestoreOnUndo.push(childItem); |
michael@0 | 1171 | yield saveItemRestoreData(childItem, childNode); |
michael@0 | 1172 | } |
michael@0 | 1173 | node.containerOpen = false; |
michael@0 | 1174 | } |
michael@0 | 1175 | } |
michael@0 | 1176 | } |
michael@0 | 1177 | |
michael@0 | 1178 | let item = { GUID: aGUID, parentGUID: null }; |
michael@0 | 1179 | itemsToRestoreOnUndo.push(item); |
michael@0 | 1180 | yield saveItemRestoreData(item); |
michael@0 | 1181 | |
michael@0 | 1182 | let itemId = yield PlacesUtils.promiseItemId(aGUID); |
michael@0 | 1183 | PlacesUtils.bookmarks.removeItem(itemId); |
michael@0 | 1184 | this.undo = function() { |
michael@0 | 1185 | for (let item of itemsToRestoreOnUndo) { |
michael@0 | 1186 | let parentId = yield PlacesUtils.promiseItemId(item.parentGUID); |
michael@0 | 1187 | let index = "index" in item ? |
michael@0 | 1188 | index : PlacesUtils.bookmarks.DEFAULT_INDEX; |
michael@0 | 1189 | let itemId; |
michael@0 | 1190 | if (item.itemType == bms.TYPE_SEPARATOR) { |
michael@0 | 1191 | itemId = bms.insertSeparator(parentId, index, item.GUID); |
michael@0 | 1192 | } |
michael@0 | 1193 | else if (item.itemType == bms.TYPE_BOOKMARK) { |
michael@0 | 1194 | itemId = bms.insertBookmark(parentId, item.uri, index, item.title, |
michael@0 | 1195 | item.GUID); |
michael@0 | 1196 | } |
michael@0 | 1197 | else { // folder |
michael@0 | 1198 | itemId = bms.createFolder(parentId, item.title, index, item.GUID); |
michael@0 | 1199 | } |
michael@0 | 1200 | |
michael@0 | 1201 | if (item.itemType == bms.TYPE_BOOKMARK) { |
michael@0 | 1202 | if (item.keyword) |
michael@0 | 1203 | bms.setKeywordForBookmark(itemId, item.keyword); |
michael@0 | 1204 | if ("tags" in item) |
michael@0 | 1205 | PlacesUtils.tagging.tagURI(item.uri, item.tags); |
michael@0 | 1206 | } |
michael@0 | 1207 | else if (item.readOnly === true) { |
michael@0 | 1208 | bms.setFolderReadonly(itemId, true); |
michael@0 | 1209 | } |
michael@0 | 1210 | |
michael@0 | 1211 | PlacesUtils.setAnnotationsForItem(itemId, item.annotations); |
michael@0 | 1212 | PlacesUtils.bookmarks.setItemDateAdded(itemId, item.dateAdded); |
michael@0 | 1213 | PlacesUtils.bookmarks.setItemLastModified(itemId, item.lastModified); |
michael@0 | 1214 | } |
michael@0 | 1215 | }; |
michael@0 | 1216 | } |
michael@0 | 1217 | }; |
michael@0 | 1218 | |
michael@0 | 1219 | /** |
michael@0 | 1220 | * Transaction for tagging a URI. |
michael@0 | 1221 | * |
michael@0 | 1222 | * Required Input Properties: uri, tags. |
michael@0 | 1223 | */ |
michael@0 | 1224 | PT.TagURI = DefineTransaction(["uri", "tags"]); |
michael@0 | 1225 | PT.TagURI.prototype = { |
michael@0 | 1226 | execute: function* (aURI, aTags) { |
michael@0 | 1227 | if (PlacesUtils.getMostRecentBookmarkForURI(aURI) == -1) { |
michael@0 | 1228 | // Tagging is only allowed for bookmarked URIs. |
michael@0 | 1229 | let unfileGUID = |
michael@0 | 1230 | yield PlacesUtils.promiseItemGUID(PlacesUtils.unfiledBookmarksFolderId); |
michael@0 | 1231 | let createTxn = TransactionsHistory.getRawTransaction( |
michael@0 | 1232 | PT.NewBookmark({ uri: aURI, tags: aTags, parentGUID: unfileGUID })); |
michael@0 | 1233 | yield createTxn.execute(); |
michael@0 | 1234 | this.undo = createTxn.undo.bind(createTxn); |
michael@0 | 1235 | this.redo = createTxn.redo.bind(createTxn); |
michael@0 | 1236 | } |
michael@0 | 1237 | else { |
michael@0 | 1238 | let currentTags = PlacesUtils.tagging.getTagsForURI(aURI); |
michael@0 | 1239 | let newTags = [t for (t of aTags) if (currentTags.indexOf(t) == -1)]; |
michael@0 | 1240 | PlacesUtils.tagging.tagURI(aURI, newTags); |
michael@0 | 1241 | this.undo = () => { PlacesUtils.tagging.untagURI(aURI, newTags); }; |
michael@0 | 1242 | this.redo = () => { PlacesUtils.tagging.tagURI(aURI, newTags); }; |
michael@0 | 1243 | } |
michael@0 | 1244 | } |
michael@0 | 1245 | }; |
michael@0 | 1246 | |
michael@0 | 1247 | /** |
michael@0 | 1248 | * Transaction for removing tags from a URI. |
michael@0 | 1249 | * |
michael@0 | 1250 | * Required Input Properties: uri. |
michael@0 | 1251 | * Optional Input Properties: tags. |
michael@0 | 1252 | * |
michael@0 | 1253 | * If |tags| is not set, all tags set for |uri| are removed. |
michael@0 | 1254 | */ |
michael@0 | 1255 | PT.UntagURI = DefineTransaction(["uri"], ["tags"]); |
michael@0 | 1256 | PT.UntagURI.prototype = { |
michael@0 | 1257 | execute: function* (aURI, aTags) { |
michael@0 | 1258 | let tagsSet = PlacesUtils.tagging.getTagsForURI(aURI); |
michael@0 | 1259 | |
michael@0 | 1260 | if (aTags && aTags.length > 0) |
michael@0 | 1261 | aTags = [t for (t of aTags) if (tagsSet.indexOf(t) != -1)]; |
michael@0 | 1262 | else |
michael@0 | 1263 | aTags = tagsSet; |
michael@0 | 1264 | |
michael@0 | 1265 | PlacesUtils.tagging.untagURI(aURI, aTags); |
michael@0 | 1266 | this.undo = () => { PlacesUtils.tagging.tagURI(aURI, aTags); }; |
michael@0 | 1267 | this.redo = () => { PlacesUtils.tagging.untagURI(aURI, aTags); }; |
michael@0 | 1268 | } |
michael@0 | 1269 | }; |