217 lines
6.2 KiB
TypeScript
217 lines
6.2 KiB
TypeScript
import Registry, { type RegistryDefinition } from '../registry.js';
|
|
import Scope from '../scope.js';
|
|
import type { Blot, BlotConstructor, Root } from './abstract/blot.js';
|
|
import ContainerBlot from './abstract/container.js';
|
|
import ParentBlot from './abstract/parent.js';
|
|
import BlockBlot from './block.js';
|
|
|
|
const OBSERVER_CONFIG = {
|
|
attributes: true,
|
|
characterData: true,
|
|
characterDataOldValue: true,
|
|
childList: true,
|
|
subtree: true,
|
|
};
|
|
|
|
const MAX_OPTIMIZE_ITERATIONS = 100;
|
|
|
|
class ScrollBlot extends ParentBlot implements Root {
|
|
public static blotName = 'scroll';
|
|
public static defaultChild = BlockBlot;
|
|
public static allowedChildren: BlotConstructor[] = [BlockBlot, ContainerBlot];
|
|
public static scope = Scope.BLOCK_BLOT;
|
|
public static tagName = 'DIV';
|
|
|
|
public observer: MutationObserver;
|
|
|
|
constructor(
|
|
public registry: Registry,
|
|
node: HTMLDivElement,
|
|
) {
|
|
// @ts-expect-error scroll is the root with no parent
|
|
super(null, node);
|
|
this.scroll = this;
|
|
this.build();
|
|
this.observer = new MutationObserver((mutations: MutationRecord[]) => {
|
|
this.update(mutations);
|
|
});
|
|
this.observer.observe(this.domNode, OBSERVER_CONFIG);
|
|
this.attach();
|
|
}
|
|
|
|
public create(input: Node | string | Scope, value?: any): Blot {
|
|
return this.registry.create(this, input, value);
|
|
}
|
|
|
|
public find(node: Node | null, bubble = false): Blot | null {
|
|
const blot = this.registry.find(node, bubble);
|
|
if (!blot) {
|
|
return null;
|
|
}
|
|
if (blot.scroll === this) {
|
|
return blot;
|
|
}
|
|
return bubble ? this.find(blot.scroll.domNode.parentNode, true) : null;
|
|
}
|
|
|
|
public query(
|
|
query: string | Node | Scope,
|
|
scope: Scope = Scope.ANY,
|
|
): RegistryDefinition | null {
|
|
return this.registry.query(query, scope);
|
|
}
|
|
|
|
public register(...definitions: RegistryDefinition[]) {
|
|
return this.registry.register(...definitions);
|
|
}
|
|
|
|
public build(): void {
|
|
if (this.scroll == null) {
|
|
return;
|
|
}
|
|
super.build();
|
|
}
|
|
|
|
public detach(): void {
|
|
super.detach();
|
|
this.observer.disconnect();
|
|
}
|
|
|
|
public deleteAt(index: number, length: number): void {
|
|
this.update();
|
|
if (index === 0 && length === this.length()) {
|
|
this.children.forEach((child) => {
|
|
child.remove();
|
|
});
|
|
} else {
|
|
super.deleteAt(index, length);
|
|
}
|
|
}
|
|
|
|
public formatAt(
|
|
index: number,
|
|
length: number,
|
|
name: string,
|
|
value: any,
|
|
): void {
|
|
this.update();
|
|
super.formatAt(index, length, name, value);
|
|
}
|
|
|
|
public insertAt(index: number, value: string, def?: any): void {
|
|
this.update();
|
|
super.insertAt(index, value, def);
|
|
}
|
|
|
|
public optimize(context?: { [key: string]: any }): void;
|
|
public optimize(
|
|
mutations: MutationRecord[],
|
|
context: { [key: string]: any },
|
|
): void;
|
|
public optimize(mutations: any = [], context: any = {}): void {
|
|
super.optimize(context);
|
|
const mutationsMap = context.mutationsMap || new WeakMap();
|
|
// We must modify mutations directly, cannot make copy and then modify
|
|
let records = Array.from(this.observer.takeRecords());
|
|
// Array.push currently seems to be implemented by a non-tail recursive function
|
|
// so we cannot just mutations.push.apply(mutations, this.observer.takeRecords());
|
|
while (records.length > 0) {
|
|
mutations.push(records.pop());
|
|
}
|
|
const mark = (blot: Blot | null, markParent = true): void => {
|
|
if (blot == null || blot === this) {
|
|
return;
|
|
}
|
|
if (blot.domNode.parentNode == null) {
|
|
return;
|
|
}
|
|
if (!mutationsMap.has(blot.domNode)) {
|
|
mutationsMap.set(blot.domNode, []);
|
|
}
|
|
if (markParent) {
|
|
mark(blot.parent);
|
|
}
|
|
};
|
|
const optimize = (blot: Blot): void => {
|
|
// Post-order traversal
|
|
if (!mutationsMap.has(blot.domNode)) {
|
|
return;
|
|
}
|
|
if (blot instanceof ParentBlot) {
|
|
blot.children.forEach(optimize);
|
|
}
|
|
mutationsMap.delete(blot.domNode);
|
|
blot.optimize(context);
|
|
};
|
|
let remaining = mutations;
|
|
for (let i = 0; remaining.length > 0; i += 1) {
|
|
if (i >= MAX_OPTIMIZE_ITERATIONS) {
|
|
throw new Error('[Parchment] Maximum optimize iterations reached');
|
|
}
|
|
remaining.forEach((mutation: MutationRecord) => {
|
|
const blot = this.find(mutation.target, true);
|
|
if (blot == null) {
|
|
return;
|
|
}
|
|
if (blot.domNode === mutation.target) {
|
|
if (mutation.type === 'childList') {
|
|
mark(this.find(mutation.previousSibling, false));
|
|
Array.from(mutation.addedNodes).forEach((node: Node) => {
|
|
const child = this.find(node, false);
|
|
mark(child, false);
|
|
if (child instanceof ParentBlot) {
|
|
child.children.forEach((grandChild: Blot) => {
|
|
mark(grandChild, false);
|
|
});
|
|
}
|
|
});
|
|
} else if (mutation.type === 'attributes') {
|
|
mark(blot.prev);
|
|
}
|
|
}
|
|
mark(blot);
|
|
});
|
|
this.children.forEach(optimize);
|
|
remaining = Array.from(this.observer.takeRecords());
|
|
records = remaining.slice();
|
|
while (records.length > 0) {
|
|
mutations.push(records.pop());
|
|
}
|
|
}
|
|
}
|
|
|
|
public update(
|
|
mutations?: MutationRecord[],
|
|
context: { [key: string]: any } = {},
|
|
): void {
|
|
mutations = mutations || this.observer.takeRecords();
|
|
const mutationsMap = new WeakMap();
|
|
mutations
|
|
.map((mutation: MutationRecord) => {
|
|
const blot = this.find(mutation.target, true);
|
|
if (blot == null) {
|
|
return null;
|
|
}
|
|
if (mutationsMap.has(blot.domNode)) {
|
|
mutationsMap.get(blot.domNode).push(mutation);
|
|
return null;
|
|
} else {
|
|
mutationsMap.set(blot.domNode, [mutation]);
|
|
return blot;
|
|
}
|
|
})
|
|
.forEach((blot: Blot | null) => {
|
|
if (blot != null && blot !== this && mutationsMap.has(blot.domNode)) {
|
|
blot.update(mutationsMap.get(blot.domNode) || [], context);
|
|
}
|
|
});
|
|
context.mutationsMap = mutationsMap;
|
|
if (mutationsMap.has(this.domNode)) {
|
|
super.update(mutationsMap.get(this.domNode), context);
|
|
}
|
|
this.optimize(mutations, context);
|
|
}
|
|
}
|
|
|
|
export default ScrollBlot;
|