editor.mock.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. /**
  2. * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
  3. * SPDX-License-Identifier: MIT
  4. */
  5. import { decorate, inject, injectable, postConstruct } from 'inversify';
  6. import {
  7. HistoryService,
  8. Operation,
  9. OperationContribution,
  10. OperationMeta,
  11. OperationRegistry,
  12. StackOperation,
  13. } from '../src';
  14. interface Node {
  15. id: number;
  16. data: any;
  17. children: Node[];
  18. }
  19. interface NodeOperationValue {
  20. parentId: number;
  21. index: number;
  22. node: Node;
  23. }
  24. interface TextOperationValue {
  25. index: number;
  26. text: string;
  27. }
  28. enum OperationType {
  29. insertNode = 'insert-node',
  30. deleteNode = 'delete-node',
  31. insertText = 'insert-text',
  32. deleteText = 'delete-text',
  33. selection = 'selection',
  34. mergeByTime = 'mergeByTime',
  35. }
  36. export function defaultRoot() {
  37. return {
  38. id: 1,
  39. data: 'test',
  40. children: [],
  41. }
  42. }
  43. export const MOCK_URI = 'file:///mock URI'
  44. @injectable()
  45. export class Editor {
  46. @inject(HistoryService)
  47. private historyService: HistoryService;
  48. @postConstruct()
  49. init() {
  50. this.historyService.context.source = this;
  51. }
  52. public node: Node = defaultRoot();
  53. public text: string = '';
  54. reset() {
  55. this.node = defaultRoot()
  56. }
  57. async undo() {
  58. await this.historyService.undo()
  59. }
  60. async redo() {
  61. await this.historyService.redo()
  62. }
  63. canRedo() {
  64. return this.historyService.canRedo()
  65. }
  66. canUndo() {
  67. return this.historyService.canUndo()
  68. }
  69. getHistoryOperations() {
  70. return this.historyService.getHistoryOperations()
  71. }
  72. handleSelection() {
  73. this.historyService.pushOperation({ type: OperationType.selection, value: {} })
  74. }
  75. handleInsert(value: NodeOperationValue) {
  76. this.historyService.pushOperation({ type: OperationType.insertNode, value })
  77. }
  78. handleInsertText(value: TextOperationValue, uri?: string, noApply?: boolean) {
  79. this.historyService.pushOperation({ type: OperationType.insertText, value, uri }, { noApply })
  80. }
  81. handleDeleteText(value: TextOperationValue) {
  82. this.historyService.pushOperation({ type: OperationType.deleteText, value }, { noApply: true})
  83. }
  84. handleMultiOperation() {
  85. this.historyService.pushOperation({ type: OperationType.mergeByTime, value: { test: 1} })
  86. this.historyService.pushOperation({ type: OperationType.mergeByTime, value: { test: 2} })
  87. this.historyService.pushOperation({ type: OperationType.mergeByTime, value: { test: 3} })
  88. this.historyService.pushOperation({ type: OperationType.mergeByTime, value: { test: 4} })
  89. }
  90. insertNode(value: NodeOperationValue) {
  91. const { parentId, index, node } = value;
  92. const parent = this.findNodeById(parentId);
  93. if (!parent) {
  94. return
  95. }
  96. parent.children.splice(index, 0, node);
  97. }
  98. deleteNode(value: NodeOperationValue) {
  99. const { parentId, index } = value;
  100. const parent = this.findNodeById(parentId);
  101. if (!parent) {
  102. return
  103. }
  104. parent.children.splice(index, 1);
  105. }
  106. insertText(value: TextOperationValue) {
  107. const { index, text } = value;
  108. this.text = this.text.slice(0, index) + text + this.text.slice(index);
  109. }
  110. deleteText(value: TextOperationValue) {
  111. const { index, text } = value;
  112. this.text = this.text.slice(0, index) + this.text.slice(index + text.length);
  113. }
  114. findNodeById(id: number): Node | null {
  115. const nodes = [this.node];
  116. while (nodes.length) {
  117. const node = nodes.shift() as Node;
  118. if (node.id === id) return node
  119. nodes.push(...node.children);
  120. }
  121. return null;
  122. }
  123. testTransact() {
  124. this.historyService.transact(() => {
  125. this.handleInsertText({ index: 0, text: 'test' })
  126. this.handleInsertText({ index: 4, text: 'test' })
  127. })
  128. }
  129. }
  130. export const insertNodeOperationMeta: OperationMeta = {
  131. type: OperationType.insertNode,
  132. inverse: (op: Operation) => ({ type: OperationType.deleteNode, value: op.value }),
  133. apply: (op: Operation, source: Editor) => {
  134. source.insertNode(op.value as NodeOperationValue)
  135. },
  136. getLabel: (op: Operation) => {
  137. const value = op.value as NodeOperationValue;
  138. return `插入节点${value?.node?.id}`
  139. }
  140. };
  141. export const deleteNodeOperationMeta: OperationMeta = {
  142. type: OperationType.deleteNode,
  143. inverse: (op: Operation) => ({ type: OperationType.insertNode, value: op.value }),
  144. apply: (op: Operation, source: Editor) => {
  145. source.deleteNode(op.value as NodeOperationValue)
  146. },
  147. };
  148. export const insertTextOperationMeta: OperationMeta = {
  149. type: OperationType.insertText,
  150. inverse: (op: Operation) => ({ type: OperationType.deleteText, value: op.value }),
  151. apply: (op: Operation, source: Editor) => {
  152. source.insertText(op.value as TextOperationValue)
  153. },
  154. shouldMerge: (op: Operation, prev: Operation | undefined) => true,
  155. getURI: () => MOCK_URI,
  156. };
  157. export const deleteTextOperationMeta: OperationMeta = {
  158. type: OperationType.deleteText,
  159. inverse: (op: Operation) => ({ type: OperationType.deleteText, value: op.value }),
  160. apply: (op: Operation, source: Editor) => {
  161. source.deleteText(op.value as TextOperationValue)
  162. },
  163. shouldMerge: (op: Operation, prev: Operation | undefined) => op,
  164. };
  165. export const selectionOperationMeta: OperationMeta = {
  166. type: OperationType.selection,
  167. inverse: (op: Operation) => ({ type: OperationType.selection, value: op.value }),
  168. apply: (op: Operation, source: Editor) => {
  169. },
  170. shouldSave: (op: Operation) => false,
  171. };
  172. export const mergeByTimeOperationMeta: OperationMeta = {
  173. type: OperationType.mergeByTime,
  174. inverse: (op: Operation) => ({ type: OperationType.mergeByTime, value: op.value }),
  175. apply: (op: Operation, source: Editor) => {
  176. },
  177. shouldMerge: (op: Operation, prev: Operation | undefined, stackItem: StackOperation) => {
  178. if (Date.now() - stackItem.getTimestamp() < 100) {
  179. return true
  180. }
  181. return false
  182. },
  183. };
  184. export class EditorRegister implements OperationContribution {
  185. registerOperationMeta(operationRegistry: OperationRegistry): void {
  186. operationRegistry.registerOperationMeta(insertNodeOperationMeta);
  187. operationRegistry.registerOperationMeta(deleteNodeOperationMeta);
  188. operationRegistry.registerOperationMeta(insertTextOperationMeta);
  189. operationRegistry.registerOperationMeta(deleteTextOperationMeta);
  190. operationRegistry.registerOperationMeta(selectionOperationMeta);
  191. operationRegistry.registerOperationMeta(mergeByTimeOperationMeta);
  192. }
  193. }
  194. decorate(injectable(), EditorRegister);