history.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import { Scope } from 'parchment';
  2. import Quill from '../core/quill';
  3. import Module from '../core/module';
  4. class History extends Module {
  5. constructor(quill, options) {
  6. super(quill, options);
  7. this.lastRecorded = 0;
  8. this.ignoreChange = false;
  9. this.clear();
  10. this.quill.on(
  11. Quill.events.EDITOR_CHANGE,
  12. (eventName, delta, oldDelta, source) => {
  13. if (eventName !== Quill.events.TEXT_CHANGE || this.ignoreChange) return;
  14. if (!this.options.userOnly || source === Quill.sources.USER) {
  15. this.record(delta, oldDelta);
  16. } else {
  17. this.transform(delta);
  18. }
  19. },
  20. );
  21. this.quill.keyboard.addBinding(
  22. { key: 'z', shortKey: true },
  23. this.undo.bind(this),
  24. );
  25. this.quill.keyboard.addBinding(
  26. { key: 'z', shortKey: true, shiftKey: true },
  27. this.redo.bind(this),
  28. );
  29. if (/Win/i.test(navigator.platform)) {
  30. this.quill.keyboard.addBinding(
  31. { key: 'y', shortKey: true },
  32. this.redo.bind(this),
  33. );
  34. }
  35. this.quill.root.addEventListener('beforeinput', event => {
  36. if (event.inputType === 'historyUndo') {
  37. this.undo();
  38. event.preventDefault();
  39. } else if (event.inputType === 'historyRedo') {
  40. this.redo();
  41. event.preventDefault();
  42. }
  43. });
  44. }
  45. change(source, dest) {
  46. if (this.stack[source].length === 0) return;
  47. const delta = this.stack[source].pop();
  48. const base = this.quill.getContents();
  49. const inverseDelta = delta.invert(base);
  50. this.stack[dest].push(inverseDelta);
  51. this.lastRecorded = 0;
  52. this.ignoreChange = true;
  53. this.quill.updateContents(delta, Quill.sources.USER);
  54. this.ignoreChange = false;
  55. const index = getLastChangeIndex(this.quill.scroll, delta);
  56. this.quill.setSelection(index);
  57. }
  58. clear() {
  59. this.stack = { undo: [], redo: [] };
  60. }
  61. cutoff() {
  62. this.lastRecorded = 0;
  63. }
  64. record(changeDelta, oldDelta) {
  65. if (changeDelta.ops.length === 0) return;
  66. this.stack.redo = [];
  67. let undoDelta = changeDelta.invert(oldDelta);
  68. const timestamp = Date.now();
  69. if (
  70. this.lastRecorded + this.options.delay > timestamp &&
  71. this.stack.undo.length > 0
  72. ) {
  73. const delta = this.stack.undo.pop();
  74. undoDelta = undoDelta.compose(delta);
  75. } else {
  76. this.lastRecorded = timestamp;
  77. }
  78. if (undoDelta.length() === 0) return;
  79. this.stack.undo.push(undoDelta);
  80. if (this.stack.undo.length > this.options.maxStack) {
  81. this.stack.undo.shift();
  82. }
  83. }
  84. redo() {
  85. this.change('redo', 'undo');
  86. }
  87. transform(delta) {
  88. transformStack(this.stack.undo, delta);
  89. transformStack(this.stack.redo, delta);
  90. }
  91. undo() {
  92. this.change('undo', 'redo');
  93. }
  94. }
  95. History.DEFAULTS = {
  96. delay: 1000,
  97. maxStack: 100,
  98. userOnly: false,
  99. };
  100. function transformStack(stack, delta) {
  101. let remoteDelta = delta;
  102. for (let i = stack.length - 1; i >= 0; i -= 1) {
  103. const oldDelta = stack[i];
  104. stack[i] = remoteDelta.transform(oldDelta, true);
  105. remoteDelta = oldDelta.transform(remoteDelta);
  106. if (stack[i].length() === 0) {
  107. stack.splice(i, 1);
  108. }
  109. }
  110. }
  111. function endsWithNewlineChange(scroll, delta) {
  112. const lastOp = delta.ops[delta.ops.length - 1];
  113. if (lastOp == null) return false;
  114. if (lastOp.insert != null) {
  115. return typeof lastOp.insert === 'string' && lastOp.insert.endsWith('\n');
  116. }
  117. if (lastOp.attributes != null) {
  118. return Object.keys(lastOp.attributes).some(attr => {
  119. return scroll.query(attr, Scope.BLOCK) != null;
  120. });
  121. }
  122. return false;
  123. }
  124. function getLastChangeIndex(scroll, delta) {
  125. const deleteLength = delta.reduce((length, op) => {
  126. return length + (op.delete || 0);
  127. }, 0);
  128. let changeIndex = delta.length() - deleteLength;
  129. if (endsWithNewlineChange(scroll, delta)) {
  130. changeIndex -= 1;
  131. }
  132. return changeIndex;
  133. }
  134. export { History as default, getLastChangeIndex };