e0ab26231c05f0a8f7d05dfa07d8c89120fa37ed9204a9293c48e8d1b30336ac17dfd5e8121f9d429ff9428b252440da82cf0a105a77eb5bc33c15ad5fdfd7 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. import LinkedList from '../../collection/linked-list.js';
  2. import ParchmentError from '../../error.js';
  3. import Scope from '../../scope.js';
  4. import type { Blot, BlotConstructor, Parent, Root } from './blot.js';
  5. import ShadowBlot from './shadow.js';
  6. function makeAttachedBlot(node: Node, scroll: Root): Blot {
  7. const found = scroll.find(node);
  8. if (found) return found;
  9. try {
  10. return scroll.create(node);
  11. } catch (e) {
  12. const blot = scroll.create(Scope.INLINE);
  13. Array.from(node.childNodes).forEach((child: Node) => {
  14. blot.domNode.appendChild(child);
  15. });
  16. if (node.parentNode) {
  17. node.parentNode.replaceChild(blot.domNode, node);
  18. }
  19. blot.attach();
  20. return blot;
  21. }
  22. }
  23. class ParentBlot extends ShadowBlot implements Parent {
  24. /**
  25. * Whitelist array of Blots that can be direct children.
  26. */
  27. public static allowedChildren?: BlotConstructor[];
  28. /**
  29. * Default child blot to be inserted if this blot becomes empty.
  30. */
  31. public static defaultChild?: BlotConstructor;
  32. public static uiClass = '';
  33. public children!: LinkedList<Blot>;
  34. public domNode!: HTMLElement;
  35. public uiNode: HTMLElement | null = null;
  36. constructor(scroll: Root, domNode: Node) {
  37. super(scroll, domNode);
  38. this.build();
  39. }
  40. public appendChild(other: Blot): void {
  41. this.insertBefore(other);
  42. }
  43. public attach(): void {
  44. super.attach();
  45. this.children.forEach((child) => {
  46. child.attach();
  47. });
  48. }
  49. public attachUI(node: HTMLElement): void {
  50. if (this.uiNode != null) {
  51. this.uiNode.remove();
  52. }
  53. this.uiNode = node;
  54. if (ParentBlot.uiClass) {
  55. this.uiNode.classList.add(ParentBlot.uiClass);
  56. }
  57. this.uiNode.setAttribute('contenteditable', 'false');
  58. this.domNode.insertBefore(this.uiNode, this.domNode.firstChild);
  59. }
  60. /**
  61. * Called during construction, should fill its own children LinkedList.
  62. */
  63. public build(): void {
  64. this.children = new LinkedList<Blot>();
  65. // Need to be reversed for if DOM nodes already in order
  66. Array.from(this.domNode.childNodes)
  67. .filter((node: Node) => node !== this.uiNode)
  68. .reverse()
  69. .forEach((node: Node) => {
  70. try {
  71. const child = makeAttachedBlot(node, this.scroll);
  72. this.insertBefore(child, this.children.head || undefined);
  73. } catch (err) {
  74. if (err instanceof ParchmentError) {
  75. return;
  76. } else {
  77. throw err;
  78. }
  79. }
  80. });
  81. }
  82. public deleteAt(index: number, length: number): void {
  83. if (index === 0 && length === this.length()) {
  84. return this.remove();
  85. }
  86. this.children.forEachAt(index, length, (child, offset, childLength) => {
  87. child.deleteAt(offset, childLength);
  88. });
  89. }
  90. public descendant<T extends Blot>(
  91. criteria: new (...args: any[]) => T,
  92. index: number,
  93. ): [T | null, number];
  94. public descendant(
  95. criteria: (blot: Blot) => boolean,
  96. index: number,
  97. ): [Blot | null, number];
  98. public descendant(criteria: any, index = 0): [Blot | null, number] {
  99. const [child, offset] = this.children.find(index);
  100. if (
  101. (criteria.blotName == null && criteria(child)) ||
  102. (criteria.blotName != null && child instanceof criteria)
  103. ) {
  104. return [child as any, offset];
  105. } else if (child instanceof ParentBlot) {
  106. return child.descendant(criteria, offset);
  107. } else {
  108. return [null, -1];
  109. }
  110. }
  111. public descendants<T extends Blot>(
  112. criteria: new (...args: any[]) => T,
  113. index?: number,
  114. length?: number,
  115. ): T[];
  116. public descendants(
  117. criteria: (blot: Blot) => boolean,
  118. index?: number,
  119. length?: number,
  120. ): Blot[];
  121. public descendants(
  122. criteria: any,
  123. index = 0,
  124. length: number = Number.MAX_VALUE,
  125. ): Blot[] {
  126. let descendants: Blot[] = [];
  127. let lengthLeft = length;
  128. this.children.forEachAt(
  129. index,
  130. length,
  131. (child: Blot, childIndex: number, childLength: number) => {
  132. if (
  133. (criteria.blotName == null && criteria(child)) ||
  134. (criteria.blotName != null && child instanceof criteria)
  135. ) {
  136. descendants.push(child);
  137. }
  138. if (child instanceof ParentBlot) {
  139. descendants = descendants.concat(
  140. child.descendants(criteria, childIndex, lengthLeft),
  141. );
  142. }
  143. lengthLeft -= childLength;
  144. },
  145. );
  146. return descendants;
  147. }
  148. public detach(): void {
  149. this.children.forEach((child) => {
  150. child.detach();
  151. });
  152. super.detach();
  153. }
  154. public enforceAllowedChildren(): void {
  155. let done = false;
  156. this.children.forEach((child: Blot) => {
  157. if (done) {
  158. return;
  159. }
  160. const allowed = this.statics.allowedChildren.some(
  161. (def: BlotConstructor) => child instanceof def,
  162. );
  163. if (allowed) {
  164. return;
  165. }
  166. if (child.statics.scope === Scope.BLOCK_BLOT) {
  167. if (child.next != null) {
  168. this.splitAfter(child);
  169. }
  170. if (child.prev != null) {
  171. this.splitAfter(child.prev);
  172. }
  173. child.parent.unwrap();
  174. done = true;
  175. } else if (child instanceof ParentBlot) {
  176. child.unwrap();
  177. } else {
  178. child.remove();
  179. }
  180. });
  181. }
  182. public formatAt(
  183. index: number,
  184. length: number,
  185. name: string,
  186. value: any,
  187. ): void {
  188. this.children.forEachAt(index, length, (child, offset, childLength) => {
  189. child.formatAt(offset, childLength, name, value);
  190. });
  191. }
  192. public insertAt(index: number, value: string, def?: any): void {
  193. const [child, offset] = this.children.find(index);
  194. if (child) {
  195. child.insertAt(offset, value, def);
  196. } else {
  197. const blot =
  198. def == null
  199. ? this.scroll.create('text', value)
  200. : this.scroll.create(value, def);
  201. this.appendChild(blot);
  202. }
  203. }
  204. public insertBefore(childBlot: Blot, refBlot?: Blot | null): void {
  205. if (childBlot.parent != null) {
  206. childBlot.parent.children.remove(childBlot);
  207. }
  208. let refDomNode: Node | null = null;
  209. this.children.insertBefore(childBlot, refBlot || null);
  210. childBlot.parent = this;
  211. if (refBlot != null) {
  212. refDomNode = refBlot.domNode;
  213. }
  214. if (
  215. this.domNode.parentNode !== childBlot.domNode ||
  216. this.domNode.nextSibling !== refDomNode
  217. ) {
  218. this.domNode.insertBefore(childBlot.domNode, refDomNode);
  219. }
  220. childBlot.attach();
  221. }
  222. public length(): number {
  223. return this.children.reduce((memo, child) => {
  224. return memo + child.length();
  225. }, 0);
  226. }
  227. public moveChildren(targetParent: Parent, refNode?: Blot | null): void {
  228. this.children.forEach((child) => {
  229. targetParent.insertBefore(child, refNode);
  230. });
  231. }
  232. public optimize(context?: { [key: string]: any }): void {
  233. super.optimize(context);
  234. this.enforceAllowedChildren();
  235. if (this.uiNode != null && this.uiNode !== this.domNode.firstChild) {
  236. this.domNode.insertBefore(this.uiNode, this.domNode.firstChild);
  237. }
  238. if (this.children.length === 0) {
  239. if (this.statics.defaultChild != null) {
  240. const child = this.scroll.create(this.statics.defaultChild.blotName);
  241. this.appendChild(child);
  242. // TODO double check if necessary
  243. // child.optimize(context);
  244. } else {
  245. this.remove();
  246. }
  247. }
  248. }
  249. public path(index: number, inclusive = false): [Blot, number][] {
  250. const [child, offset] = this.children.find(index, inclusive);
  251. const position: [Blot, number][] = [[this, index]];
  252. if (child instanceof ParentBlot) {
  253. return position.concat(child.path(offset, inclusive));
  254. } else if (child != null) {
  255. position.push([child, offset]);
  256. }
  257. return position;
  258. }
  259. public removeChild(child: Blot): void {
  260. this.children.remove(child);
  261. }
  262. public replaceWith(name: string | Blot, value?: any): Blot {
  263. const replacement =
  264. typeof name === 'string' ? this.scroll.create(name, value) : name;
  265. if (replacement instanceof ParentBlot) {
  266. this.moveChildren(replacement);
  267. }
  268. return super.replaceWith(replacement);
  269. }
  270. public split(index: number, force = false): Blot | null {
  271. if (!force) {
  272. if (index === 0) {
  273. return this;
  274. }
  275. if (index === this.length()) {
  276. return this.next;
  277. }
  278. }
  279. const after = this.clone() as ParentBlot;
  280. if (this.parent) {
  281. this.parent.insertBefore(after, this.next || undefined);
  282. }
  283. this.children.forEachAt(index, this.length(), (child, offset, _length) => {
  284. const split = child.split(offset, force);
  285. if (split != null) {
  286. after.appendChild(split);
  287. }
  288. });
  289. return after;
  290. }
  291. public splitAfter(child: Blot): Parent {
  292. const after = this.clone() as ParentBlot;
  293. while (child.next != null) {
  294. after.appendChild(child.next);
  295. }
  296. if (this.parent) {
  297. this.parent.insertBefore(after, this.next || undefined);
  298. }
  299. return after;
  300. }
  301. public unwrap(): void {
  302. if (this.parent) {
  303. this.moveChildren(this.parent, this.next || undefined);
  304. }
  305. this.remove();
  306. }
  307. public update(
  308. mutations: MutationRecord[],
  309. _context: { [key: string]: any },
  310. ): void {
  311. const addedNodes: Node[] = [];
  312. const removedNodes: Node[] = [];
  313. mutations.forEach((mutation) => {
  314. if (mutation.target === this.domNode && mutation.type === 'childList') {
  315. addedNodes.push(...mutation.addedNodes);
  316. removedNodes.push(...mutation.removedNodes);
  317. }
  318. });
  319. removedNodes.forEach((node: Node) => {
  320. // Check node has actually been removed
  321. // One exception is Chrome does not immediately remove IFRAMEs
  322. // from DOM but MutationRecord is correct in its reported removal
  323. if (
  324. node.parentNode != null &&
  325. // @ts-expect-error Fix me later
  326. node.tagName !== 'IFRAME' &&
  327. document.body.compareDocumentPosition(node) &
  328. Node.DOCUMENT_POSITION_CONTAINED_BY
  329. ) {
  330. return;
  331. }
  332. const blot = this.scroll.find(node);
  333. if (blot == null) {
  334. return;
  335. }
  336. if (
  337. blot.domNode.parentNode == null ||
  338. blot.domNode.parentNode === this.domNode
  339. ) {
  340. blot.detach();
  341. }
  342. });
  343. addedNodes
  344. .filter((node) => {
  345. return node.parentNode === this.domNode && node !== this.uiNode;
  346. })
  347. .sort((a, b) => {
  348. if (a === b) {
  349. return 0;
  350. }
  351. if (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) {
  352. return 1;
  353. }
  354. return -1;
  355. })
  356. .forEach((node) => {
  357. let refBlot: Blot | null = null;
  358. if (node.nextSibling != null) {
  359. refBlot = this.scroll.find(node.nextSibling);
  360. }
  361. const blot = makeAttachedBlot(node, this.scroll);
  362. if (blot.next !== refBlot || blot.next == null) {
  363. if (blot.parent != null) {
  364. blot.parent.removeChild(this);
  365. }
  366. this.insertBefore(blot, refBlot || undefined);
  367. }
  368. });
  369. this.enforceAllowedChildren();
  370. }
  371. }
  372. export default ParentBlot;