history.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. /*globals svgedit*/
  2. /*jslint vars: true, eqeq: true, continue: true, forin: true*/
  3. /**
  4. * Package: svedit.history
  5. *
  6. * Licensed under the MIT License
  7. *
  8. * Copyright(c) 2010 Jeff Schiller
  9. */
  10. // Dependencies:
  11. // 1) jQuery
  12. // 2) svgtransformlist.js
  13. // 3) svgutils.js
  14. (function() {'use strict';
  15. if (!svgedit.history) {
  16. svgedit.history = {};
  17. }
  18. // Group: Undo/Redo history management
  19. svgedit.history.HistoryEventTypes = {
  20. BEFORE_APPLY: 'before_apply',
  21. AFTER_APPLY: 'after_apply',
  22. BEFORE_UNAPPLY: 'before_unapply',
  23. AFTER_UNAPPLY: 'after_unapply'
  24. };
  25. var removedElements = {};
  26. /**
  27. * An interface that all command objects must implement.
  28. * @typedef svgedit.history.HistoryCommand
  29. * @type {object}
  30. * void apply(svgedit.history.HistoryEventHandler);
  31. * void unapply(svgedit.history.HistoryEventHandler);
  32. * Element[] elements();
  33. * String getText();
  34. *
  35. * static String type();
  36. * }
  37. *
  38. * Interface: svgedit.history.HistoryEventHandler
  39. * An interface for objects that will handle history events.
  40. *
  41. * interface svgedit.history.HistoryEventHandler {
  42. * void handleHistoryEvent(eventType, command);
  43. * }
  44. *
  45. * eventType is a string conforming to one of the HistoryEvent types.
  46. * command is an object fulfilling the HistoryCommand interface.
  47. */
  48. /**
  49. * @class svgedit.history.MoveElementCommand
  50. * @implements svgedit.history.HistoryCommand
  51. * History command for an element that had its DOM position changed
  52. * @param {Element} elem - The DOM element that was moved
  53. * @param {Element} oldNextSibling - The element's next sibling before it was moved
  54. * @param {Element} oldParent - The element's parent before it was moved
  55. * @param {string} [text] - An optional string visible to user related to this change
  56. */
  57. svgedit.history.MoveElementCommand = function(elem, oldNextSibling, oldParent, text) {
  58. this.elem = elem;
  59. this.text = text ? ("Move " + elem.tagName + " to " + text) : ("Move " + elem.tagName);
  60. this.oldNextSibling = oldNextSibling;
  61. this.oldParent = oldParent;
  62. this.newNextSibling = elem.nextSibling;
  63. this.newParent = elem.parentNode;
  64. };
  65. svgedit.history.MoveElementCommand.type = function() { return 'svgedit.history.MoveElementCommand'; };
  66. svgedit.history.MoveElementCommand.prototype.type = svgedit.history.MoveElementCommand.type;
  67. svgedit.history.MoveElementCommand.prototype.getText = function() {
  68. return this.text;
  69. };
  70. /**
  71. * Re-positions the element
  72. * @param {handleHistoryEvent: function}
  73. */
  74. svgedit.history.MoveElementCommand.prototype.apply = function(handler) {
  75. // TODO(codedread): Refactor this common event code into a base HistoryCommand class.
  76. if (handler) {
  77. handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_APPLY, this);
  78. }
  79. this.elem = this.newParent.insertBefore(this.elem, this.newNextSibling);
  80. if (handler) {
  81. handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_APPLY, this);
  82. }
  83. };
  84. /**
  85. * Positions the element back to its original location
  86. * @param {handleHistoryEvent: function}
  87. */
  88. svgedit.history.MoveElementCommand.prototype.unapply = function(handler) {
  89. if (handler) {
  90. handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_UNAPPLY, this);
  91. }
  92. this.elem = this.oldParent.insertBefore(this.elem, this.oldNextSibling);
  93. if (handler) {
  94. handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_UNAPPLY, this);
  95. }
  96. };
  97. // Function: svgedit.history.MoveElementCommand.elements
  98. // Returns array with element associated with this command
  99. svgedit.history.MoveElementCommand.prototype.elements = function() {
  100. return [this.elem];
  101. };
  102. // Class: svgedit.history.InsertElementCommand
  103. // implements svgedit.history.HistoryCommand
  104. // History command for an element that was added to the DOM
  105. //
  106. // Parameters:
  107. // elem - The newly added DOM element
  108. // text - An optional string visible to user related to this change
  109. svgedit.history.InsertElementCommand = function(elem, text) {
  110. this.elem = elem;
  111. this.text = text || ("Create " + elem.tagName);
  112. this.parent = elem.parentNode;
  113. this.nextSibling = this.elem.nextSibling;
  114. };
  115. svgedit.history.InsertElementCommand.type = function() { return 'svgedit.history.InsertElementCommand'; };
  116. svgedit.history.InsertElementCommand.prototype.type = svgedit.history.InsertElementCommand.type;
  117. // Function: svgedit.history.InsertElementCommand.getText
  118. svgedit.history.InsertElementCommand.prototype.getText = function() {
  119. return this.text;
  120. };
  121. // Function: svgedit.history.InsertElementCommand.apply
  122. // Re-Inserts the new element
  123. svgedit.history.InsertElementCommand.prototype.apply = function(handler) {
  124. if (handler) {
  125. handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_APPLY, this);
  126. }
  127. this.elem = this.parent.insertBefore(this.elem, this.nextSibling);
  128. if (handler) {
  129. handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_APPLY, this);
  130. }
  131. };
  132. // Function: svgedit.history.InsertElementCommand.unapply
  133. // Removes the element
  134. svgedit.history.InsertElementCommand.prototype.unapply = function(handler) {
  135. if (handler) {
  136. handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_UNAPPLY, this);
  137. }
  138. this.parent = this.elem.parentNode;
  139. this.elem = this.elem.parentNode.removeChild(this.elem);
  140. if (handler) {
  141. handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_UNAPPLY, this);
  142. }
  143. };
  144. // Function: svgedit.history.InsertElementCommand.elements
  145. // Returns array with element associated with this command
  146. svgedit.history.InsertElementCommand.prototype.elements = function() {
  147. return [this.elem];
  148. };
  149. // Class: svgedit.history.RemoveElementCommand
  150. // implements svgedit.history.HistoryCommand
  151. // History command for an element removed from the DOM
  152. //
  153. // Parameters:
  154. // elem - The removed DOM element
  155. // oldNextSibling - the DOM element's nextSibling when it was in the DOM
  156. // oldParent - The DOM element's parent
  157. // text - An optional string visible to user related to this change
  158. svgedit.history.RemoveElementCommand = function(elem, oldNextSibling, oldParent, text) {
  159. this.elem = elem;
  160. this.text = text || ("Delete " + elem.tagName);
  161. this.nextSibling = oldNextSibling;
  162. this.parent = oldParent;
  163. // special hack for webkit: remove this element's entry in the svgTransformLists map
  164. svgedit.transformlist.removeElementFromListMap(elem);
  165. };
  166. svgedit.history.RemoveElementCommand.type = function() { return 'svgedit.history.RemoveElementCommand'; };
  167. svgedit.history.RemoveElementCommand.prototype.type = svgedit.history.RemoveElementCommand.type;
  168. // Function: svgedit.history.RemoveElementCommand.getText
  169. svgedit.history.RemoveElementCommand.prototype.getText = function() {
  170. return this.text;
  171. };
  172. // Function: RemoveElementCommand.apply
  173. // Re-removes the new element
  174. svgedit.history.RemoveElementCommand.prototype.apply = function(handler) {
  175. if (handler) {
  176. handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_APPLY, this);
  177. }
  178. svgedit.transformlist.removeElementFromListMap(this.elem);
  179. this.parent = this.elem.parentNode;
  180. this.elem = this.parent.removeChild(this.elem);
  181. if (handler) {
  182. handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_APPLY, this);
  183. }
  184. };
  185. // Function: RemoveElementCommand.unapply
  186. // Re-adds the new element
  187. svgedit.history.RemoveElementCommand.prototype.unapply = function(handler) {
  188. if (handler) {
  189. handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_UNAPPLY, this);
  190. }
  191. svgedit.transformlist.removeElementFromListMap(this.elem);
  192. if (this.nextSibling == null) {
  193. if (window.console) {
  194. console.log('Error: reference element was lost');
  195. }
  196. }
  197. this.parent.insertBefore(this.elem, this.nextSibling);
  198. if (handler) {
  199. handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_UNAPPLY, this);
  200. }
  201. };
  202. // Function: RemoveElementCommand.elements
  203. // Returns array with element associated with this command
  204. svgedit.history.RemoveElementCommand.prototype.elements = function() {
  205. return [this.elem];
  206. };
  207. // Class: svgedit.history.ChangeElementCommand
  208. // implements svgedit.history.HistoryCommand
  209. // History command to make a change to an element.
  210. // Usually an attribute change, but can also be textcontent.
  211. //
  212. // Parameters:
  213. // elem - The DOM element that was changed
  214. // attrs - An object with the attributes to be changed and the values they had *before* the change
  215. // text - An optional string visible to user related to this change
  216. svgedit.history.ChangeElementCommand = function(elem, attrs, text) {
  217. this.elem = elem;
  218. this.text = text ? ("Change " + elem.tagName + " " + text) : ("Change " + elem.tagName);
  219. this.newValues = {};
  220. this.oldValues = attrs;
  221. var attr;
  222. for (attr in attrs) {
  223. if (attr == "#text") {this.newValues[attr] = elem.textContent;}
  224. else if (attr == "#href") {this.newValues[attr] = svgedit.utilities.getHref(elem);}
  225. else {this.newValues[attr] = elem.getAttribute(attr);}
  226. }
  227. };
  228. svgedit.history.ChangeElementCommand.type = function() { return 'svgedit.history.ChangeElementCommand'; };
  229. svgedit.history.ChangeElementCommand.prototype.type = svgedit.history.ChangeElementCommand.type;
  230. // Function: svgedit.history.ChangeElementCommand.getText
  231. svgedit.history.ChangeElementCommand.prototype.getText = function() {
  232. return this.text;
  233. };
  234. // Function: svgedit.history.ChangeElementCommand.apply
  235. // Performs the stored change action
  236. svgedit.history.ChangeElementCommand.prototype.apply = function(handler) {
  237. if (handler) {
  238. handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_APPLY, this);
  239. }
  240. var bChangedTransform = false;
  241. var attr;
  242. for (attr in this.newValues ) {
  243. if (this.newValues[attr]) {
  244. if (attr == "#text") {this.elem.textContent = this.newValues[attr];}
  245. else if (attr == "#href") {svgedit.utilities.setHref(this.elem, this.newValues[attr]);}
  246. else {this.elem.setAttribute(attr, this.newValues[attr]);}
  247. }
  248. else {
  249. if (attr == "#text") {
  250. this.elem.textContent = "";
  251. }
  252. else {
  253. this.elem.setAttribute(attr, "");
  254. this.elem.removeAttribute(attr);
  255. }
  256. }
  257. if (attr == "transform") { bChangedTransform = true; }
  258. }
  259. // relocate rotational transform, if necessary
  260. if (!bChangedTransform) {
  261. var angle = svgedit.utilities.getRotationAngle(this.elem);
  262. if (angle) {
  263. // TODO: These instances of elem either need to be declared as global
  264. // (which would not be good for conflicts) or declare/use this.elem
  265. var bbox = elem.getBBox();
  266. var cx = bbox.x + bbox.width/2,
  267. cy = bbox.y + bbox.height/2;
  268. var rotate = ["rotate(", angle, " ", cx, ",", cy, ")"].join('');
  269. if (rotate != elem.getAttribute("transform")) {
  270. elem.setAttribute("transform", rotate);
  271. }
  272. }
  273. }
  274. if (handler) {
  275. handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_APPLY, this);
  276. }
  277. return true;
  278. };
  279. // Function: svgedit.history.ChangeElementCommand.unapply
  280. // Reverses the stored change action
  281. svgedit.history.ChangeElementCommand.prototype.unapply = function(handler) {
  282. if (handler) {
  283. handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_UNAPPLY, this);
  284. }
  285. var bChangedTransform = false;
  286. var attr;
  287. for (attr in this.oldValues ) {
  288. if (this.oldValues[attr]) {
  289. if (attr == "#text") {this.elem.textContent = this.oldValues[attr];}
  290. else if (attr == "#href") {svgedit.utilities.setHref(this.elem, this.oldValues[attr]);}
  291. else {
  292. this.elem.setAttribute(attr, this.oldValues[attr]);
  293. }
  294. }
  295. else {
  296. if (attr == "#text") {
  297. this.elem.textContent = "";
  298. }
  299. else {this.elem.removeAttribute(attr);}
  300. }
  301. if (attr == "transform") { bChangedTransform = true; }
  302. }
  303. // relocate rotational transform, if necessary
  304. if (!bChangedTransform) {
  305. var angle = svgedit.utilities.getRotationAngle(this.elem);
  306. if (angle) {
  307. var bbox = elem.getBBox();
  308. var cx = bbox.x + bbox.width/2,
  309. cy = bbox.y + bbox.height/2;
  310. var rotate = ["rotate(", angle, " ", cx, ",", cy, ")"].join('');
  311. if (rotate != elem.getAttribute("transform")) {
  312. elem.setAttribute("transform", rotate);
  313. }
  314. }
  315. }
  316. // Remove transformlist to prevent confusion that causes bugs like 575.
  317. svgedit.transformlist.removeElementFromListMap(this.elem);
  318. if (handler) {
  319. handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_UNAPPLY, this);
  320. }
  321. return true;
  322. };
  323. // Function: ChangeElementCommand.elements
  324. // Returns array with element associated with this command
  325. svgedit.history.ChangeElementCommand.prototype.elements = function() {
  326. return [this.elem];
  327. };
  328. // TODO: create a 'typing' command object that tracks changes in text
  329. // if a new Typing command is created and the top command on the stack is also a Typing
  330. // and they both affect the same element, then collapse the two commands into one
  331. // Class: svgedit.history.BatchCommand
  332. // implements svgedit.history.HistoryCommand
  333. // History command that can contain/execute multiple other commands
  334. //
  335. // Parameters:
  336. // text - An optional string visible to user related to this change
  337. svgedit.history.BatchCommand = function(text) {
  338. this.text = text || "Batch Command";
  339. this.stack = [];
  340. };
  341. svgedit.history.BatchCommand.type = function() { return 'svgedit.history.BatchCommand'; };
  342. svgedit.history.BatchCommand.prototype.type = svgedit.history.BatchCommand.type;
  343. // Function: svgedit.history.BatchCommand.getText
  344. svgedit.history.BatchCommand.prototype.getText = function() {
  345. return this.text;
  346. };
  347. // Function: svgedit.history.BatchCommand.apply
  348. // Runs "apply" on all subcommands
  349. svgedit.history.BatchCommand.prototype.apply = function(handler) {
  350. if (handler) {
  351. handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_APPLY, this);
  352. }
  353. var i,
  354. len = this.stack.length;
  355. for (i = 0; i < len; ++i) {
  356. this.stack[i].apply(handler);
  357. }
  358. if (handler) {
  359. handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_APPLY, this);
  360. }
  361. };
  362. // Function: svgedit.history.BatchCommand.unapply
  363. // Runs "unapply" on all subcommands
  364. svgedit.history.BatchCommand.prototype.unapply = function(handler) {
  365. if (handler) {
  366. handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.BEFORE_UNAPPLY, this);
  367. }
  368. var i;
  369. for (i = this.stack.length-1; i >= 0; i--) {
  370. this.stack[i].unapply(handler);
  371. }
  372. if (handler) {
  373. handler.handleHistoryEvent(svgedit.history.HistoryEventTypes.AFTER_UNAPPLY, this);
  374. }
  375. };
  376. // Function: svgedit.history.BatchCommand.elements
  377. // Iterate through all our subcommands and returns all the elements we are changing
  378. svgedit.history.BatchCommand.prototype.elements = function() {
  379. var elems = [];
  380. var cmd = this.stack.length;
  381. while (cmd--) {
  382. var thisElems = this.stack[cmd].elements();
  383. var elem = thisElems.length;
  384. while (elem--) {
  385. if (elems.indexOf(thisElems[elem]) == -1) {elems.push(thisElems[elem]);}
  386. }
  387. }
  388. return elems;
  389. };
  390. // Function: svgedit.history.BatchCommand.addSubCommand
  391. // Adds a given command to the history stack
  392. //
  393. // Parameters:
  394. // cmd - The undo command object to add
  395. svgedit.history.BatchCommand.prototype.addSubCommand = function(cmd) {
  396. this.stack.push(cmd);
  397. };
  398. // Function: svgedit.history.BatchCommand.isEmpty
  399. // Returns a boolean indicating whether or not the batch command is empty
  400. svgedit.history.BatchCommand.prototype.isEmpty = function() {
  401. return this.stack.length === 0;
  402. };
  403. // Class: svgedit.history.UndoManager
  404. // Parameters:
  405. // historyEventHandler - an object that conforms to the HistoryEventHandler interface
  406. // (see above)
  407. svgedit.history.UndoManager = function(historyEventHandler) {
  408. this.handler_ = historyEventHandler || null;
  409. this.undoStackPointer = 0;
  410. this.undoStack = [];
  411. // this is the stack that stores the original values, the elements and
  412. // the attribute name for begin/finish
  413. this.undoChangeStackPointer = -1;
  414. this.undoableChangeStack = [];
  415. };
  416. // Function: svgedit.history.UndoManager.resetUndoStack
  417. // Resets the undo stack, effectively clearing the undo/redo history
  418. svgedit.history.UndoManager.prototype.resetUndoStack = function() {
  419. this.undoStack = [];
  420. this.undoStackPointer = 0;
  421. };
  422. // Function: svgedit.history.UndoManager.getUndoStackSize
  423. // Returns:
  424. // Integer with the current size of the undo history stack
  425. svgedit.history.UndoManager.prototype.getUndoStackSize = function() {
  426. return this.undoStackPointer;
  427. };
  428. // Function: svgedit.history.UndoManager.getRedoStackSize
  429. // Returns:
  430. // Integer with the current size of the redo history stack
  431. svgedit.history.UndoManager.prototype.getRedoStackSize = function() {
  432. return this.undoStack.length - this.undoStackPointer;
  433. };
  434. // Function: svgedit.history.UndoManager.getNextUndoCommandText
  435. // Returns:
  436. // String associated with the next undo command
  437. svgedit.history.UndoManager.prototype.getNextUndoCommandText = function() {
  438. return this.undoStackPointer > 0 ? this.undoStack[this.undoStackPointer-1].getText() : "";
  439. };
  440. // Function: svgedit.history.UndoManager.getNextRedoCommandText
  441. // Returns:
  442. // String associated with the next redo command
  443. svgedit.history.UndoManager.prototype.getNextRedoCommandText = function() {
  444. return this.undoStackPointer < this.undoStack.length ? this.undoStack[this.undoStackPointer].getText() : "";
  445. };
  446. // Function: svgedit.history.UndoManager.undo
  447. // Performs an undo step
  448. svgedit.history.UndoManager.prototype.undo = function() {
  449. if (this.undoStackPointer > 0) {
  450. var cmd = this.undoStack[--this.undoStackPointer];
  451. cmd.unapply(this.handler_);
  452. }
  453. };
  454. // Function: svgedit.history.UndoManager.redo
  455. // Performs a redo step
  456. svgedit.history.UndoManager.prototype.redo = function() {
  457. if (this.undoStackPointer < this.undoStack.length && this.undoStack.length > 0) {
  458. var cmd = this.undoStack[this.undoStackPointer++];
  459. cmd.apply(this.handler_);
  460. }
  461. };
  462. // Function: svgedit.history.UndoManager.addCommandToHistory
  463. // Adds a command object to the undo history stack
  464. //
  465. // Parameters:
  466. // cmd - The command object to add
  467. svgedit.history.UndoManager.prototype.addCommandToHistory = function(cmd) {
  468. // FIXME: we MUST compress consecutive text changes to the same element
  469. // (right now each keystroke is saved as a separate command that includes the
  470. // entire text contents of the text element)
  471. // TODO: consider limiting the history that we store here (need to do some slicing)
  472. // if our stack pointer is not at the end, then we have to remove
  473. // all commands after the pointer and insert the new command
  474. if (this.undoStackPointer < this.undoStack.length && this.undoStack.length > 0) {
  475. this.undoStack = this.undoStack.splice(0, this.undoStackPointer);
  476. }
  477. this.undoStack.push(cmd);
  478. this.undoStackPointer = this.undoStack.length;
  479. };
  480. // Function: svgedit.history.UndoManager.beginUndoableChange
  481. // This function tells the canvas to remember the old values of the
  482. // attrName attribute for each element sent in. The elements and values
  483. // are stored on a stack, so the next call to finishUndoableChange() will
  484. // pop the elements and old values off the stack, gets the current values
  485. // from the DOM and uses all of these to construct the undo-able command.
  486. //
  487. // Parameters:
  488. // attrName - The name of the attribute being changed
  489. // elems - Array of DOM elements being changed
  490. svgedit.history.UndoManager.prototype.beginUndoableChange = function(attrName, elems) {
  491. var p = ++this.undoChangeStackPointer;
  492. var i = elems.length;
  493. var oldValues = new Array(i), elements = new Array(i);
  494. while (i--) {
  495. var elem = elems[i];
  496. if (elem == null) {continue;}
  497. elements[i] = elem;
  498. oldValues[i] = elem.getAttribute(attrName);
  499. }
  500. this.undoableChangeStack[p] = {
  501. 'attrName': attrName,
  502. 'oldValues': oldValues,
  503. 'elements': elements
  504. };
  505. };
  506. // Function: svgedit.history.UndoManager.finishUndoableChange
  507. // This function returns a BatchCommand object which summarizes the
  508. // change since beginUndoableChange was called. The command can then
  509. // be added to the command history
  510. //
  511. // Returns:
  512. // Batch command object with resulting changes
  513. svgedit.history.UndoManager.prototype.finishUndoableChange = function() {
  514. var p = this.undoChangeStackPointer--;
  515. var changeset = this.undoableChangeStack[p];
  516. var i = changeset.elements.length;
  517. var attrName = changeset.attrName;
  518. var batchCmd = new svgedit.history.BatchCommand("Change " + attrName);
  519. while (i--) {
  520. var elem = changeset.elements[i];
  521. if (elem == null) {continue;}
  522. var changes = {};
  523. changes[attrName] = changeset.oldValues[i];
  524. if (changes[attrName] != elem.getAttribute(attrName)) {
  525. batchCmd.addSubCommand(new svgedit.history.ChangeElementCommand(elem, changes, attrName));
  526. }
  527. }
  528. this.undoableChangeStack[p] = null;
  529. return batchCmd;
  530. };
  531. }());