angular-history-rails 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b0fc62aebce63961823cf3a20af88bf8e2075ece
4
+ data.tar.gz: 051505ba4d9a0b6b23e9dba1b3535ba253bcd0f6
5
+ SHA512:
6
+ metadata.gz: d64bcdeecedcfa6b02fd5eb52b6abef3069aaca658482668b5c9131c33c030f31b83ed0a20f3238d8df805a1b282deea2277f629ecbf9b27d67018e1fe60be7a
7
+ data.tar.gz: 513d8fb43a1267d84a69337c6edc21a8cfc34e3f250934aee3428cd91b87f67167bf607087b47f32c47332cbd0bda804c83a709a276c4c219e5d18380c7cf6ed
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in angular-history-rails.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Paul Vonderscher
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,14 @@
1
+ Rails 3.1 asset-pipeline gem to provide [angular-history.js](https://github.com/decipherinc/angular-history).
2
+
3
+ Also provides the optional [ngLazyBind service](https://github.com/Ticore/ngLazyBind).
4
+
5
+ # Setup
6
+
7
+ In your Gemfile:
8
+
9
+ gem 'angular-history-rails'
10
+
11
+ In your application.js manifest:
12
+
13
+ //= require angular-lazy-bind
14
+ //= require angular-history
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,22 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'angular/history/rails/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "angular-history-rails"
8
+ spec.version = Angular::History::Rails::VERSION
9
+ spec.authors = ["Paul Vonderscher"]
10
+ spec.email = ["paul.vonderscher@gmail.com"]
11
+ spec.summary = %q{Adds the angular-history module to the asset pipeline}
12
+ spec.homepage = "https://github.com/VonD/angular-history-rails/"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "bundler", "~> 1.5"
21
+ spec.add_development_dependency "rake"
22
+ end
@@ -0,0 +1,9 @@
1
+ require "angular/history/rails/version"
2
+
3
+ module Angular
4
+ module History
5
+ module Rails
6
+ # Your code goes here...
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ module Angular
2
+ module History
3
+ module Rails
4
+ VERSION = "0.7.3"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,1517 @@
1
+ /*global angular*/
2
+
3
+ /**
4
+ * @ngdoc overview
5
+ * @name decipher.history
6
+ * @description
7
+ * A history service for AngularJS. Undo/redo, that sort of thing. Has nothing to do with the "back" button, unless you want it to.
8
+ *
9
+ */
10
+ (function () {
11
+ 'use strict';
12
+
13
+ var DEEPWATCH_EXP = /^\s*(.*?)\s+for\s+(?:([\$\w][\$\w\d]*)|(?:\(\s*([\$\w][\$\w\d]*)\s*,\s*([\$\w][\$\w\d]*)\s*\)))\s+in\s+(.*?)$/,
14
+ DEFAULT_TIMEOUT = 1000,
15
+ lazyBindFound = false,
16
+ isDefined = angular.isDefined,
17
+ isUndefined = angular.isUndefined,
18
+ isFunction = angular.isFunction,
19
+ isArray = angular.isArray,
20
+ isString = angular.isString,
21
+ isObject = angular.isObject,
22
+ forEach = angular.forEach,
23
+ copy = angular.copy,
24
+ bind = angular.bind;
25
+
26
+ /**
27
+ * Polyfill for Object.keys
28
+ *
29
+ * @see: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys
30
+ */
31
+ if (!Object.keys) {
32
+ Object.keys = (function () {
33
+ var hasOwnProperty = Object.prototype.hasOwnProperty,
34
+ hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
35
+ dontEnums = [
36
+ 'toString',
37
+ 'toLocaleString',
38
+ 'valueOf',
39
+ 'hasOwnProperty',
40
+ 'isPrototypeOf',
41
+ 'propertyIsEnumerable',
42
+ 'constructor'
43
+ ],
44
+ dontEnumsLength = dontEnums.length;
45
+
46
+ return function (obj) {
47
+ if (typeof obj !== 'object' && typeof obj !== 'function' ||
48
+ obj === null) {
49
+ throw new TypeError('Object.keys called on non-object');
50
+ }
51
+
52
+ var result = [];
53
+
54
+ for (var prop in obj) {
55
+ if (hasOwnProperty.call(obj, prop)) {
56
+ result.push(prop);
57
+ }
58
+ }
59
+
60
+ if (hasDontEnumBug) {
61
+ for (var i = 0; i < dontEnumsLength; i++) {
62
+ if (hasOwnProperty.call(obj,
63
+ dontEnums[i])) {
64
+ result.push(dontEnums[i]);
65
+ }
66
+ }
67
+ }
68
+ return result;
69
+ };
70
+ })();
71
+ }
72
+
73
+ // stub out lazyBind if we don't have it.
74
+ try {
75
+ angular.module('lazyBind');
76
+ lazyBindFound = true;
77
+ }
78
+ catch (e) {
79
+ angular.module('lazyBind', []).factory('$lazyBind', angular.noop);
80
+ }
81
+
82
+ /**
83
+ * @ngdoc service
84
+ * @name decipher.history.service:History
85
+ * @description
86
+ * Provides an API for keeping a history of model values.
87
+ */
88
+ angular.module('decipher.history', ['lazyBind']).service('History',
89
+ function ($parse, $rootScope, $interpolate, $lazyBind, $timeout, $log,
90
+ $injector) {
91
+ var service = this,
92
+ history = {},
93
+ pointers = {},
94
+ watches = {},
95
+ watchObjs = {},
96
+ lazyWatches = {},
97
+ descriptions = {},
98
+ // TODO: async safe?
99
+ batching = false, // whether or not we are currently in a batch
100
+ deepWatchId = 0; // incrementing ID of deep {@link decipher.history.object:Watch Watch instance}s
101
+
102
+ /**
103
+ * @ngdoc object
104
+ * @name decipher.history.object:Watch
105
+ * @overview
106
+ * @constructor
107
+ * @description
108
+ * An object instance that provides several methods for executing handlers after
109
+ * certain changes have been made.
110
+ *
111
+ * Each function return the `Watch` instance, so you can chain the calls.
112
+ *
113
+ * See the docs for {@link decipher.history.service:History#deepWatch History.deepWatch()} for an example of using these functions.
114
+ *
115
+ * @todo ability to remove all handlers at once, or all handlers of a certain type
116
+ */
117
+ var Watch = function Watch(exp, scope) {
118
+ this.exp = exp;
119
+ this.scope = scope || $rootScope;
120
+
121
+ this.$handlers = {
122
+ $change : {},
123
+ $undo : {},
124
+ $rollback : {},
125
+ $redo : {},
126
+ $revert : {},
127
+ };
128
+
129
+ this.$ignores = {};
130
+ };
131
+
132
+ /**
133
+ * @description
134
+ * Helper method for the add*Handler functions.
135
+ * @param {string} where Type of handler, corresponds to object defined in constructor
136
+ * @param {string} name Name of handler to be supplied by user
137
+ * @param {Function} fn Handler function to execute
138
+ * @param {Object} resolve Mapping of function parameters to values
139
+ * @private
140
+ * @returns {Watch} This {@link decipher.history.object:Watch Watch instance}
141
+ */
142
+ Watch.prototype._addHandler =
143
+ function _addHandler(where, name, fn, resolve) {
144
+ if (!where || !name || !fn) {
145
+ throw new Error('invalid parameters to _addHandler()');
146
+ }
147
+ this.$handlers[where][name] = {
148
+ fn: fn,
149
+ resolve: resolve || {}
150
+ };
151
+ return this;
152
+ };
153
+
154
+ /**
155
+ * @description
156
+ * Helper method for remove*Handler functions.
157
+ * @param {string} where Type of handler, corresponds to object defined in constructor
158
+ * @param {string} name Name of handler to be supplied by user
159
+ * @private
160
+ * @returns {Watch} This {@link decipher.history.object:Watch Watch instance}
161
+ */
162
+ Watch.prototype._removeHandler = function (where, name) {
163
+ if (!name) {
164
+ throw new Error('invalid parameters to _removeHandler()');
165
+ }
166
+ delete this.$handlers[where][name];
167
+ return this;
168
+ };
169
+
170
+ /**
171
+ * @ngdoc function
172
+ * @name decipher.history.object:Watch#addChangeHandler
173
+ * @methodOf decipher.history.object:Watch
174
+ * @method
175
+ * @param {string} name Unique name of handler
176
+ * @param {Function} fn Function to execute upon change
177
+ * @param {object} resolve Mapping of function parameters to values
178
+ * @description
179
+ * Adds a change handler function with name `name` to be executed
180
+ * whenever a value matching this watch's expression changes (is archived).
181
+ * @returns {Watch} This {@link decipher.history.object:Watch Watch instance}
182
+ */
183
+ Watch.prototype.addChangeHandler =
184
+ function addChangeHandler(name, fn, resolve) {
185
+ if (!name || !fn) {
186
+ throw new Error('invalid parameters');
187
+ }
188
+ return this._addHandler('$change', name, fn, resolve);
189
+ };
190
+ /**
191
+ * @ngdoc function
192
+ * @name decipher.history.object:Watch#addUndoHandler
193
+ * @methodOf decipher.history.object:Watch
194
+ * @method
195
+ * @param {string} name Unique name of handler
196
+ * @param {Function} fn Function to execute upon change
197
+ * @param {object} resolve Mapping of function parameters to values
198
+ * @description
199
+ * Adds an undo handler function with name `name` to be executed
200
+ * whenever a value matching this watch's expression is undone.
201
+ * @returns {Watch} This {@link decipher.history.object:Watch Watch instance}
202
+ */
203
+ Watch.prototype.addUndoHandler =
204
+ function addUndoHandler(name, fn, resolve) {
205
+ if (!name || !fn) {
206
+ throw new Error('invalid parameters');
207
+ }
208
+ return this._addHandler('$undo', name, fn, resolve);
209
+ };
210
+ /**
211
+ * @ngdoc function
212
+ * @name decipher.history.object:Watch#addRedoHandler
213
+ * @methodOf decipher.history.object:Watch
214
+ * @method
215
+ * @param {string} name Unique name of handler
216
+ * @param {Function} fn Function to execute upon change
217
+ * @param {object} resolve Mapping of function parameters to values
218
+ * @description
219
+ * Adds a redo handler function with name `name` to be executed
220
+ * whenever a value matching this watch's expression is redone.
221
+ * @returns {Watch} This {@link decipher.history.object:Watch Watch instance}
222
+ */
223
+ Watch.prototype.addRedoHandler =
224
+ function addRedoHandler(name, fn, resolve) {
225
+ if (!name || !fn) {
226
+ throw new Error('invalid parameters');
227
+ }
228
+ return this._addHandler('$redo', name, fn, resolve);
229
+ };
230
+ /**
231
+ * @ngdoc function
232
+ * @name decipher.history.object:Watch#addRevertHandler
233
+ * @methodOf decipher.history.object:Watch
234
+ * @method
235
+ * @param {string} name Unique name of handler
236
+ * @param {Function} fn Function to execute upon change
237
+ * @param {object} resolve Mapping of function parameters to values
238
+ * @description
239
+ * Adds a revert handler function with name `name` to be executed
240
+ * whenever a value matching this watch's expression is reverted.
241
+ * @returns {Watch} This {@link decipher.history.object:Watch Watch instance}
242
+ */
243
+ Watch.prototype.addRevertHandler =
244
+ function addRevertHandler(name, fn, resolve) {
245
+ if (!name || !fn) {
246
+ throw new Error('invalid parameters');
247
+ }
248
+ return this._addHandler('$revert', name, fn, resolve);
249
+ };
250
+ /**
251
+ * @ngdoc function
252
+ * @name decipher.history.object:Watch#addRollbackHandler
253
+ * @methodOf decipher.history.object:Watch
254
+ * @method
255
+ * @param {string} name Unique name of handler
256
+ * @param {Function} fn Function to execute upon change
257
+ * @param {object} resolve Mapping of function parameters to values
258
+ * @description
259
+ * Adds a rollback handler function with name `name` to be executed
260
+ * whenever the batch tied to this watch is rolled back.
261
+ * @returns {Watch} This {@link decipher.history.object:Watch Watch instance}
262
+ */
263
+ Watch.prototype.addRollbackHandler =
264
+ function addRollbackHandler(name, fn, resolve) {
265
+ if (!name || !fn) {
266
+ throw new Error('invalid parameters');
267
+ }
268
+ return this._addHandler('$rollback', name, fn, resolve);
269
+ };
270
+
271
+ /**
272
+ * @ngdoc function
273
+ * @name decipher.history.object:Watch#removeRevertHandler
274
+ * @methodOf decipher.history.object:Watch
275
+ * @method
276
+ * @param {string} name Name of handler to remove
277
+ * @returns {Watch} This {@link decipher.history.object:Watch Watch instance}
278
+ * @description
279
+ * Removes a revert handler with name `name`.
280
+ */
281
+ Watch.prototype.removeRevertHandler = function removeRevertHandler(name) {
282
+ if (!name) {
283
+ throw new Error('invalid parameters');
284
+ }
285
+ return this._removeHandler('$revert', name);
286
+ };
287
+ /**
288
+ * @ngdoc function
289
+ * @name decipher.history.object:Watch#removeChangeHandler
290
+ * @methodOf decipher.history.object:Watch
291
+ * @method
292
+ * @param {string} name Name of handler to remove
293
+ * @returns {Watch} This {@link decipher.history.object:Watch Watch instance}
294
+ * @description
295
+ * Removes a change handler with name `name`.
296
+ */
297
+ Watch.prototype.removeChangeHandler = function removeChangeHandler(name) {
298
+ if (!name) {
299
+ throw new Error('invalid parameters');
300
+ }
301
+ return this._removeHandler('$change', name);
302
+ };
303
+ /**
304
+ * @ngdoc function
305
+ * @name decipher.history.object:Watch#removeUndoHandler
306
+ * @methodOf decipher.history.object:Watch
307
+ * @method
308
+ * @param {string} name Name of handler to remove
309
+ * @returns {Watch} This {@link decipher.history.object:Watch Watch instance}
310
+ * @description
311
+ * Removes a undo handler with name `name`.
312
+ */
313
+ Watch.prototype.removeUndoHandler = function removeUndoHandler(name) {
314
+ if (!name) {
315
+ throw new Error('invalid parameters');
316
+ }
317
+ return this._removeHandler('$undo', name);
318
+ };
319
+
320
+ /**
321
+ * @ngdoc function
322
+ * @name decipher.history.object:Watch#removeRollbackHandler
323
+ * @methodOf decipher.history.object:Watch
324
+ * @method
325
+ * @param {string} name Name of handler to remove
326
+ * @returns {Watch} This {@link decipher.history.object:Watch Watch instance}
327
+ * @description
328
+ * Removes a rollback handler with name `name`.
329
+ */
330
+ Watch.prototype.removeRollbackHandler =
331
+ function removeRollbackHandler(name) {
332
+ return this._removeHandler('$rollback', name);
333
+ };
334
+
335
+ /**
336
+ * @ngdoc function
337
+ * @name decipher.history.object:Watch#removeRedoHandler
338
+ * @methodOf decipher.history.object:Watch
339
+ * @method
340
+ * @param {string} name Name of handler to remove
341
+ * @returns {Watch} This {@link decipher.history.object:Watch Watch instance}
342
+ * @description
343
+ * Removes a redo handler with name `name`.
344
+ */
345
+ Watch.prototype.removeRedoHandler =
346
+ function removeRedoHandler(name) {
347
+ if (!name) {
348
+ throw new Error('invalid parameters');
349
+ }
350
+ return this._removeHandler('$redo', name);
351
+ };
352
+
353
+ /**
354
+ * Fires all handlers for a particular type, optionally w/ a scope.
355
+ * @param {string} where Watch type
356
+ * @param {string} exp Expression
357
+ * @param {Scope} [scope] Optional Scope
358
+ * @private
359
+ */
360
+ Watch.prototype._fireHandlers =
361
+ function _fireHandlers(where, exp, scope) {
362
+ var hasScope = isDefined(scope),
363
+ localScope = this.scope, that = this;
364
+ forEach(this.$handlers[where], function (handler) {
365
+ var locals = {
366
+ $locals: localScope
367
+ };
368
+ if (isDefined(scope)) {
369
+ locals.$locals = scope;
370
+ }
371
+ if (isDefined(exp)) {
372
+ locals.$expression = exp;
373
+ }
374
+ forEach(handler.resolve, function (value, key) {
375
+ if (hasScope) {
376
+ locals[key] = $parse(value)(scope);
377
+ } else {
378
+ locals[key] = value;
379
+ }
380
+ });
381
+ $injector.invoke(handler.fn, scope || that, locals);
382
+ });
383
+ };
384
+
385
+ /**
386
+ * Fires the change handlers
387
+ * @param {Scope} scope Scope
388
+ * @param {string} exp Expression
389
+ * @private
390
+ */
391
+ Watch.prototype._fireChangeHandlers =
392
+ function _fireChangeHandlers(exp, scope) {
393
+ this._fireHandlers('$change', exp, scope);
394
+ };
395
+
396
+ /**
397
+ * Fires the undo handlers
398
+ * @param {Scope} scope Scope
399
+ * @param {string} exp Expression
400
+ * @private
401
+ */
402
+ Watch.prototype._fireUndoHandlers =
403
+ function _fireUndoHandlers(exp, scope) {
404
+ this._fireHandlers('$undo', exp, scope);
405
+ };
406
+
407
+ /**
408
+ * Fires the redo handlers
409
+ * @param {Scope} scope Scope
410
+ * @param {string} exp Expression
411
+ * @private
412
+ */
413
+ Watch.prototype._fireRedoHandlers =
414
+ function _fireRedoHandlers(exp, scope) {
415
+ this._fireHandlers('$redo', exp, scope);
416
+ };
417
+
418
+ /**
419
+ * Fires the revert handlers
420
+ * @param {Scope} scope Scope
421
+ * @param {string} exp Expression
422
+ * @private
423
+ */
424
+ Watch.prototype._fireRevertHandlers =
425
+ function _fireRevertHandlers(exp, scope) {
426
+ this._fireHandlers('$revert', exp, scope);
427
+ };
428
+
429
+ /**
430
+ * Fires the rollback handlers (note lack of scope and expression)
431
+ * @private
432
+ */
433
+ Watch.prototype._fireRollbackHandlers =
434
+ function _fireRollbackHandlers() {
435
+ this._fireHandlers('$rollback');
436
+ };
437
+
438
+ /**
439
+ * Decline to broadcast an event for this Watch.
440
+ * @param {string} eventName Name of event to avoid. i.e. "History.archived"
441
+ * @param {Function=} callback Optional callback
442
+ * @param {Object=} resolve Optional mapping of parameters to invoke
443
+ * the callback with.
444
+ * @returns {Watch} this Watch object
445
+ */
446
+ Watch.prototype.ignoreEvent =
447
+ function ignoreEvent(eventName, callback, resolve) {
448
+ // special case; we cannot ignore History.archived within a Watch obj
449
+ // created from a batch. there may be a way around this.
450
+ if (this.exp === null && eventName === 'History.archived') {
451
+ $log.warn('cannot ignore History.archived event for batch');
452
+ return this;
453
+ }
454
+ resolve = resolve || {};
455
+ if (isFunction(callback)) {
456
+ this.$ignores[eventName] = {
457
+ callback: callback,
458
+ resolve: resolve
459
+ };
460
+ } else if (isDefined(callback)) {
461
+ this.$ignores[eventName] = {
462
+ callback: function cb() {
463
+ return callback;
464
+ },
465
+ resolve: resolve
466
+ };
467
+ }
468
+ return this;
469
+ };
470
+
471
+ /**
472
+ * Broadcasts an event, taking ignored events into account.
473
+ * @param {string} eventName Event to broadcast
474
+ * @param {*} data Some data to pass
475
+ * @private
476
+ */
477
+ Watch.prototype._broadcast = function _broadcast(eventName, data) {
478
+ var ignore = this.$ignores[eventName];
479
+ if (!ignore ||
480
+ (isFunction(ignore.callback) &&
481
+ !$injector.invoke(ignore.callback, this.scope, ignore.resolve))) {
482
+ $rootScope.$broadcast(eventName, data);
483
+ }
484
+ };
485
+
486
+ /**
487
+ * Undoes last change against this watch object's target.
488
+ */
489
+ Watch.prototype.undo = function undo() {
490
+ if (this.exp === null) {
491
+ $log.warn("attempt to undo a batch; use rollback() instead");
492
+ return;
493
+ }
494
+ service.undo(this.exp, this.scope);
495
+ };
496
+
497
+ /**
498
+ * Redoes last undo against this watch object's target.
499
+ */
500
+ Watch.prototype.redo = function redo() {
501
+ if (this.exp === null) {
502
+ $log.warn("attempt to redo a batch; just execute the batch callback again");
503
+ }
504
+ service.redo(this.exp, this.scope);
505
+ };
506
+
507
+ /**
508
+ * Reverts this target's watch object.
509
+ * @param {number=0} pointer Pointer to revert to
510
+ */
511
+ Watch.prototype.revert = function revert(pointer) {
512
+ if (this.exp === null) {
513
+ $log.warn("attempt to revert a batch; use rollback() instead");
514
+ }
515
+ service.revert(this.exp, this.scope, pointer);
516
+ };
517
+
518
+ /**
519
+ * Whether or not you may undo this watch object's target
520
+ * @returns {boolean}
521
+ */
522
+ Watch.prototype.canUndo = function canUndo() {
523
+ return this.exp === null ? false :
524
+ service.canUndo(this.exp, this.scope);
525
+ };
526
+
527
+ /**
528
+ * Whether or not you may redo this watch object's target
529
+ * @returns {boolean}
530
+ */
531
+ Watch.prototype.canRedo = function canRedo() {
532
+ return this.exp === null ? false :
533
+ service.canRedo(this.exp, this.scope);
534
+ };
535
+
536
+ /**
537
+ * Evaluates an expression on the scope lazily. That means it will return
538
+ * a new value every DEFAULT_TIMEOUT ms at maximum, even if you change it between
539
+ * now and then. This allows us to $broadcast at an interval instead of after
540
+ * every scope change.
541
+ * @param {Object} scope AngularJS Scope
542
+ * @param {string} exp AngularJS expression to evaluate
543
+ * @param {number} [timeout=DEFAULT_TIMEOUT] How often to change the value
544
+ * @returns {Function}
545
+ */
546
+ var lazyWatch = function lazyWatch(scope, exp, timeout) {
547
+ var bind = $lazyBind(scope);
548
+ bind.cacheTime(timeout || DEFAULT_TIMEOUT);
549
+
550
+ /**
551
+ * This is the "expression function" we use to $watch with. You normally
552
+ * $watch a string, but you can also watch a function, and this is one of
553
+ * those functions. This is where the actual lazy evaluation happens.
554
+ */
555
+ return function () {
556
+ return bind.call(scope, exp);
557
+ };
558
+ };
559
+
560
+ /**
561
+ * Initializes object stores for a Scope id
562
+ * @param {string} id Sccope id
563
+ * @private
564
+ */
565
+ this._initStores = function _initStores(id) {
566
+ if (isUndefined(watches[id])) {
567
+ watches[id] = {};
568
+ }
569
+ if (isUndefined(lazyWatches[id])) {
570
+ lazyWatches[id] = {};
571
+ }
572
+ if (isUndefined(descriptions[id])) {
573
+ descriptions[id] = {};
574
+ }
575
+ if (isUndefined(history[id])) {
576
+ history[id] = {};
577
+ }
578
+ if (isUndefined(watchObjs[id])) {
579
+ watchObjs[id] = {};
580
+ }
581
+ if (isUndefined(pointers[id])) {
582
+ pointers[id] = {};
583
+ }
584
+ };
585
+
586
+ /**
587
+ * When an expression changes, store the information about it
588
+ * and increment a pointer.
589
+ * @param {string|Function} exp Expression
590
+ * @param {string} id Scope $id
591
+ * @param {Scope} locals AngularJS scope
592
+ * @param {boolean} pass Whether or not to pass on the first call
593
+ * @param {string} description AngularJS string to interpolate
594
+ * @return {Function} Watch function
595
+ * @private
596
+ */
597
+ this._archive = function (exp, id, locals, pass, description) {
598
+ var _initStores = this._initStores;
599
+ return function (newVal, oldVal) {
600
+ var watchObj;
601
+ _initStores(id);
602
+ if (description) {
603
+ descriptions[id][exp] = $interpolate(description)(locals);
604
+ }
605
+ if (pass) {
606
+ pass = false;
607
+ return;
608
+ }
609
+ if (isUndefined(history[id][exp])) {
610
+ history[id][exp] = [];
611
+ }
612
+ if (isUndefined(pointers[id][exp])) {
613
+ pointers[id][exp] = 0;
614
+ }
615
+ history[id][exp].splice(pointers[id][exp] + 1);
616
+ history[id][exp].push(copy(newVal));
617
+ pointers[id][exp] = history[id][exp].length - 1;
618
+ if (pointers[id][exp] > 0 && isDefined(watchObjs[id]) &&
619
+ isDefined(watchObj = watchObjs[id][exp])) {
620
+ if (!batching) {
621
+ watchObj._fireChangeHandlers(exp, locals);
622
+ }
623
+ watchObj._broadcast('History.archived', {
624
+ expression: exp,
625
+ newValue: newVal,
626
+ oldValue: oldVal,
627
+ description: descriptions[id][exp],
628
+ locals: locals
629
+ });
630
+ }
631
+ };
632
+ };
633
+
634
+ /**
635
+ * @ngdoc function
636
+ * @name decipher.history.service:History#watch
637
+ * @method
638
+ * @methodOf decipher.history.service:History
639
+ * @description
640
+ * Register some expression(s) for watching.
641
+ * @param {string|string[]} exps Array of expressions or one expression as a string
642
+ * @param {Scope=} scope Scope; defaults to `$rootScope`
643
+ * @param {string=} description Description of this change
644
+ * @param {Object=} lazyOptions Options for lazy loading. Only valid
645
+ * property is `timeout` at this point
646
+ * @returns {Watch|Array} {@link decipher.history.object:Watch Watch instance} or array of them
647
+ *
648
+ * @example
649
+ * <example module="decipher.history">
650
+ <file name="script.js">
651
+
652
+ angular.module('decipher.history')
653
+ .run(function(History, $rootScope) {
654
+ $rootScope.foo = 'foo';
655
+
656
+ $rootScope.$on('History.archived', function(evt, data) {
657
+ $rootScope.message = data.description;
658
+ });
659
+
660
+ History.watch('foo', $rootScope, 'you changed the foo');
661
+ });
662
+ </file>
663
+ <file name="index.html">
664
+ <input type="text" ng-model="foo"/> {{foo}}<br/>
665
+ <span ng-show="message">{{message}}</span><br/>
666
+ </file>
667
+ </example>
668
+ */
669
+ this.watch = function watch(exps, scope, description, lazyOptions) {
670
+ if (isUndefined(exps)) {
671
+ throw new Error('expression required');
672
+ }
673
+ scope = scope || $rootScope;
674
+ description = description || '';
675
+ var i,
676
+ id = scope.$id,
677
+ exp,
678
+ objs = [],
679
+ watchObj,
680
+ model;
681
+
682
+ if (!isArray(exps)) {
683
+ exps = [exps];
684
+ }
685
+
686
+ this._initStores(id);
687
+
688
+ i = exps.length;
689
+ while (i--) {
690
+ exp = exps[i];
691
+
692
+ // assert we have an assignable model
693
+ model = $parse(exp);
694
+ if (isUndefined(model.assign)) {
695
+ throw 'expression "' + exp +
696
+ '" is not an assignable expression';
697
+ }
698
+
699
+ // blast any old watches
700
+ if (isFunction(watches[id][exp])) {
701
+ watches[id][exp]();
702
+ }
703
+
704
+ descriptions[id][exp] = $interpolate(description)(scope);
705
+
706
+ this._watch(exp, scope, false, lazyOptions);
707
+ watchObjs[id][exp] = watchObj = new Watch(exp, scope);
708
+ objs.push(watchObj);
709
+ }
710
+
711
+ return objs.length > 1 ? objs : objs[0];
712
+ };
713
+
714
+ /**
715
+ * @ngdoc function
716
+ * @name decipher.history.service:History#deepWatch
717
+ * @method
718
+ * @methodOf decipher.history.service:History
719
+ * @description
720
+ * Allows you to watch an entire array/object full of objects, but only watch
721
+ * a certain property of each object.
722
+ *
723
+ * @example
724
+ * <example module="decipher.history">
725
+ <file name="script.js">
726
+ angular.module('decipher.history')
727
+ .run(function(History, $rootScope) {
728
+ var exp, locals;
729
+
730
+ $rootScope.foos = [
731
+ {id: 1, name: 'herp'},
732
+ {id: 2, name: 'derp'}
733
+ ];
734
+
735
+ $rootScope.$on('History.archived', function(evt, data) {
736
+ $rootScope.message = data.description;
737
+ exp = data.expression;
738
+ locals = data.locals;
739
+ })
740
+
741
+ History.deepWatch('foo.name for foo in foos', $rootScope,
742
+ 'Changed {{foo.id}} to name "{{foo.name}}"')
743
+ .addChangeHandler('myChangeHandler', function($expression,
744
+ $locals, foo) {
745
+ console.log(foo);
746
+ console.log("(totally hit the server and update the model)");
747
+ $rootScope.undo = function() {
748
+ History.undo($expression, $locals);
749
+ };
750
+ $rootScope.canUndo = function() {
751
+ return History.canUndo($expression, $locals);
752
+ };
753
+ }, {foo: 'foo'});
754
+ });
755
+ </file>
756
+ <file name="index.html">
757
+ <input type="text" ng-model="foos[0].name"/> {{foos[0].name}}<br/>
758
+ <span ng-show="message">{{message}}</span><br/>
759
+ <button ng-disabled="!canUndo()" ng-click="undo()">Undo!</button>
760
+ </file>
761
+ </example>
762
+ * @param {(string|string[])} exp Expression or array of expressions to watch
763
+ * @param {Scope=} scope Scope; defaults to `$rootScope`
764
+ * @param {string=} description Description of this change
765
+ * @param {Object=} lazyOptions Options for lazy loading. Only valid
766
+ * property is `timeout` at this point
767
+ * @return {Watch} {@link decipher.history.object:Watch Watch instance}
768
+ */
769
+ this.deepWatch =
770
+ function deepWatch(exp, scope, description, lazyOptions) {
771
+ var match,
772
+ targetName,
773
+ valueFn,
774
+ keyName,
775
+ value,
776
+ valueName,
777
+ valuesName,
778
+ watchObj,
779
+ id = scope.$id,
780
+ _clear = bind(this, this._clear),
781
+ _initStores = this._initStores,
782
+ _archive = bind(this, this._archive),
783
+ createDeepWatch = function createDeepWatch(targetName, valueName,
784
+ keyName, watchObj) {
785
+ return function (values) {
786
+ forEach(values, function (v, k) {
787
+
788
+ var locals = scope.$new(),
789
+ id = locals.$id;
790
+ locals.$$deepWatchId = scope.$$deepWatch[targetName];
791
+ locals.$$deepWatchTargetName = targetName;
792
+ locals[valueName] = v;
793
+ if (keyName) {
794
+ locals[keyName] = k;
795
+ }
796
+ value = valueFn(scope, locals);
797
+
798
+ _initStores(id);
799
+
800
+ descriptions[id][exp] = $interpolate(description)(locals);
801
+
802
+ if (isFunction(watches[id][targetName])) {
803
+ watches[id][targetName]();
804
+ }
805
+
806
+ if (lazyBindFound && isObject(lazyOptions)) {
807
+ watches[id][targetName] =
808
+ locals.$watch(lazyWatch(locals, targetName,
809
+ lazyOptions.timeout || 500),
810
+ _archive(targetName, id, locals, false, description),
811
+ true);
812
+ lazyWatches[id][targetName] = true;
813
+ }
814
+ else {
815
+ watches[id][targetName] = locals.$watch(targetName,
816
+ _archive(targetName, id, locals, false, description),
817
+ true);
818
+ lazyWatches[id][targetName] = false;
819
+ }
820
+
821
+ watchObjs[id][targetName] = watchObj;
822
+
823
+ locals.$on('$destroy', function () {
824
+ _clear(scope);
825
+ });
826
+
827
+ });
828
+
829
+ };
830
+ };
831
+
832
+ description = description || '';
833
+ if (!(match = exp.match(DEEPWATCH_EXP))) {
834
+ throw 'expected expression in form of "_select_ for (_key_,)? _value_ in _collection_" but got "' +
835
+ exp + '"';
836
+ }
837
+ targetName = match[1];
838
+ valueName = match[4] || match[2];
839
+ valueFn = $parse(valueName);
840
+ keyName = match[3];
841
+ valuesName = match[5];
842
+
843
+ if (isUndefined(scope.$$deepWatch)) {
844
+ scope.$$deepWatch = {};
845
+ }
846
+
847
+ // if we already have a deepWatch on this value, we
848
+ // need to kill all the child scopes. because reasons
849
+ if (isDefined(scope.$$deepWatch[targetName])) {
850
+ _clear(scope, targetName);
851
+ }
852
+ scope.$$deepWatch[targetName] = ++deepWatchId;
853
+
854
+ _initStores(id);
855
+ watchObjs[id][targetName] = watchObj = new Watch(targetName, scope);
856
+
857
+ // TODO: assert this doesn't leak memory like crazy. it might if
858
+ // we remove things from the values context.
859
+ watches[id][targetName] = scope.$watchCollection(valuesName,
860
+ createDeepWatch(targetName, valueName, keyName,
861
+ watchObj));
862
+
863
+ return watchObj;
864
+ };
865
+
866
+ /**
867
+ * Clears a bunch of information for a scope and optionally an array of expressions.
868
+ * Lacking an expression, this will eliminate an entire scopesworth of data.
869
+ * It will recognize deep watches and clear them out completely.
870
+ * @param {Scope} scope Scope obj
871
+ * @param {(string|string[])} exps Expression or array of expressions
872
+ * @private
873
+ */
874
+ this._clear = function _clear(scope, exps) {
875
+ var id = scope.$id,
876
+ i,
877
+ nextSibling,
878
+ exp,
879
+ clear = function clear(id, key) {
880
+ var zap = function zap(what) {
881
+ if (isDefined(what[id][key])) {
882
+ delete what[id][key];
883
+ if (Object.keys(what[id]).length === 0) {
884
+ delete what[id];
885
+ }
886
+ }
887
+ };
888
+
889
+ if (isDefined(watches[id]) &&
890
+ isFunction(watches[id][key])) {
891
+ watches[id][key]();
892
+ }
893
+ if (isDefined(watches[id])) {
894
+ zap(watches);
895
+ }
896
+ if (isDefined(watchObjs[id])) {
897
+ zap(watchObjs);
898
+ }
899
+ if (isDefined(history[id])) {
900
+ zap(history);
901
+ }
902
+ if (isDefined(pointers[id])) {
903
+ zap(pointers);
904
+ }
905
+ if (isDefined(lazyWatches[id])) {
906
+ zap(lazyWatches);
907
+ }
908
+ },
909
+
910
+ clearAll = function clearAll(id) {
911
+ forEach(watches[id], function (watch) {
912
+ return isFunction(watch) && watch();
913
+ });
914
+ delete watches[id];
915
+ delete history[id];
916
+ delete pointers[id];
917
+ delete lazyWatches[id];
918
+ delete watchObjs[id];
919
+ };
920
+
921
+ if (isString(exps)) {
922
+ exps = [exps];
923
+ }
924
+ else if (isUndefined(exps) && isDefined(watches[id])) {
925
+ exps = Object.keys(watches[id]);
926
+ }
927
+
928
+ if (isDefined(exps)) {
929
+ i = exps.length;
930
+ while (i--) {
931
+ exp = exps[i];
932
+ clear(id, exp);
933
+ }
934
+ } else {
935
+ clearAll(id);
936
+ }
937
+ nextSibling = scope.$$childHead;
938
+ while (nextSibling) {
939
+ this._clear(nextSibling, exp);
940
+ nextSibling = nextSibling.$$nextSibling;
941
+ }
942
+ };
943
+
944
+
945
+ /**
946
+ * @ngdoc function
947
+ * @name decipher.history.service:History#forget
948
+ * @method
949
+ * @methodOf decipher.history.service:History
950
+ * @description
951
+ * Unregister some watched expression(s).
952
+ * @param {(string|string[])} exps Array of expressions or one expression as a string
953
+ * @param {Scope=} scope Scope object; defaults to $rootScope
954
+ */
955
+ this.forget = function forget(scope, exps) {
956
+ scope = scope || $rootScope;
957
+ if (isDefined(exps) && isString(exps)) {
958
+ exps = [exps];
959
+ }
960
+ this._clear(scope, exps);
961
+ };
962
+
963
+ /**
964
+ * Internal function to change some value in the stack to another.
965
+ * Kills the watch and then calls `_watch()` to restore it.
966
+ * @param {Scope} scope Scope object
967
+ * @param {string} exp AngularJS expression
968
+ * @param {array} stack History stack; see `History.history`
969
+ * @param {number} pointer Pointer
970
+ * @returns {{oldValue: {*}, newValue: {*}}} The old value and the new value
971
+ * @private
972
+ */
973
+ this._do = function _do(scope, exp, stack, pointer) {
974
+ var model,
975
+ oldValue,
976
+ id = scope.$id;
977
+ if (isFunction(watches[id][exp])) {
978
+ watches[id][exp]();
979
+ delete watches[id][exp];
980
+ }
981
+ model = $parse(exp);
982
+ oldValue = model(scope);
983
+ // todo: assert there's no bug here with unassignable expressions
984
+ model.assign(scope, stack[pointer]);
985
+ this._watch(exp, scope, true);
986
+ return {
987
+ oldValue: oldValue,
988
+ newValue: model(scope)
989
+ };
990
+ };
991
+
992
+ /**
993
+ * @ngdoc function
994
+ * @name decipher.history.service:History#undo
995
+ * @method
996
+ * @methodOf decipher.history.service:History
997
+ * @description
998
+ * Undos an expression to last known value.
999
+ * @param {string} exp Expression to undo
1000
+ * @param {Scope=} scope Scope; defaults to `$rootScope`
1001
+ */
1002
+ this.undo = function undo(exp, scope) {
1003
+ scope = scope || $rootScope;
1004
+ if (isUndefined(exp)) {
1005
+ throw new Error('expression required');
1006
+ }
1007
+ var id = scope.$id,
1008
+ scopeHistory = history[id],
1009
+ stack,
1010
+ values,
1011
+ pointer,
1012
+ watchObj;
1013
+
1014
+ if (isUndefined(scopeHistory)) {
1015
+ throw 'could not find history for scope ' + id;
1016
+ }
1017
+
1018
+ stack = scopeHistory[exp];
1019
+ if (isUndefined(stack)) {
1020
+ throw 'could not find history in scope "' + id +
1021
+ ' against expression "' + exp + '"';
1022
+ }
1023
+ pointer = --pointers[id][exp];
1024
+ if (pointer < 0) {
1025
+ $log.warn('attempt to undo past history');
1026
+ pointers[id][exp]++;
1027
+ return;
1028
+ }
1029
+ values = this._do(scope, exp, stack, pointer);
1030
+ if (isDefined(watchObjs[id]) &&
1031
+ isDefined(watchObjs[id][exp])) {
1032
+ watchObj = watchObjs[id][exp];
1033
+ watchObj._fireUndoHandlers(exp, scope);
1034
+ watchObj._broadcast('History.undone', {
1035
+ expression: exp,
1036
+ newValue: values.newValue,
1037
+ oldValue: values.oldValue,
1038
+ description: descriptions[id][exp],
1039
+ scope: scope
1040
+ });
1041
+ }
1042
+ };
1043
+
1044
+ /**
1045
+ * Actually issues the appropriate scope.$watch
1046
+ * @param {string} exp Expression
1047
+ * @param {Scope=} scope Scope; defaults to $rootScope
1048
+ * @param {boolean=} pass Whether or not to skip the first watch execution. Defaults to false
1049
+ * @param {Object} lazyOptions Options to send the lazy module
1050
+ * @private
1051
+ */
1052
+ this._watch = function _watch(exp, scope, pass, lazyOptions) {
1053
+ var id;
1054
+ scope = scope || $rootScope;
1055
+ pass = pass || false;
1056
+ id = scope.$id;
1057
+
1058
+ // do we have an array or object?
1059
+ if (lazyBindFound && (isObject(lazyOptions) ||
1060
+ (lazyWatches[id] && !!lazyWatches[id][exp]))) {
1061
+ watches[id][exp] =
1062
+ scope.$watch(lazyWatch(scope, exp, lazyOptions.timeout),
1063
+ bind(this, this._archive(exp, id, scope, pass)), true);
1064
+ lazyWatches[id][exp] = true;
1065
+ }
1066
+ else {
1067
+ watches[id][exp] =
1068
+ scope.$watch(exp, bind(this, this._archive(exp, id, scope, pass)),
1069
+ true);
1070
+ lazyWatches[id][exp] = false;
1071
+ }
1072
+
1073
+ };
1074
+
1075
+ /**
1076
+ * @ngdoc function
1077
+ * @name decipher.history.service:History#redo
1078
+ * @method
1079
+ * @methodOf decipher.history.service:History
1080
+ * @description
1081
+ * Redoes (?) the last undo.
1082
+ * @param {string} exp Expression to redo
1083
+ * @param {Scope=} scope Scope; defaults to `$rootScope`
1084
+ */
1085
+ this.redo = function redo(exp, scope) {
1086
+ scope = scope || $rootScope;
1087
+ var id = scope.$id,
1088
+ stack = history[id][exp],
1089
+ values,
1090
+ pointer,
1091
+ watchObj;
1092
+
1093
+ if (isUndefined(stack)) {
1094
+ throw 'could not find history in scope "' + id +
1095
+ ' against expression "' + exp + '"';
1096
+ }
1097
+ pointer = ++pointers[id][exp];
1098
+ if (pointer === stack.length) {
1099
+ $log.warn('attempt to redo past history');
1100
+ pointers[id][exp]--;
1101
+ return;
1102
+ }
1103
+
1104
+ values = this._do(scope, exp, stack, pointer);
1105
+
1106
+ if (isDefined(watchObjs[id]) &&
1107
+ isDefined(watchObjs[id][exp])) {
1108
+ watchObj = watchObjs[id][exp];
1109
+ watchObj._fireRedoHandlers(exp, scope);
1110
+ watchObj._broadcast('History.redone', {
1111
+ expression: exp,
1112
+ oldValue: copy(values.newValue),
1113
+ newValue: copy(values.oldValue),
1114
+ description: descriptions[id][exp],
1115
+ scope: scope
1116
+ });
1117
+ }
1118
+ };
1119
+
1120
+ /**
1121
+ * @ngdoc function
1122
+ * @name decipher.history.service:History#canUndo
1123
+ * @method
1124
+ * @methodOf decipher.history.service:History
1125
+ * @description
1126
+ * Whether or not we have accumulated any history for a particular expression.
1127
+ * @param {string} exp Expression
1128
+ * @param {Scope=} scope Scope; defaults to $rootScope
1129
+ * @return {boolean} Whether or not you can issue an `undo()`
1130
+ * @example
1131
+ * <example module="decipher.history">
1132
+ <file name="script.js">
1133
+ angular.module('decipher.history').run(function(History, $rootScope) {
1134
+ $rootScope.foo = 'bar';
1135
+ History.watch('foo');
1136
+ $rootScope.canUndo = History.canUndo;
1137
+ });
1138
+ </file>
1139
+ <file name="index.html">
1140
+ <input type="text" ng-model="foo"/> Can undo? {{canUndo('foo')}}
1141
+ </file>
1142
+ </example>
1143
+ */
1144
+ this.canUndo = function canUndo(exp, scope) {
1145
+ var id;
1146
+ scope = scope || $rootScope;
1147
+ id = scope.$id;
1148
+ return isDefined(pointers[id]) &&
1149
+ isDefined(pointers[id][exp]) &&
1150
+ pointers[id][exp] > 0;
1151
+ };
1152
+
1153
+ /**
1154
+ * @ngdoc function
1155
+ * @name decipher.history.service:History#canRedo
1156
+ * @method
1157
+ * @methodOf decipher.history.service:History
1158
+ * @description
1159
+ * Whether or not we can redo an expression's value.
1160
+ * @param {string} exp Expression
1161
+ * @param {Scope=} scope Scope; defaults to $rootScope
1162
+ * @return {Boolean} Whether or not you can issue a `redo()`
1163
+ * @example
1164
+ * <example module="decipher.history">
1165
+ <file name="script.js">
1166
+ angular.module('decipher.history').run(function(History, $rootScope) {
1167
+ $rootScope.foo = 'bar';
1168
+ History.watch('foo');
1169
+ $rootScope.canRedo = History.canRedo;
1170
+ $rootScope.canUndo = History.canUndo;
1171
+ $rootScope.undo = History.undo;
1172
+ });
1173
+ </file>
1174
+ <file name="index.html">
1175
+ <input type="text" ng-model="foo"/> <br/>
1176
+ <button ng-show="canUndo('foo')" ng-click="undo('foo')">Undo</button><br/>
1177
+ Can redo? {{canRedo('foo')}}
1178
+ </file>
1179
+ </example>
1180
+ */
1181
+ this.canRedo = function canRedo(exp, scope) {
1182
+ var id;
1183
+ scope = scope || $rootScope;
1184
+ id = scope.$id;
1185
+ return isDefined(pointers[id]) &&
1186
+ isDefined(pointers[id][exp]) &&
1187
+ pointers[id][exp] < history[id][exp].length - 1;
1188
+ };
1189
+
1190
+ /**
1191
+ * @ngdoc function
1192
+ * @method
1193
+ * @methodOf decipher.history.service:History
1194
+ * @name decipher.history.service:History#revert
1195
+ * @description
1196
+ * Reverts to earliest known value of some expression, or at a particular
1197
+ * pointer if you please.
1198
+ * @param {string} exp Expression
1199
+ * @param {Scope=} scope Scope; defaults to $rootScope
1200
+ * @param {number=} pointer Optional; defaults to 0
1201
+ */
1202
+ this.revert = function (exp, scope, pointer) {
1203
+ scope = scope || $rootScope;
1204
+ pointer = pointer || 0;
1205
+ var id = scope.$id,
1206
+ stack = history[id][exp],
1207
+ values,
1208
+ watchObj;
1209
+
1210
+ if (isUndefined(stack)) {
1211
+ $log.warn('nothing to revert');
1212
+ return;
1213
+ }
1214
+ values = this._do(scope, exp, stack, pointer);
1215
+
1216
+ // wait; what is this?
1217
+ history[id][exp].splice();
1218
+ pointers[id][exp] = pointer;
1219
+
1220
+ if (isDefined(watchObjs[id]) &&
1221
+ isDefined(watchObjs[id][exp])) {
1222
+ watchObj = watchObjs[id][exp];
1223
+ watchObj._fireRevertHandlers(exp, scope);
1224
+ watchObj._broadcast('History.reverted', {
1225
+ expression: exp,
1226
+ oldValue: copy(values.newValue),
1227
+ newValue: copy(values.oldValue),
1228
+ description: descriptions[id][exp],
1229
+ scope: scope,
1230
+ pointer: pointer
1231
+ });
1232
+ }
1233
+ };
1234
+
1235
+ /**
1236
+ * @ngdoc function
1237
+ * @name decipher.history.service:History#batch
1238
+ * @method
1239
+ * @methodOf decipher.history.service:History
1240
+ * @description
1241
+ * Executes a function within a batch context which can then be rolled back.
1242
+ * @param {function} fn Function to execute
1243
+ * @param {Scope=} scope Scope object; defaults to `$rootScope`
1244
+ * @param {string=} description Description of this change
1245
+ * @returns {Watch} {@link decipher.history.object:Watch Watch instance}
1246
+ * @example
1247
+ <example module="decipher.history">
1248
+ <file name="script.js">
1249
+ angular.module('decipher.history').run(function(History, $rootScope) {
1250
+ var t;
1251
+
1252
+ $rootScope.herp = 'derp';
1253
+ $rootScope.bar = 'baz';
1254
+ $rootScope.frick = 'frack';
1255
+
1256
+ $rootScope.$on('History.batchEnded', function(evt, data) {
1257
+ t = data.transaction;
1258
+ });
1259
+
1260
+ History.watch('herp');
1261
+ History.watch('bar');
1262
+ History.watch('frick');
1263
+
1264
+ $rootScope.batch = function() {
1265
+ History.batch(function() {
1266
+ $rootScope.herp = 'derp2';
1267
+ $rootScope.bar = 'baz2';
1268
+ $rootScope.frick = 'frack2';
1269
+ })
1270
+ .addRollbackHandler('myRollbackHandler', function() {
1271
+ $rootScope.message = 'rolled a bunch of stuff back';
1272
+ });
1273
+ $rootScope.message = "batch complete";
1274
+ };
1275
+
1276
+ $rootScope.rollback = function() {
1277
+ if (isDefined(t)) {
1278
+ History.rollback(t);
1279
+ }
1280
+ };
1281
+ });
1282
+ </file>
1283
+ <file name="index.html">
1284
+ <ul>
1285
+ <li>herp: {{herp}}</li>
1286
+ <li>bar: {{bar}}</li>
1287
+ <li>frick: {{frick}}</li>
1288
+ </ul>
1289
+ <button ng-click="batch()">Batch</button>
1290
+ <button ng-click="rollback()">Rollback</button><br/>
1291
+ {{message}}
1292
+ </file>
1293
+ </example>
1294
+ */
1295
+ this.batch = function batch(fn, scope, description) {
1296
+ var _clear = bind(this, this._clear),
1297
+ _initStores = this._initStores,
1298
+ listener,
1299
+ watchObj,
1300
+ child;
1301
+ scope = scope || $rootScope;
1302
+ if (!isFunction(fn)) {
1303
+ throw new Error('transaction requires a function');
1304
+ }
1305
+
1306
+ child = scope.$new();
1307
+ child.$on('$destroy', function () {
1308
+ _clear(child);
1309
+ });
1310
+
1311
+ listener = scope.$on('History.archived', function (evt, data) {
1312
+ var deepChild,
1313
+ exp = data.expression,
1314
+ id;
1315
+ if (data.locals.$id !== child.$id) {
1316
+ deepChild = child.$new();
1317
+ deepChild.$on('$destroy', function () {
1318
+ _clear(deepChild);
1319
+ });
1320
+ deepChild.$$locals = data.locals;
1321
+ id = deepChild.$id;
1322
+ _initStores(id);
1323
+ history[id][exp] =
1324
+ copy(history[data.locals.$id][exp]);
1325
+ pointers[id][exp] = pointers[data.locals.$id][exp] - 1;
1326
+ }
1327
+ });
1328
+
1329
+ watchObjs[child.$id] = watchObj = new Watch(null, child);
1330
+ watchObj._broadcast('History.batchBegan', {
1331
+ transaction: child,
1332
+ description: description
1333
+ });
1334
+
1335
+ // we need to put this into a timeout and apply manually
1336
+ // since it's not clear when the watchers will get fired,
1337
+ // and we must ensure that any existing watchers on the archived
1338
+ // event can be skipped before the batchEnd occurs.
1339
+ batching = true;
1340
+ $timeout(function () {
1341
+ fn(child);
1342
+ scope.$apply();
1343
+ })
1344
+ .then(function () {
1345
+ listener();
1346
+ batching = false;
1347
+ watchObj._broadcast('History.batchEnded', {
1348
+ transaction: child,
1349
+ description: description
1350
+ });
1351
+ });
1352
+
1353
+
1354
+ return watchObj;
1355
+ };
1356
+
1357
+ /**
1358
+ * @ngdoc function
1359
+ * @name decipher.history.service:History#rollback
1360
+ * @method
1361
+ * @methodOf decipher.history.service:History
1362
+ * @description
1363
+ * Rolls a transaction back that was executed via {@link decipher.history.service:History#batch batch()}.
1364
+ *
1365
+ * For an example, see {@link decipher.history.service:History#batch batch()}.
1366
+ * @param {Scope} t Scope object in which the transaction was executed.
1367
+ */
1368
+ this.rollback = function rollback(t) {
1369
+
1370
+ var _do = bind(this, this._do),
1371
+ parent = t.$parent,
1372
+ packets = {},
1373
+ nextSibling,
1374
+ watchObj,
1375
+ nextSiblingLocals;
1376
+ if (!t || !isObject(t)) {
1377
+ throw new Error('must pass a scope to rollback');
1378
+ }
1379
+
1380
+ function _rollback(scope, comparisonScope) {
1381
+ var id = scope.$id,
1382
+ comparisonScopeId = comparisonScope.$id,
1383
+ stack = history[id],
1384
+ pointer,
1385
+ descs,
1386
+ exp,
1387
+ values,
1388
+ exps,
1389
+ rolledback,
1390
+ i;
1391
+ if (stack) {
1392
+ exps = Object.keys(stack);
1393
+ i = exps.length;
1394
+ } else {
1395
+ // might not actually have history, it's ok
1396
+ return;
1397
+ }
1398
+ while (i--) {
1399
+ exp = exps[i];
1400
+ values = [];
1401
+ descs = [];
1402
+ pointer = pointers[comparisonScopeId][exp];
1403
+ rolledback = false;
1404
+ while (pointer > pointers[id][exp]) {
1405
+ pointer--;
1406
+ values.push(_do(comparisonScope,
1407
+ exp, history[comparisonScopeId][exp], pointer));
1408
+ pointers[comparisonScopeId][exp] = pointer;
1409
+ descs.push(descriptions[comparisonScopeId][exp]);
1410
+ // throw this off the history stack so
1411
+ // we don't end up with it in the stack while we
1412
+ // do normal undo() calls later against the same
1413
+ // expression and scope
1414
+ history[comparisonScopeId][exp].pop();
1415
+ rolledback = true;
1416
+ }
1417
+ if (rolledback) {
1418
+ packets[exp] = {
1419
+ values: values,
1420
+ scope: scope,
1421
+ comparisonScope: comparisonScope,
1422
+ descriptions: descs
1423
+ };
1424
+ }
1425
+ }
1426
+ }
1427
+
1428
+ watchObj = watchObjs[t.$id];
1429
+
1430
+ if (isDefined(parent) &&
1431
+ isDefined(history[parent.$id])) {
1432
+ _rollback(t, parent);
1433
+ }
1434
+ nextSibling = t.$$childHead;
1435
+ while (nextSibling) {
1436
+ nextSiblingLocals = nextSibling.$$locals;
1437
+ if (nextSiblingLocals) {
1438
+ _rollback(nextSibling, nextSiblingLocals);
1439
+ }
1440
+ nextSibling = nextSibling.$$nextSibling;
1441
+ }
1442
+ watchObj._fireRollbackHandlers();
1443
+ watchObj._broadcast('History.rolledback', packets);
1444
+
1445
+ };
1446
+
1447
+ /**
1448
+ * @ngdoc property
1449
+ * @name decipher.history.service:History#history
1450
+ * @propertyOf decipher.history.service:History
1451
+ * @description
1452
+ * The complete history stack, keyed by Scope `$id` and then expression.
1453
+ * @type {{}}
1454
+ */
1455
+ this.history = history;
1456
+
1457
+ /**
1458
+ * @ngdoc property
1459
+ * @name decipher.history.service:History#descriptions
1460
+ * @propertyOf decipher.history.service:History
1461
+ * @description
1462
+ * The complete map of change descriptions, keyed by Scope `$id` and then expression.
1463
+ * @type {{}}
1464
+ */
1465
+ this.descriptions = descriptions;
1466
+
1467
+ /**
1468
+ * @ngdoc property
1469
+ * @name decipher.history.service:History#pointers
1470
+ * @propertyOf decipher.history.service:History
1471
+ * @description
1472
+ * The complete pointer map, keyed by Scope `$id` and then expression.
1473
+ * @type {{}}
1474
+ */
1475
+ this.pointers = pointers;
1476
+
1477
+ /**
1478
+ * @ngdoc property
1479
+ * @name decipher.history.service:History#watches
1480
+ * @propertyOf decipher.history.service:History
1481
+ * @description
1482
+ * The complete index of all AngularJS `$watch`es, keyed by Scope `$id` and then expression.
1483
+ * @type {{}}
1484
+ */
1485
+ this.watches = watches;
1486
+
1487
+ /**
1488
+ * @ngdoc property
1489
+ * @name decipher.history.service:History#lazyWatches
1490
+ * @propertyOf decipher.history.service:History
1491
+ * @description
1492
+ * The complete index of all AngularJS `$watch`es designated to be "lazy", keyed by Scope `$id` and then expression.
1493
+ * @type {{}}
1494
+ */
1495
+ this.lazyWatches = lazyWatches;
1496
+
1497
+ /**
1498
+ * @ngdoc property
1499
+ * @name decipher.history.service:History#watchObjs
1500
+ * @propertyOf decipher.history.service:History
1501
+ * @description
1502
+ * The complete index of all {@link decipher.history.object:Watch Watch} objects registered, keyed by Scope `$id` and then (optionally) expression.
1503
+ * @type {{}}
1504
+ */
1505
+ this.watchObjs = watchObjs;
1506
+
1507
+ /**
1508
+ * @ngdoc property
1509
+ * @name decipher.history.service:History#Watch
1510
+ * @propertyOf decipher.history.service:History
1511
+ * @description
1512
+ * Here's the Watch prototype for you to play with.
1513
+ * @type {Watch}
1514
+ */
1515
+ this.Watch = Watch;
1516
+ });
1517
+ })();