b9185f323726ac7a8d8853f01fef1877f35ca01c66ea7c6dac0a9ff401f516a8011bc5fce28075312823afb95a8c5daf41bf9840b867f5bd787466048deac6 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import Registry, { type RegistryDefinition } from '../registry.js';
  2. import Scope from '../scope.js';
  3. import type { Blot, BlotConstructor, Root } from './abstract/blot.js';
  4. import ContainerBlot from './abstract/container.js';
  5. import ParentBlot from './abstract/parent.js';
  6. import BlockBlot from './block.js';
  7. const OBSERVER_CONFIG = {
  8. attributes: true,
  9. characterData: true,
  10. characterDataOldValue: true,
  11. childList: true,
  12. subtree: true,
  13. };
  14. const MAX_OPTIMIZE_ITERATIONS = 100;
  15. class ScrollBlot extends ParentBlot implements Root {
  16. public static blotName = 'scroll';
  17. public static defaultChild = BlockBlot;
  18. public static allowedChildren: BlotConstructor[] = [BlockBlot, ContainerBlot];
  19. public static scope = Scope.BLOCK_BLOT;
  20. public static tagName = 'DIV';
  21. public observer: MutationObserver;
  22. constructor(
  23. public registry: Registry,
  24. node: HTMLDivElement,
  25. ) {
  26. // @ts-expect-error scroll is the root with no parent
  27. super(null, node);
  28. this.scroll = this;
  29. this.build();
  30. this.observer = new MutationObserver((mutations: MutationRecord[]) => {
  31. this.update(mutations);
  32. });
  33. this.observer.observe(this.domNode, OBSERVER_CONFIG);
  34. this.attach();
  35. }
  36. public create(input: Node | string | Scope, value?: any): Blot {
  37. return this.registry.create(this, input, value);
  38. }
  39. public find(node: Node | null, bubble = false): Blot | null {
  40. const blot = this.registry.find(node, bubble);
  41. if (!blot) {
  42. return null;
  43. }
  44. if (blot.scroll === this) {
  45. return blot;
  46. }
  47. return bubble ? this.find(blot.scroll.domNode.parentNode, true) : null;
  48. }
  49. public query(
  50. query: string | Node | Scope,
  51. scope: Scope = Scope.ANY,
  52. ): RegistryDefinition | null {
  53. return this.registry.query(query, scope);
  54. }
  55. public register(...definitions: RegistryDefinition[]) {
  56. return this.registry.register(...definitions);
  57. }
  58. public build(): void {
  59. if (this.scroll == null) {
  60. return;
  61. }
  62. super.build();
  63. }
  64. public detach(): void {
  65. super.detach();
  66. this.observer.disconnect();
  67. }
  68. public deleteAt(index: number, length: number): void {
  69. this.update();
  70. if (index === 0 && length === this.length()) {
  71. this.children.forEach((child) => {
  72. child.remove();
  73. });
  74. } else {
  75. super.deleteAt(index, length);
  76. }
  77. }
  78. public formatAt(
  79. index: number,
  80. length: number,
  81. name: string,
  82. value: any,
  83. ): void {
  84. this.update();
  85. super.formatAt(index, length, name, value);
  86. }
  87. public insertAt(index: number, value: string, def?: any): void {
  88. this.update();
  89. super.insertAt(index, value, def);
  90. }
  91. public optimize(context?: { [key: string]: any }): void;
  92. public optimize(
  93. mutations: MutationRecord[],
  94. context: { [key: string]: any },
  95. ): void;
  96. public optimize(mutations: any = [], context: any = {}): void {
  97. super.optimize(context);
  98. const mutationsMap = context.mutationsMap || new WeakMap();
  99. // We must modify mutations directly, cannot make copy and then modify
  100. let records = Array.from(this.observer.takeRecords());
  101. // Array.push currently seems to be implemented by a non-tail recursive function
  102. // so we cannot just mutations.push.apply(mutations, this.observer.takeRecords());
  103. while (records.length > 0) {
  104. mutations.push(records.pop());
  105. }
  106. const mark = (blot: Blot | null, markParent = true): void => {
  107. if (blot == null || blot === this) {
  108. return;
  109. }
  110. if (blot.domNode.parentNode == null) {
  111. return;
  112. }
  113. if (!mutationsMap.has(blot.domNode)) {
  114. mutationsMap.set(blot.domNode, []);
  115. }
  116. if (markParent) {
  117. mark(blot.parent);
  118. }
  119. };
  120. const optimize = (blot: Blot): void => {
  121. // Post-order traversal
  122. if (!mutationsMap.has(blot.domNode)) {
  123. return;
  124. }
  125. if (blot instanceof ParentBlot) {
  126. blot.children.forEach(optimize);
  127. }
  128. mutationsMap.delete(blot.domNode);
  129. blot.optimize(context);
  130. };
  131. let remaining = mutations;
  132. for (let i = 0; remaining.length > 0; i += 1) {
  133. if (i >= MAX_OPTIMIZE_ITERATIONS) {
  134. throw new Error('[Parchment] Maximum optimize iterations reached');
  135. }
  136. remaining.forEach((mutation: MutationRecord) => {
  137. const blot = this.find(mutation.target, true);
  138. if (blot == null) {
  139. return;
  140. }
  141. if (blot.domNode === mutation.target) {
  142. if (mutation.type === 'childList') {
  143. mark(this.find(mutation.previousSibling, false));
  144. Array.from(mutation.addedNodes).forEach((node: Node) => {
  145. const child = this.find(node, false);
  146. mark(child, false);
  147. if (child instanceof ParentBlot) {
  148. child.children.forEach((grandChild: Blot) => {
  149. mark(grandChild, false);
  150. });
  151. }
  152. });
  153. } else if (mutation.type === 'attributes') {
  154. mark(blot.prev);
  155. }
  156. }
  157. mark(blot);
  158. });
  159. this.children.forEach(optimize);
  160. remaining = Array.from(this.observer.takeRecords());
  161. records = remaining.slice();
  162. while (records.length > 0) {
  163. mutations.push(records.pop());
  164. }
  165. }
  166. }
  167. public update(
  168. mutations?: MutationRecord[],
  169. context: { [key: string]: any } = {},
  170. ): void {
  171. mutations = mutations || this.observer.takeRecords();
  172. const mutationsMap = new WeakMap();
  173. mutations
  174. .map((mutation: MutationRecord) => {
  175. const blot = this.find(mutation.target, true);
  176. if (blot == null) {
  177. return null;
  178. }
  179. if (mutationsMap.has(blot.domNode)) {
  180. mutationsMap.get(blot.domNode).push(mutation);
  181. return null;
  182. } else {
  183. mutationsMap.set(blot.domNode, [mutation]);
  184. return blot;
  185. }
  186. })
  187. .forEach((blot: Blot | null) => {
  188. if (blot != null && blot !== this && mutationsMap.has(blot.domNode)) {
  189. blot.update(mutationsMap.get(blot.domNode) || [], context);
  190. }
  191. });
  192. context.mutationsMap = mutationsMap;
  193. if (mutationsMap.has(this.domNode)) {
  194. super.update(mutationsMap.get(this.domNode), context);
  195. }
  196. this.optimize(mutations, context);
  197. }
  198. }
  199. export default ScrollBlot;