import { Blot, Parent, Leaf } from './blot'; import LinkedList from '../../collection/linked-list'; import ShadowBlot from './shadow'; import * as Registry from '../../registry'; class ContainerBlot extends ShadowBlot implements Parent { static defaultChild: string; static allowedChildren: any[]; children!: LinkedList; domNode!: HTMLElement; constructor(domNode: Node) { super(domNode); this.build(); } appendChild(other: Blot): void { this.insertBefore(other); } attach(): void { super.attach(); this.children.forEach(child => { child.attach(); }); } build(): void { this.children = new LinkedList(); // Need to be reversed for if DOM nodes already in order [].slice .call(this.domNode.childNodes) .reverse() .forEach((node: Node) => { try { let child = makeBlot(node); this.insertBefore(child, this.children.head || undefined); } catch (err) { if (err instanceof Registry.ParchmentError) return; else throw err; } }); } deleteAt(index: number, length: number): void { if (index === 0 && length === this.length()) { return this.remove(); } this.children.forEachAt(index, length, function(child, offset, length) { child.deleteAt(offset, length); }); } descendant(criteria: { new (): Blot }, index: number): [Blot | null, number]; descendant(criteria: (blot: Blot) => boolean, index: number): [Blot | null, number]; descendant(criteria: any, index: number): [Blot | null, number] { let [child, offset] = this.children.find(index); if ( (criteria.blotName == null && criteria(child)) || (criteria.blotName != null && child instanceof criteria) ) { return [child, offset]; } else if (child instanceof ContainerBlot) { return child.descendant(criteria, offset); } else { return [null, -1]; } } descendants(criteria: { new (): Blot }, index: number, length: number): Blot[]; descendants(criteria: (blot: Blot) => boolean, index: number, length: number): Blot[]; descendants(criteria: any, index: number = 0, length: number = Number.MAX_VALUE): Blot[] { let descendants: Blot[] = []; let lengthLeft = length; this.children.forEachAt(index, length, function(child: Blot, index: number, length: number) { if ( (criteria.blotName == null && criteria(child)) || (criteria.blotName != null && child instanceof criteria) ) { descendants.push(child); } if (child instanceof ContainerBlot) { descendants = descendants.concat(child.descendants(criteria, index, lengthLeft)); } lengthLeft -= length; }); return descendants; } detach(): void { this.children.forEach(function(child) { child.detach(); }); super.detach(); } formatAt(index: number, length: number, name: string, value: any): void { this.children.forEachAt(index, length, function(child, offset, length) { child.formatAt(offset, length, name, value); }); } insertAt(index: number, value: string, def?: any): void { let [child, offset] = this.children.find(index); if (child) { child.insertAt(offset, value, def); } else { let blot = def == null ? Registry.create('text', value) : Registry.create(value, def); this.appendChild(blot); } } insertBefore(childBlot: Blot, refBlot?: Blot): void { if ( this.statics.allowedChildren != null && !this.statics.allowedChildren.some(function(child: Registry.BlotConstructor) { return childBlot instanceof child; }) ) { throw new Registry.ParchmentError( `Cannot insert ${(childBlot).statics.blotName} into ${this.statics.blotName}`, ); } childBlot.insertInto(this, refBlot); } length(): number { return this.children.reduce(function(memo, child) { return memo + child.length(); }, 0); } moveChildren(targetParent: Parent, refNode?: Blot): void { this.children.forEach(function(child) { targetParent.insertBefore(child, refNode); }); } optimize(context: { [key: string]: any }) { super.optimize(context); if (this.children.length === 0) { if (this.statics.defaultChild != null) { let child = Registry.create(this.statics.defaultChild); this.appendChild(child); child.optimize(context); } else { this.remove(); } } } path(index: number, inclusive: boolean = false): [Blot, number][] { let [child, offset] = this.children.find(index, inclusive); let position: [Blot, number][] = [[this, index]]; if (child instanceof ContainerBlot) { return position.concat(child.path(offset, inclusive)); } else if (child != null) { position.push([child, offset]); } return position; } removeChild(child: Blot): void { this.children.remove(child); } replace(target: Blot): void { if (target instanceof ContainerBlot) { target.moveChildren(this); } super.replace(target); } split(index: number, force: boolean = false): Blot { if (!force) { if (index === 0) return this; if (index === this.length()) return this.next; } let after = this.clone(); this.parent.insertBefore(after, this.next); this.children.forEachAt(index, this.length(), function(child, offset, length) { child = child.split(offset, force); after.appendChild(child); }); return after; } unwrap(): void { this.moveChildren(this.parent, this.next); this.remove(); } update(mutations: MutationRecord[], context: { [key: string]: any }): void { let addedNodes: Node[] = []; let removedNodes: Node[] = []; mutations.forEach(mutation => { if (mutation.target === this.domNode && mutation.type === 'childList') { addedNodes.push.apply(addedNodes, mutation.addedNodes); removedNodes.push.apply(removedNodes, mutation.removedNodes); } }); removedNodes.forEach((node: Node) => { // Check node has actually been removed // One exception is Chrome does not immediately remove IFRAMEs // from DOM but MutationRecord is correct in its reported removal if ( node.parentNode != null && // @ts-ignore node.tagName !== 'IFRAME' && document.body.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_CONTAINED_BY ) { return; } let blot = Registry.find(node); if (blot == null) return; if (blot.domNode.parentNode == null || blot.domNode.parentNode === this.domNode) { blot.detach(); } }); addedNodes .filter(node => { return node.parentNode == this.domNode; }) .sort(function(a, b) { if (a === b) return 0; if (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) { return 1; } return -1; }) .forEach(node => { let refBlot: Blot | null = null; if (node.nextSibling != null) { refBlot = Registry.find(node.nextSibling); } let blot = makeBlot(node); if (blot.next != refBlot || blot.next == null) { if (blot.parent != null) { blot.parent.removeChild(this); } this.insertBefore(blot, refBlot || undefined); } }); } } function makeBlot(node: Node): Blot { let blot = Registry.find(node); if (blot == null) { try { blot = Registry.create(node); } catch (e) { blot = Registry.create(Registry.Scope.INLINE); [].slice.call(node.childNodes).forEach(function(child: Node) { // @ts-ignore blot.domNode.appendChild(child); }); if (node.parentNode) { node.parentNode.replaceChild(blot.domNode, node); } blot.attach(); } } return blot; } export default ContainerBlot;