123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400 |
- import LinkedList from '../../collection/linked-list.js';
- import ParchmentError from '../../error.js';
- import Scope from '../../scope.js';
- import type { Blot, BlotConstructor, Parent, Root } from './blot.js';
- import ShadowBlot from './shadow.js';
- function makeAttachedBlot(node: Node, scroll: Root): Blot {
- const found = scroll.find(node);
- if (found) return found;
- try {
- return scroll.create(node);
- } catch (e) {
- const blot = scroll.create(Scope.INLINE);
- Array.from(node.childNodes).forEach((child: Node) => {
- blot.domNode.appendChild(child);
- });
- if (node.parentNode) {
- node.parentNode.replaceChild(blot.domNode, node);
- }
- blot.attach();
- return blot;
- }
- }
- class ParentBlot extends ShadowBlot implements Parent {
- /**
- * Whitelist array of Blots that can be direct children.
- */
- public static allowedChildren?: BlotConstructor[];
- /**
- * Default child blot to be inserted if this blot becomes empty.
- */
- public static defaultChild?: BlotConstructor;
- public static uiClass = '';
- public children!: LinkedList<Blot>;
- public domNode!: HTMLElement;
- public uiNode: HTMLElement | null = null;
- constructor(scroll: Root, domNode: Node) {
- super(scroll, domNode);
- this.build();
- }
- public appendChild(other: Blot): void {
- this.insertBefore(other);
- }
- public attach(): void {
- super.attach();
- this.children.forEach((child) => {
- child.attach();
- });
- }
- public attachUI(node: HTMLElement): void {
- if (this.uiNode != null) {
- this.uiNode.remove();
- }
- this.uiNode = node;
- if (ParentBlot.uiClass) {
- this.uiNode.classList.add(ParentBlot.uiClass);
- }
- this.uiNode.setAttribute('contenteditable', 'false');
- this.domNode.insertBefore(this.uiNode, this.domNode.firstChild);
- }
- /**
- * Called during construction, should fill its own children LinkedList.
- */
- public build(): void {
- this.children = new LinkedList<Blot>();
- // Need to be reversed for if DOM nodes already in order
- Array.from(this.domNode.childNodes)
- .filter((node: Node) => node !== this.uiNode)
- .reverse()
- .forEach((node: Node) => {
- try {
- const child = makeAttachedBlot(node, this.scroll);
- this.insertBefore(child, this.children.head || undefined);
- } catch (err) {
- if (err instanceof ParchmentError) {
- return;
- } else {
- throw err;
- }
- }
- });
- }
- public deleteAt(index: number, length: number): void {
- if (index === 0 && length === this.length()) {
- return this.remove();
- }
- this.children.forEachAt(index, length, (child, offset, childLength) => {
- child.deleteAt(offset, childLength);
- });
- }
- public descendant<T extends Blot>(
- criteria: new (...args: any[]) => T,
- index: number,
- ): [T | null, number];
- public descendant(
- criteria: (blot: Blot) => boolean,
- index: number,
- ): [Blot | null, number];
- public descendant(criteria: any, index = 0): [Blot | null, number] {
- const [child, offset] = this.children.find(index);
- if (
- (criteria.blotName == null && criteria(child)) ||
- (criteria.blotName != null && child instanceof criteria)
- ) {
- return [child as any, offset];
- } else if (child instanceof ParentBlot) {
- return child.descendant(criteria, offset);
- } else {
- return [null, -1];
- }
- }
- public descendants<T extends Blot>(
- criteria: new (...args: any[]) => T,
- index?: number,
- length?: number,
- ): T[];
- public descendants(
- criteria: (blot: Blot) => boolean,
- index?: number,
- length?: number,
- ): Blot[];
- public descendants(
- criteria: any,
- index = 0,
- length: number = Number.MAX_VALUE,
- ): Blot[] {
- let descendants: Blot[] = [];
- let lengthLeft = length;
- this.children.forEachAt(
- index,
- length,
- (child: Blot, childIndex: number, childLength: number) => {
- if (
- (criteria.blotName == null && criteria(child)) ||
- (criteria.blotName != null && child instanceof criteria)
- ) {
- descendants.push(child);
- }
- if (child instanceof ParentBlot) {
- descendants = descendants.concat(
- child.descendants(criteria, childIndex, lengthLeft),
- );
- }
- lengthLeft -= childLength;
- },
- );
- return descendants;
- }
- public detach(): void {
- this.children.forEach((child) => {
- child.detach();
- });
- super.detach();
- }
- public enforceAllowedChildren(): void {
- let done = false;
- this.children.forEach((child: Blot) => {
- if (done) {
- return;
- }
- const allowed = this.statics.allowedChildren.some(
- (def: BlotConstructor) => child instanceof def,
- );
- if (allowed) {
- return;
- }
- if (child.statics.scope === Scope.BLOCK_BLOT) {
- if (child.next != null) {
- this.splitAfter(child);
- }
- if (child.prev != null) {
- this.splitAfter(child.prev);
- }
- child.parent.unwrap();
- done = true;
- } else if (child instanceof ParentBlot) {
- child.unwrap();
- } else {
- child.remove();
- }
- });
- }
- public formatAt(
- index: number,
- length: number,
- name: string,
- value: any,
- ): void {
- this.children.forEachAt(index, length, (child, offset, childLength) => {
- child.formatAt(offset, childLength, name, value);
- });
- }
- public insertAt(index: number, value: string, def?: any): void {
- const [child, offset] = this.children.find(index);
- if (child) {
- child.insertAt(offset, value, def);
- } else {
- const blot =
- def == null
- ? this.scroll.create('text', value)
- : this.scroll.create(value, def);
- this.appendChild(blot);
- }
- }
- public insertBefore(childBlot: Blot, refBlot?: Blot | null): void {
- if (childBlot.parent != null) {
- childBlot.parent.children.remove(childBlot);
- }
- let refDomNode: Node | null = null;
- this.children.insertBefore(childBlot, refBlot || null);
- childBlot.parent = this;
- if (refBlot != null) {
- refDomNode = refBlot.domNode;
- }
- if (
- this.domNode.parentNode !== childBlot.domNode ||
- this.domNode.nextSibling !== refDomNode
- ) {
- this.domNode.insertBefore(childBlot.domNode, refDomNode);
- }
- childBlot.attach();
- }
- public length(): number {
- return this.children.reduce((memo, child) => {
- return memo + child.length();
- }, 0);
- }
- public moveChildren(targetParent: Parent, refNode?: Blot | null): void {
- this.children.forEach((child) => {
- targetParent.insertBefore(child, refNode);
- });
- }
- public optimize(context?: { [key: string]: any }): void {
- super.optimize(context);
- this.enforceAllowedChildren();
- if (this.uiNode != null && this.uiNode !== this.domNode.firstChild) {
- this.domNode.insertBefore(this.uiNode, this.domNode.firstChild);
- }
- if (this.children.length === 0) {
- if (this.statics.defaultChild != null) {
- const child = this.scroll.create(this.statics.defaultChild.blotName);
- this.appendChild(child);
- // TODO double check if necessary
- // child.optimize(context);
- } else {
- this.remove();
- }
- }
- }
- public path(index: number, inclusive = false): [Blot, number][] {
- const [child, offset] = this.children.find(index, inclusive);
- const position: [Blot, number][] = [[this, index]];
- if (child instanceof ParentBlot) {
- return position.concat(child.path(offset, inclusive));
- } else if (child != null) {
- position.push([child, offset]);
- }
- return position;
- }
- public removeChild(child: Blot): void {
- this.children.remove(child);
- }
- public replaceWith(name: string | Blot, value?: any): Blot {
- const replacement =
- typeof name === 'string' ? this.scroll.create(name, value) : name;
- if (replacement instanceof ParentBlot) {
- this.moveChildren(replacement);
- }
- return super.replaceWith(replacement);
- }
- public split(index: number, force = false): Blot | null {
- if (!force) {
- if (index === 0) {
- return this;
- }
- if (index === this.length()) {
- return this.next;
- }
- }
- const after = this.clone() as ParentBlot;
- if (this.parent) {
- this.parent.insertBefore(after, this.next || undefined);
- }
- this.children.forEachAt(index, this.length(), (child, offset, _length) => {
- const split = child.split(offset, force);
- if (split != null) {
- after.appendChild(split);
- }
- });
- return after;
- }
- public splitAfter(child: Blot): Parent {
- const after = this.clone() as ParentBlot;
- while (child.next != null) {
- after.appendChild(child.next);
- }
- if (this.parent) {
- this.parent.insertBefore(after, this.next || undefined);
- }
- return after;
- }
- public unwrap(): void {
- if (this.parent) {
- this.moveChildren(this.parent, this.next || undefined);
- }
- this.remove();
- }
- public update(
- mutations: MutationRecord[],
- _context: { [key: string]: any },
- ): void {
- const addedNodes: Node[] = [];
- const removedNodes: Node[] = [];
- mutations.forEach((mutation) => {
- if (mutation.target === this.domNode && mutation.type === 'childList') {
- addedNodes.push(...mutation.addedNodes);
- removedNodes.push(...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-expect-error Fix me later
- node.tagName !== 'IFRAME' &&
- document.body.compareDocumentPosition(node) &
- Node.DOCUMENT_POSITION_CONTAINED_BY
- ) {
- return;
- }
- const blot = this.scroll.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 && node !== this.uiNode;
- })
- .sort((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 = this.scroll.find(node.nextSibling);
- }
- const blot = makeAttachedBlot(node, this.scroll);
- if (blot.next !== refBlot || blot.next == null) {
- if (blot.parent != null) {
- blot.parent.removeChild(this);
- }
- this.insertBefore(blot, refBlot || undefined);
- }
- });
- this.enforceAllowedChildren();
- }
- }
- export default ParentBlot;
|