block.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import Delta from 'quill-delta';
  2. import {
  3. AttributorStore,
  4. BlockBlot,
  5. EmbedBlot,
  6. LeafBlot,
  7. Scope,
  8. } from 'parchment';
  9. import Break from './break';
  10. import Inline from './inline';
  11. import TextBlot from './text';
  12. const NEWLINE_LENGTH = 1;
  13. class Block extends BlockBlot {
  14. constructor(scroll, domNode) {
  15. super(scroll, domNode);
  16. this.cache = {};
  17. }
  18. delta() {
  19. if (this.cache.delta == null) {
  20. this.cache.delta = blockDelta(this);
  21. }
  22. return this.cache.delta;
  23. }
  24. deleteAt(index, length) {
  25. super.deleteAt(index, length);
  26. this.cache = {};
  27. }
  28. formatAt(index, length, name, value) {
  29. if (length <= 0) return;
  30. if (this.scroll.query(name, Scope.BLOCK)) {
  31. if (index + length === this.length()) {
  32. this.format(name, value);
  33. }
  34. } else {
  35. super.formatAt(
  36. index,
  37. Math.min(length, this.length() - index - 1),
  38. name,
  39. value,
  40. );
  41. }
  42. this.cache = {};
  43. }
  44. insertAt(index, value, def) {
  45. if (def != null) {
  46. super.insertAt(index, value, def);
  47. this.cache = {};
  48. return;
  49. }
  50. if (value.length === 0) return;
  51. const lines = value.split('\n');
  52. const text = lines.shift();
  53. if (text.length > 0) {
  54. if (index < this.length() - 1 || this.children.tail == null) {
  55. super.insertAt(Math.min(index, this.length() - 1), text);
  56. } else {
  57. this.children.tail.insertAt(this.children.tail.length(), text);
  58. }
  59. this.cache = {};
  60. }
  61. let block = this;
  62. lines.reduce((lineIndex, line) => {
  63. block = block.split(lineIndex, true);
  64. block.insertAt(0, line);
  65. return line.length;
  66. }, index + text.length);
  67. }
  68. insertBefore(blot, ref) {
  69. const { head } = this.children;
  70. super.insertBefore(blot, ref);
  71. if (head instanceof Break) {
  72. head.remove();
  73. }
  74. this.cache = {};
  75. }
  76. length() {
  77. if (this.cache.length == null) {
  78. this.cache.length = super.length() + NEWLINE_LENGTH;
  79. }
  80. return this.cache.length;
  81. }
  82. moveChildren(target, ref) {
  83. super.moveChildren(target, ref);
  84. this.cache = {};
  85. }
  86. optimize(context) {
  87. super.optimize(context);
  88. this.cache = {};
  89. }
  90. path(index) {
  91. return super.path(index, true);
  92. }
  93. removeChild(child) {
  94. super.removeChild(child);
  95. this.cache = {};
  96. }
  97. split(index, force = false) {
  98. if (force && (index === 0 || index >= this.length() - NEWLINE_LENGTH)) {
  99. const clone = this.clone();
  100. if (index === 0) {
  101. this.parent.insertBefore(clone, this);
  102. return this;
  103. }
  104. this.parent.insertBefore(clone, this.next);
  105. return clone;
  106. }
  107. const next = super.split(index, force);
  108. this.cache = {};
  109. return next;
  110. }
  111. }
  112. Block.blotName = 'block';
  113. Block.tagName = 'P';
  114. Block.defaultChild = Break;
  115. Block.allowedChildren = [Break, Inline, EmbedBlot, TextBlot];
  116. class BlockEmbed extends EmbedBlot {
  117. attach() {
  118. super.attach();
  119. this.attributes = new AttributorStore(this.domNode);
  120. }
  121. delta() {
  122. return new Delta().insert(this.value(), {
  123. ...this.formats(),
  124. ...this.attributes.values(),
  125. });
  126. }
  127. format(name, value) {
  128. const attribute = this.scroll.query(name, Scope.BLOCK_ATTRIBUTE);
  129. if (attribute != null) {
  130. this.attributes.attribute(attribute, value);
  131. }
  132. }
  133. formatAt(index, length, name, value) {
  134. this.format(name, value);
  135. }
  136. insertAt(index, value, def) {
  137. if (typeof value === 'string' && value.endsWith('\n')) {
  138. const block = this.scroll.create(Block.blotName);
  139. this.parent.insertBefore(block, index === 0 ? this : this.next);
  140. block.insertAt(0, value.slice(0, -1));
  141. } else {
  142. super.insertAt(index, value, def);
  143. }
  144. }
  145. }
  146. BlockEmbed.scope = Scope.BLOCK_BLOT;
  147. // It is important for cursor behavior BlockEmbeds use tags that are block level elements
  148. function blockDelta(blot, filter = true) {
  149. return blot
  150. .descendants(LeafBlot)
  151. .reduce((delta, leaf) => {
  152. if (leaf.length() === 0) {
  153. return delta;
  154. }
  155. return delta.insert(leaf.value(), bubbleFormats(leaf, {}, filter));
  156. }, new Delta())
  157. .insert('\n', bubbleFormats(blot));
  158. }
  159. function bubbleFormats(blot, formats = {}, filter = true) {
  160. if (blot == null) return formats;
  161. if (typeof blot.formats === 'function') {
  162. formats = {
  163. ...formats,
  164. ...blot.formats(),
  165. };
  166. if (filter) {
  167. // exclude syntax highlighting from deltas and getFormat()
  168. delete formats['code-token'];
  169. }
  170. }
  171. if (
  172. blot.parent == null ||
  173. blot.parent.statics.blotName === 'scroll' ||
  174. blot.parent.statics.scope !== blot.statics.scope
  175. ) {
  176. return formats;
  177. }
  178. return bubbleFormats(blot.parent, formats, filter);
  179. }
  180. export { blockDelta, bubbleFormats, BlockEmbed, Block as default };