angular-history-rails 0.7.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ })();