PageRenderTime 63ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 1ms

/Backbone.Undo.js

https://github.com/caseygrun/Backbone.Undo.js
JavaScript | 785 lines | 406 code | 29 blank | 350 comment | 77 complexity | 2bc52051d9a45c5c86df2248b6ae0889 MD5 | raw file
  1. /*!
  2. * Backbone.Undo.js v0.2
  3. *
  4. * Copyright (c)2013 Oliver Sartun
  5. * Released under the MIT License
  6. *
  7. * Documentation and full license available at
  8. * https://github.com/osartun/Backbone.Undo.js
  9. */
  10. // AMD support
  11. (function (factory) {
  12. typeof exports !== 'undefined'
  13. ? (module.exports = factory)
  14. : factory(_, Backbone);
  15. })(function (_, Backbone, undefined) {
  16. var core_slice = Array.prototype.slice;
  17. /**
  18. * As call is faster than apply, this is a faster version of apply as it uses call.
  19. *
  20. * @param {Function} fn The function to execute
  21. * @param {Object} ctx The context the function should be called in
  22. * @param {Array} args The array of arguments that should be applied to the function
  23. * @return Forwards whatever the called function returns
  24. */
  25. function apply (fn, ctx, args) {
  26. return args.length <= 4 ?
  27. fn.call(ctx, args[0], args[1], args[2], args[3]) :
  28. fn.apply(ctx, args);
  29. }
  30. /**
  31. * Uses slice on an array or an array-like object.
  32. *
  33. * @param {Array|Object} arr The array or array-like object.
  34. * @param {Number} [index] The index from where the array should be sliced. Default is 0.
  35. * @return {Array} The sliced array
  36. */
  37. function slice (arr, index) {
  38. return core_slice.call(arr, index);
  39. }
  40. /**
  41. * Checks if an object has one or more specific keys. The keys
  42. * don't have to be an owned property.
  43. * You can call this function either this way:
  44. * hasKeys(obj, ["a", "b", "c"])
  45. * or this way:
  46. * hasKeys(obj, "a", "b", "c")
  47. *
  48. * @param {Object} obj The object to check on
  49. * @param {Array} keys The keys to check for
  50. * @return {Boolean} True, if the object has all those keys
  51. */
  52. function hasKeys (obj, keys) {
  53. if (obj == null) return false;
  54. if (!_.isArray(keys)) {
  55. keys = slice(arguments, 1);
  56. }
  57. return _.all(keys, function (key) {
  58. return key in obj;
  59. });
  60. }
  61. /**
  62. * Returns a number that is unique per call stack. The number gets
  63. * changed after the call stack has been completely processed.
  64. *
  65. * @return {number} MagicFusionIndex
  66. */
  67. var getMagicFusionIndex = (function () {
  68. // If you add several models to a collection or set several
  69. // attributes on a model all in sequence and yet all for
  70. // example in one function, then several Undo-Actions are
  71. // generated.
  72. // If you want to undo your last action only the last model
  73. // would be removed from the collection or the last set
  74. // attribute would be changed back to its previous value.
  75. // To prevent that we have to figure out a way to combine
  76. // all those actions that happened "at the same time".
  77. // Timestamps aren't exact enough. A complex routine could
  78. // run several milliseconds and in that time produce a lot
  79. // of actions with different timestamps.
  80. // Instead we take advantage of the single-threadedness of
  81. // JavaScript:
  82. var callstackWasIndexed = false, magicFusionIndex = -1;
  83. function indexCycle() {
  84. magicFusionIndex++;
  85. callstackWasIndexed = true;
  86. _.defer(function () {
  87. // Here comes the magic. With a Timeout of 0
  88. // milliseconds this function gets called whenever
  89. // the current callstack is completed
  90. callstackWasIndexed = false;
  91. })
  92. }
  93. return function () {
  94. if (!callstackWasIndexed) {
  95. indexCycle();
  96. }
  97. return magicFusionIndex;
  98. }
  99. })();
  100. /**
  101. * To prevent binding a listener several times to one
  102. * object, we register the objects in an ObjectRegistry
  103. *
  104. * @constructor
  105. */
  106. function ObjectRegistry () {
  107. // This uses two different ways of storing
  108. // objects: In case the object has a cid
  109. // (which Backbone objects typically have)
  110. // it uses this cid as an index. That way
  111. // the Array's length attribute doesn't
  112. // change and the object isn't an item
  113. // in the array, but an object-property.
  114. // Otherwise it's added to the Array as an
  115. // item.
  116. // That way we can use the fast property-
  117. // lookup and only have to fall back to
  118. // iterating over the array in case
  119. // non-Backbone-objects are registered.
  120. this.registeredObjects = [];
  121. // To return a list of all registered
  122. // objects in the 'get' method we have to
  123. // store the objects that have a cid in
  124. // an additional array.
  125. this.cidIndexes = [];
  126. }
  127. ObjectRegistry.prototype = {
  128. /**
  129. * Returns whether the object is already registered in this ObjectRegistry or not.
  130. *
  131. * @this {ObjectRegistry}
  132. * @param {Object} obj The object to check
  133. * @return {Boolean} True if the object is already registered
  134. */
  135. isRegistered: function (obj) {
  136. // This is where we get a performance boost
  137. // by using the two different ways of storing
  138. // objects.
  139. return obj && obj.cid ? this.registeredObjects[obj.cid] : _.contains(this.registeredObjects, obj);
  140. },
  141. /**
  142. * Registers an object in this ObjectRegistry.
  143. *
  144. * @this {ObjectRegistry}
  145. * @param {Object} obj The object to register
  146. * @return {undefined}
  147. */
  148. register: function (obj) {
  149. if (!this.isRegistered(obj)) {
  150. if (obj && obj.cid) {
  151. this.registeredObjects[obj.cid] = obj;
  152. this.cidIndexes.push(obj.cid);
  153. } else {
  154. this.registeredObjects.push(obj);
  155. }
  156. return true;
  157. }
  158. return false;
  159. },
  160. /**
  161. * Unregisters an object from this ObjectRegistry.
  162. *
  163. * @this {ObjectRegistry}
  164. * @param {Object} obj The object to unregister
  165. * @return {undefined}
  166. */
  167. unregister: function (obj) {
  168. if (this.isRegistered(obj)) {
  169. if (obj && obj.cid) {
  170. delete this.registeredObjects[obj.cid];
  171. this.cidIndexes.splice(_.indexOf(this.cidIndexes, obj.cid), 1);
  172. } else {
  173. var i = _.indexOf(this.registeredObjects, obj);
  174. this.registeredObjects.splice(i, 1);
  175. }
  176. return true;
  177. }
  178. return false;
  179. },
  180. /**
  181. * Returns an array of all objects that are currently in this ObjectRegistry.
  182. *
  183. * @return {Array} An array of all the objects which are currently in the ObjectRegistry
  184. */
  185. get: function () {
  186. return (_.map(this.cidIndexes, function (cid) {return this.registeredObjects[cid];}, this)).concat(this.registeredObjects);
  187. }
  188. }
  189. /**
  190. * Binds or unbinds the "all"-listener for one or more objects.
  191. *
  192. * @param {String} which Either "on" or "off"
  193. * @param {Object[]} objects Array of the objects on which the "all"-listener should be bound / unbound to
  194. * @param {Function} [fn] The function that should be bound / unbound. Optional in case of "off"
  195. * @param {Object} [ctx] The context the function should be called in
  196. * @return {undefined}
  197. */
  198. function onoff(which, objects, fn, ctx) {
  199. for (var i = 0, l = objects.length, obj; i < l; i++) {
  200. obj = objects[i];
  201. if (!obj) continue;
  202. if (which === "on") {
  203. if (!ctx.objectRegistry.register(obj)) {
  204. // register returned false, so obj was already registered
  205. continue;
  206. }
  207. } else {
  208. if (!ctx.objectRegistry.unregister(obj)) {
  209. // unregister returned false, so obj wasn't registered
  210. continue;
  211. }
  212. }
  213. if (_.isFunction(obj[which])) {
  214. obj[which]("all", fn, ctx);
  215. }
  216. }
  217. }
  218. /**
  219. * Calls the undo/redo-function for a specific action.
  220. *
  221. * @param {String} which Either "undo" or "redo"
  222. * @param {Object} action The Action's attributes
  223. * @return {undefined}
  224. */
  225. function actionUndoRedo (which, action) {
  226. var type = action.type, undoTypes = action.undoTypes, fn = !undoTypes[type] || undoTypes[type][which];
  227. if (_.isFunction(fn)) {
  228. fn(action.object, action.before, action.after, action.options);
  229. }
  230. }
  231. /**
  232. * The main undo/redo function.
  233. *
  234. * @param {String} which Either "undo" or "redo"
  235. * @param {UndoManager} manager The UndoManager-instance on which an "undo"/"redo"-Event is triggered afterwards
  236. * @param {UndoStack} stack The UndoStack on which we perform
  237. * @param {Boolean} magic If true, undoes / redoes all actions with the same magicFusionIndex
  238. * @return {undefined}
  239. */
  240. function managerUndoRedo (which, manager, stack, magic) {
  241. if (stack.isCurrentlyUndoRedoing ||
  242. (which === "undo" && stack.pointer === -1) ||
  243. (which === "redo" && stack.pointer === stack.length - 1)) {
  244. // We're either currently in an undo- / redo-process or
  245. // we reached the end of the stack
  246. return;
  247. }
  248. stack.isCurrentlyUndoRedoing = true;
  249. var action, actions, isUndo = which === "undo";
  250. if (isUndo) {
  251. action = stack.at(stack.pointer);
  252. stack.pointer--;
  253. } else {
  254. stack.pointer++;
  255. action = stack.at(stack.pointer);
  256. }
  257. actions = magic ? stack.where({"magicFusionIndex": action.get("magicFusionIndex")}) : [action];
  258. stack.pointer += (isUndo ? -1 : 1) * (actions.length - 1);
  259. while (action = isUndo ? actions.pop() : actions.shift()) {
  260. // Here we're calling the Action's undo / redo method
  261. action[which]();
  262. }
  263. stack.isCurrentlyUndoRedoing = false;
  264. manager.trigger(which, manager);
  265. }
  266. /**
  267. * Checks whether an UndoAction should be created or not. Therefore it checks
  268. * whether a "condition" property is set in the undoTypes-object of the specific
  269. * event type. If not, it returns true. If it's set and a boolean, it returns it.
  270. * If it's a function, it returns its result, converting it into a boolean.
  271. * Otherwise it returns true.
  272. *
  273. * @param {Object} undoTypesType The object within the UndoTypes that holds the function for this event type (i.e. "change")
  274. * @param {Arguments} args The arguments the "condition" function is called with
  275. * @return {Boolean} True, if an UndoAction should be created
  276. */
  277. function validateUndoActionCreation (undoTypesType, args) {
  278. var condition = undoTypesType.condition, type = typeof condition;
  279. return type === "function" ? !!apply(condition, undoTypesType, args) :
  280. type === "boolean" ? condition : true;
  281. }
  282. /**
  283. * Adds an Undo-Action to the stack.
  284. *
  285. * @param {UndoStack} stack The undostack the action should be added to.
  286. * @param {String} type The event type (i.e. "change")
  287. * @param {Arguments} args The arguments passed to the undoTypes' "on"-handler
  288. * @param {OwnedUndoTypes} undoTypes The undoTypes-object which has the "on"-handler
  289. * @return {undefined}
  290. */
  291. function addToStack(stack, type, args, undoTypes) {
  292. if (stack.track && !stack.isCurrentlyUndoRedoing && type in undoTypes &&
  293. validateUndoActionCreation(undoTypes[type], args)) {
  294. // An UndoAction should be created
  295. var res = apply(undoTypes[type]["on"], undoTypes[type], args), diff;
  296. if (hasKeys(res, "object", "before", "after")) {
  297. res.type = type;
  298. res.magicFusionIndex = getMagicFusionIndex();
  299. res.undoTypes = undoTypes;
  300. if (stack.pointer < stack.length - 1) {
  301. // New Actions must always be added to the end of the stack.
  302. // If the pointer is not pointed to the last action in the
  303. // stack, presumably because actions were undone before, then
  304. // all following actions must be discarded
  305. var diff = stack.length - stack.pointer - 1;
  306. while (diff--) {
  307. stack.pop();
  308. }
  309. }
  310. stack.pointer = stack.length;
  311. stack.add(res);
  312. if (stack.length > stack.maximumStackLength) {
  313. stack.shift();
  314. stack.pointer--;
  315. }
  316. }
  317. }
  318. }
  319. /**
  320. * Predefined UndoTypes object with default handlers for the most common events.
  321. * @type {Object}
  322. */
  323. var UndoTypes = {
  324. "add": {
  325. "undo": function (collection, ignore, model, options) {
  326. // Undo add = remove
  327. collection.remove(model, options);
  328. },
  329. "redo": function (collection, ignore, model, options) {
  330. // Redo add = add
  331. if (options.index) {
  332. options.at = options.index;
  333. }
  334. collection.add(model, options);
  335. },
  336. "on": function (model, collection, options) {
  337. return {
  338. object: collection,
  339. before: undefined,
  340. after: model,
  341. options: _.clone(options)
  342. };
  343. }
  344. },
  345. "remove": {
  346. "undo": function (collection, model, ignore, options) {
  347. if ("index" in options) {
  348. options.at = options.index;
  349. }
  350. collection.add(model, options);
  351. },
  352. "redo": function (collection, model, ignore, options) {
  353. collection.remove(model, options);
  354. },
  355. "on": function (model, collection, options) {
  356. return {
  357. object: collection,
  358. before: model,
  359. after: undefined,
  360. options: _.clone(options)
  361. };
  362. }
  363. },
  364. "change": {
  365. "undo": function (model, before, after, options) {
  366. if (_.isEmpty(before)) {
  367. _.each(_.keys(after), model.unset, model);
  368. } else {
  369. model.set(before);
  370. if (options && options.unsetData && options.unsetData.before && options.unsetData.before.length) {
  371. _.each(options.unsetData.before, model.unset, model);
  372. }
  373. }
  374. },
  375. "redo": function (model, before, after, options) {
  376. if (_.isEmpty(after)) {
  377. _.each(_.keys(before), model.unset, model);
  378. } else {
  379. model.set(after);
  380. if (options && options.unsetData && options.unsetData.after && options.unsetData.after.length) {
  381. _.each(options.unsetData.after, model.unset, model);
  382. }
  383. }
  384. },
  385. "on": function (model, options) {
  386. var
  387. afterAttributes = model.changedAttributes(),
  388. keysAfter = _.keys(afterAttributes),
  389. previousAttributes = _.pick(model.previousAttributes(), keysAfter),
  390. keysPrevious = _.keys(previousAttributes),
  391. unsetData = (options || (options = {})).unsetData = {
  392. after: [],
  393. before: []
  394. };
  395. if (keysAfter.length != keysPrevious.length) {
  396. // There are new attributes or old attributes have been unset
  397. if (keysAfter.length > keysPrevious.length) {
  398. // New attributes have been added
  399. _.each(keysAfter, function (val) {
  400. if (!(val in previousAttributes)) {
  401. unsetData.before.push(val);
  402. }
  403. }, this);
  404. } else {
  405. // Old attributes have been unset
  406. _.each(keysPrevious, function (val) {
  407. if (!(val in afterAttributes)) {
  408. unsetData.after.push(val);
  409. }
  410. })
  411. }
  412. }
  413. return {
  414. object: model,
  415. before: previousAttributes,
  416. after: afterAttributes,
  417. options: _.clone(options)
  418. };
  419. }
  420. },
  421. "reset": {
  422. "undo": function (collection, before, after) {
  423. collection.reset(before);
  424. },
  425. "redo": function (collection, before, after) {
  426. collection.reset(after);
  427. },
  428. "on": function (collection, options) {
  429. return {
  430. object: collection,
  431. before: options.previousModels,
  432. after: _.clone(collection.models)
  433. };
  434. }
  435. }
  436. };
  437. /**
  438. * Every UndoManager instance has an own undoTypes object
  439. * which is an instance of OwnedUndoTypes. OwnedUndoTypes'
  440. * prototype is the global UndoTypes object. Changes to the
  441. * global UndoTypes object take effect on every instance of
  442. * UndoManager as the object is its prototype. And yet every
  443. * local UndoTypes object can be changed individually.
  444. *
  445. * @constructor
  446. */
  447. function OwnedUndoTypes () {}
  448. OwnedUndoTypes.prototype = UndoTypes;
  449. /**
  450. * Adds, changes or removes an undo-type from an UndoTypes-object.
  451. * You can call it this way:
  452. * manipulateUndoType (1, "reset", {"on": function () {}}, undoTypes)
  453. * or this way to perform bulk actions:
  454. * manipulateUndoType (1, {"reset": {"on": function () {}}}, undoTypes)
  455. * In case of removing undo-types you can pass an Array for performing
  456. * bulk actions:
  457. * manipulateUndoType(2, ["reset", "change"], undoTypes)
  458. *
  459. * @param {Number} manipType Indicates the kind of action to execute: 0 for add, 1 for change, 2 for remove
  460. * @param {String|Object|Array} undoType The type of undoType that should be added/changed/removed. Can be an object / array to perform bulk actions
  461. * @param {Object} [fns] Object with the functions to add / change. Is optional in case you passed an object as undoType that contains these functions
  462. * @param {OwnedUndoTypes|UndoTypes} undoTypesInstance The undoTypes object to act on
  463. * @return {undefined}
  464. */
  465. function manipulateUndoType (manipType, undoType, fns, undoTypesInstance) {
  466. // manipType
  467. // 0: add
  468. // 1: change
  469. // 2: remove
  470. if (typeof undoType === "object") {
  471. // bulk action. Iterate over this data.
  472. return _.each(undoType, function (val, key) {
  473. if (manipType === 2) { // remove
  474. // undoType is an array
  475. manipulateUndoType (manipType, val, fns, undoTypesInstance);
  476. } else {
  477. // undoType is an object
  478. manipulateUndoType (manipType, key, val, fns);
  479. }
  480. })
  481. }
  482. switch (manipType) {
  483. case 0: // add
  484. if (hasKeys(fns, "undo", "redo", "on") && _.all(_.pick(fns, "undo", "redo", "on"), _.isFunction)) {
  485. undoTypesInstance[undoType] = fns;
  486. }
  487. break;
  488. case 1: // change
  489. if (undoTypesInstance[undoType] && _.isObject(fns)) {
  490. // undoTypeInstance[undoType] may be a prototype's property
  491. // So, if we did this _.extend(undoTypeInstance[undoType], fns)
  492. // we would extend the object on the prototype which means
  493. // that this change would have a global effect
  494. // Instead we just want to manipulate this instance. That's why
  495. // we're doing this:
  496. undoTypesInstance[undoType] = _.extend({}, undoTypesInstance[undoType], fns);
  497. }
  498. break;
  499. case 2: // remove
  500. delete undoTypesInstance[undoType];
  501. break;
  502. }
  503. }
  504. /**
  505. * Instantiating "Action" creates the UndoActions that
  506. * are collected in an UndoStack. It holds all relevant
  507. * data to undo / redo an action and has an undo / redo
  508. * method.
  509. */
  510. var Action = Backbone.Model.extend({
  511. defaults: {
  512. type: null, // "add", "change", "reset", etc.
  513. object: null, // The object on which the action occurred
  514. before: null, // The previous values which were changed with this action
  515. after: null, // The values after this action
  516. magicFusionIndex: null // The magicFusionIndex helps to combine
  517. // all actions that occurred "at the same time" to undo/redo them altogether
  518. },
  519. /**
  520. * Undoes this action.
  521. * @param {OwnedUndoTypes|UndoTypes} undoTypes The undoTypes object which contains the "undo"-handler that should be used
  522. * @return {undefined}
  523. */
  524. undo: function (undoTypes) {
  525. actionUndoRedo("undo", this.attributes);
  526. },
  527. /**
  528. * Redoes this action.
  529. * @param {OwnedUndoTypes|UndoTypes} undoTypes The undoTypes object which contains the "redo"-handler that should be used
  530. * @return {undefined}
  531. */
  532. redo: function (undoTypes) {
  533. actionUndoRedo("redo", this.attributes);
  534. }
  535. }),
  536. /**
  537. * An UndoStack is a collection of UndoActions in
  538. * chronological order.
  539. */
  540. UndoStack = Backbone.Collection.extend({
  541. model: Action,
  542. pointer: -1, // The pointer indicates the index where we are located within the stack. We start at -1
  543. track: false,
  544. isCurrentlyUndoRedoing: false,
  545. maximumStackLength: Infinity,
  546. setMaxLength: function (val) {
  547. this.maximumStackLength = val;
  548. }
  549. }),
  550. /**
  551. * An instance of UndoManager can keep track of
  552. * changes to objects and helps to undo them.
  553. */
  554. UndoManager = Backbone.Model.extend({
  555. defaults: {
  556. maximumStackLength: Infinity,
  557. track: false
  558. },
  559. /**
  560. * The constructor function.
  561. * @param {attr} [attr] Object with parameters. The available parameters are:
  562. * - maximumStackLength {number} Set the undo-stack's maximum size
  563. * - track {boolean} Start tracking changes right away
  564. * @return {undefined}
  565. */
  566. initialize: function (attr) {
  567. this.stack = new UndoStack;
  568. this.objectRegistry = new ObjectRegistry();
  569. this.undoTypes = new OwnedUndoTypes();
  570. // sync the maximumStackLength attribute with our stack
  571. this.stack.setMaxLength(this.get("maximumStackLength"));
  572. this.on("change:maximumStackLength", function (model, value) {
  573. this.stack.setMaxLength(value);
  574. }, this);
  575. // Start tracking, if attr.track == true
  576. if (attr && attr.track) {
  577. this.startTracking();
  578. }
  579. // Register objects passed in the "register" attribute
  580. if (attr && attr.register) {
  581. if (_.isArray(attr.register) || _.isArguments(attr.register)) {
  582. apply(this.register, this, attr.register);
  583. } else {
  584. this.register(attr.register);
  585. }
  586. }
  587. },
  588. /**
  589. * Starts tracking. Changes of registered objects won't be processed until you've called this function
  590. * @return {undefined}
  591. */
  592. startTracking: function () {
  593. this.set("track", true);
  594. this.stack.track = true;
  595. },
  596. /**
  597. * Stops tracking. Afterwards changes of registered objects won't be processed.
  598. * @return {undefined}
  599. */
  600. stopTracking: function () {
  601. this.set("track", false);
  602. this.stack.track = false;
  603. },
  604. /**
  605. * This is the "all"-handler which is bound to registered
  606. * objects. It creates an UndoAction from the event and adds
  607. * it to the stack.
  608. *
  609. * @param {String} type The event type
  610. * @return {undefined}
  611. */
  612. _addToStack: function (type) {
  613. addToStack(this.stack, type, slice(arguments, 1), this.undoTypes);
  614. },
  615. /**
  616. * Registers one or more objects to track their changes.
  617. * @param {...Object} obj The object or objects of which changes should be tracked
  618. * @return {undefined}
  619. */
  620. register: function () {
  621. onoff("on", arguments, this._addToStack, this);
  622. },
  623. /**
  624. * Unregisters one or more objects.
  625. * @param {...Object} obj The object or objects of which changes shouldn't be tracked any longer
  626. * @return {undefined}
  627. */
  628. unregister: function () {
  629. onoff("off", arguments, this._addToStack, this);
  630. },
  631. /**
  632. * Unregisters all previously registered objects.
  633. * @return {undefined}
  634. */
  635. unregisterAll: function () {
  636. apply(this.unregister, this, this.objectRegistry.get());
  637. },
  638. /**
  639. * Undoes the last action or the last set of actions in case 'magic' is true.
  640. * @param {Boolean} [magic] If true, all actions that happened basically at the same time are undone together
  641. * @return {undefined}
  642. */
  643. undo: function (magic) {
  644. managerUndoRedo("undo", this, this.stack, magic);
  645. },
  646. /**
  647. * Redoes a previously undone action or a set of actions.
  648. * @param {Boolean} [magic] If true, all actions that happened basically at the same time are redone together
  649. * @return {undefined}
  650. */
  651. redo: function (magic) {
  652. managerUndoRedo("redo", this, this.stack, magic);
  653. },
  654. /**
  655. * Checks if there's an action in the stack that can be undone / redone
  656. * @param {String} type Either "undo" or "redo"
  657. * @return {Boolean} True if there is a set of actions which can be undone / redone
  658. */
  659. isAvailable: function (type) {
  660. var s = this.stack, l = s.length;
  661. switch (type) {
  662. case "undo": return l > 0 && s.pointer > -1;
  663. case "redo": return l > 0 && s.pointer < l - 1;
  664. default: return false;
  665. }
  666. },
  667. /**
  668. * Sets the stack-reference to the stack of another undoManager.
  669. * @param {UndoManager} undoManager The undoManager whose stack-reference is set to this stack
  670. * @return {undefined}
  671. */
  672. merge: function (undoManager) {
  673. // This sets the stack-reference to the stack of another
  674. // undoManager so that the stack of this other undoManager
  675. // is used by two different managers.
  676. // This enables to set up a main-undoManager and besides it
  677. // several others for special, exceptional cases (by using
  678. // instance-based custom UndoTypes). Models / collections
  679. // which need this special treatment are only registered at
  680. // those special undoManagers. Those special ones are then
  681. // merged into the main-undoManager to write on its stack.
  682. // That way it's easier to manage exceptional cases.
  683. var args = _.isArray(undoManager) ? undoManager : slice(arguments), manager;
  684. while (manager = args.pop()) {
  685. if (manager instanceof UndoManager &&
  686. manager.stack instanceof UndoStack) {
  687. // set the stack reference to our stack
  688. manager.stack = this.stack;
  689. }
  690. }
  691. },
  692. /**
  693. * Add an UndoType to this specific UndoManager-instance.
  694. * @param {String} type The event this UndoType is made for
  695. * @param {Object} fns An object of functions that are called to generate the data for an UndoAction or to process it. Must have the properties "undo", "redo" and "on". Can have the property "condition".
  696. * @return {undefined}
  697. */
  698. addUndoType: function (type, fns) {
  699. manipulateUndoType(0, type, fns, this.undoTypes);
  700. },
  701. /**
  702. * Overwrite properties of an existing UndoType for this specific UndoManager-instance.
  703. * @param {String} type The event the UndoType is made for
  704. * @param {Object} fns An object of functions that are called to generate the data for an UndoAction or to process it. It extends the existing object.
  705. * @return {undefined}
  706. */
  707. changeUndoType: function (type, fns) {
  708. manipulateUndoType(1, type, fns, this.undoTypes);
  709. },
  710. /**
  711. * Remove one or more UndoTypes of this specific UndoManager-instance to fall back to the global UndoTypes.
  712. * @param {String|Array} type The event the UndoType that should be removed is made for. You can also pass an array of events.
  713. * @return {undefined}
  714. */
  715. removeUndoType: function (type) {
  716. manipulateUndoType(2, type, undefined, this.undoTypes);
  717. }
  718. });
  719. _.extend(UndoManager, {
  720. /**
  721. * Change the UndoManager's default attributes
  722. * @param {Object} defaultAttributes An object with the new default values.
  723. * @return {undefined}
  724. */
  725. defaults: function (defaultAttributes) {
  726. _.extend(UndoManager.prototype.defaults, defaultAttributes);
  727. },
  728. /**
  729. * Add an UndoType to the global UndoTypes-object.
  730. * @param {String} type The event this UndoType is made for
  731. * @param {Object} fns An object of functions that are called to generate the data for an UndoAction or to process it. Must have the properties "undo", "redo" and "on". Can have the property "condition".
  732. * @return {undefined}
  733. */
  734. "addUndoType": function (type, fns) {
  735. manipulateUndoType(0, type, fns, UndoTypes);
  736. },
  737. /**
  738. * Overwrite properties of an existing UndoType in the global UndoTypes-object.
  739. * @param {String} type The event the UndoType is made for
  740. * @param {Object} fns An object of functions that are called to generate the data for an UndoAction or to process it. It extends the existing object.
  741. * @return {undefined}
  742. */
  743. "changeUndoType": function (type, fns) {
  744. manipulateUndoType(1, type, fns, UndoTypes)
  745. },
  746. /**
  747. * Remove one or more UndoTypes of this specific UndoManager-instance to fall back to the global UndoTypes.
  748. * @param {String|Array} type The event the UndoType that should be removed is made for. You can also pass an array of events.
  749. * @return {undefined}
  750. */
  751. "removeUndoType": function (type) {
  752. manipulateUndoType(2, type, undefined, UndoTypes);
  753. }
  754. })
  755. Backbone.UndoManager = UndoManager;
  756. });