fc5470d75b860e14f405a54ddabb9c58beed711b201a5af9bcb8014c9b116fb39554f5b8699a8a79914235eb33c0256b243044c2eea9399f751b86d63cf065 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import { Scope } from 'parchment';
  2. import Module from '../core/module.js';
  3. import Quill from '../core/quill.js';
  4. class History extends Module {
  5. static DEFAULTS = {
  6. delay: 1000,
  7. maxStack: 100,
  8. userOnly: false
  9. };
  10. lastRecorded = 0;
  11. ignoreChange = false;
  12. stack = {
  13. undo: [],
  14. redo: []
  15. };
  16. currentRange = null;
  17. constructor(quill, options) {
  18. super(quill, options);
  19. this.quill.on(Quill.events.EDITOR_CHANGE, (eventName, value, oldValue, source) => {
  20. if (eventName === Quill.events.SELECTION_CHANGE) {
  21. if (value && source !== Quill.sources.SILENT) {
  22. this.currentRange = value;
  23. }
  24. } else if (eventName === Quill.events.TEXT_CHANGE) {
  25. if (!this.ignoreChange) {
  26. if (!this.options.userOnly || source === Quill.sources.USER) {
  27. this.record(value, oldValue);
  28. } else {
  29. this.transform(value);
  30. }
  31. }
  32. this.currentRange = transformRange(this.currentRange, value);
  33. }
  34. });
  35. this.quill.keyboard.addBinding({
  36. key: 'z',
  37. shortKey: true
  38. }, this.undo.bind(this));
  39. this.quill.keyboard.addBinding({
  40. key: ['z', 'Z'],
  41. shortKey: true,
  42. shiftKey: true
  43. }, this.redo.bind(this));
  44. if (/Win/i.test(navigator.platform)) {
  45. this.quill.keyboard.addBinding({
  46. key: 'y',
  47. shortKey: true
  48. }, this.redo.bind(this));
  49. }
  50. this.quill.root.addEventListener('beforeinput', event => {
  51. if (event.inputType === 'historyUndo') {
  52. this.undo();
  53. event.preventDefault();
  54. } else if (event.inputType === 'historyRedo') {
  55. this.redo();
  56. event.preventDefault();
  57. }
  58. });
  59. }
  60. change(source, dest) {
  61. if (this.stack[source].length === 0) return;
  62. const item = this.stack[source].pop();
  63. if (!item) return;
  64. const base = this.quill.getContents();
  65. const inverseDelta = item.delta.invert(base);
  66. this.stack[dest].push({
  67. delta: inverseDelta,
  68. range: transformRange(item.range, inverseDelta)
  69. });
  70. this.lastRecorded = 0;
  71. this.ignoreChange = true;
  72. this.quill.updateContents(item.delta, Quill.sources.USER);
  73. this.ignoreChange = false;
  74. this.restoreSelection(item);
  75. }
  76. clear() {
  77. this.stack = {
  78. undo: [],
  79. redo: []
  80. };
  81. }
  82. cutoff() {
  83. this.lastRecorded = 0;
  84. }
  85. record(changeDelta, oldDelta) {
  86. if (changeDelta.ops.length === 0) return;
  87. this.stack.redo = [];
  88. let undoDelta = changeDelta.invert(oldDelta);
  89. let undoRange = this.currentRange;
  90. const timestamp = Date.now();
  91. if (
  92. // @ts-expect-error Fix me later
  93. this.lastRecorded + this.options.delay > timestamp && this.stack.undo.length > 0) {
  94. const item = this.stack.undo.pop();
  95. if (item) {
  96. undoDelta = undoDelta.compose(item.delta);
  97. undoRange = item.range;
  98. }
  99. } else {
  100. this.lastRecorded = timestamp;
  101. }
  102. if (undoDelta.length() === 0) return;
  103. this.stack.undo.push({
  104. delta: undoDelta,
  105. range: undoRange
  106. });
  107. // @ts-expect-error Fix me later
  108. if (this.stack.undo.length > this.options.maxStack) {
  109. this.stack.undo.shift();
  110. }
  111. }
  112. redo() {
  113. this.change('redo', 'undo');
  114. }
  115. transform(delta) {
  116. transformStack(this.stack.undo, delta);
  117. transformStack(this.stack.redo, delta);
  118. }
  119. undo() {
  120. this.change('undo', 'redo');
  121. }
  122. restoreSelection(stackItem) {
  123. if (stackItem.range) {
  124. this.quill.setSelection(stackItem.range, Quill.sources.USER);
  125. } else {
  126. const index = getLastChangeIndex(this.quill.scroll, stackItem.delta);
  127. this.quill.setSelection(index, Quill.sources.USER);
  128. }
  129. }
  130. }
  131. function transformStack(stack, delta) {
  132. let remoteDelta = delta;
  133. for (let i = stack.length - 1; i >= 0; i -= 1) {
  134. const oldItem = stack[i];
  135. stack[i] = {
  136. delta: remoteDelta.transform(oldItem.delta, true),
  137. range: oldItem.range && transformRange(oldItem.range, remoteDelta)
  138. };
  139. remoteDelta = oldItem.delta.transform(remoteDelta);
  140. if (stack[i].delta.length() === 0) {
  141. stack.splice(i, 1);
  142. }
  143. }
  144. }
  145. function endsWithNewlineChange(scroll, delta) {
  146. const lastOp = delta.ops[delta.ops.length - 1];
  147. if (lastOp == null) return false;
  148. if (lastOp.insert != null) {
  149. return typeof lastOp.insert === 'string' && lastOp.insert.endsWith('\n');
  150. }
  151. if (lastOp.attributes != null) {
  152. return Object.keys(lastOp.attributes).some(attr => {
  153. return scroll.query(attr, Scope.BLOCK) != null;
  154. });
  155. }
  156. return false;
  157. }
  158. function getLastChangeIndex(scroll, delta) {
  159. const deleteLength = delta.reduce((length, op) => {
  160. return length + (op.delete || 0);
  161. }, 0);
  162. let changeIndex = delta.length() - deleteLength;
  163. if (endsWithNewlineChange(scroll, delta)) {
  164. changeIndex -= 1;
  165. }
  166. return changeIndex;
  167. }
  168. function transformRange(range, delta) {
  169. if (!range) return range;
  170. const start = delta.transformPosition(range.index);
  171. const end = delta.transformPosition(range.index + range.length);
  172. return {
  173. index: start,
  174. length: end - start
  175. };
  176. }
  177. export { History as default, getLastChangeIndex };
  178. //# sourceMappingURL=history.js.map