editor.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. import Delta from 'quill-delta';
  2. import DeltaOp from 'quill-delta/lib/op';
  3. import Parchment from 'parchment';
  4. import CodeBlock from '../formats/code';
  5. import CursorBlot from '../blots/cursor';
  6. import Block, { bubbleFormats } from '../blots/block';
  7. import Break from '../blots/break';
  8. import clone from 'clone';
  9. import equal from 'deep-equal';
  10. import extend from 'extend';
  11. const ASCII = /^[ -~]*$/;
  12. class Editor {
  13. constructor(scroll) {
  14. this.scroll = scroll;
  15. this.delta = this.getDelta();
  16. }
  17. applyDelta(delta) {
  18. let consumeNextNewline = false;
  19. this.scroll.update();
  20. let scrollLength = this.scroll.length();
  21. this.scroll.batchStart();
  22. delta = normalizeDelta(delta);
  23. delta.reduce((index, op) => {
  24. let length = op.retain || op.delete || op.insert.length || 1;
  25. let attributes = op.attributes || {};
  26. if (op.insert != null) {
  27. if (typeof op.insert === 'string') {
  28. let text = op.insert;
  29. if (text.endsWith('\n') && consumeNextNewline) {
  30. consumeNextNewline = false;
  31. text = text.slice(0, -1);
  32. }
  33. if (index >= scrollLength && !text.endsWith('\n')) {
  34. consumeNextNewline = true;
  35. }
  36. this.scroll.insertAt(index, text);
  37. let [line, offset] = this.scroll.line(index);
  38. let formats = extend({}, bubbleFormats(line));
  39. if (line instanceof Block) {
  40. let [leaf, ] = line.descendant(Parchment.Leaf, offset);
  41. formats = extend(formats, bubbleFormats(leaf));
  42. }
  43. attributes = DeltaOp.attributes.diff(formats, attributes) || {};
  44. } else if (typeof op.insert === 'object') {
  45. let key = Object.keys(op.insert)[0]; // There should only be one key
  46. if (key == null) return index;
  47. this.scroll.insertAt(index, key, op.insert[key]);
  48. }
  49. scrollLength += length;
  50. }
  51. Object.keys(attributes).forEach((name) => {
  52. this.scroll.formatAt(index, length, name, attributes[name]);
  53. });
  54. return index + length;
  55. }, 0);
  56. delta.reduce((index, op) => {
  57. if (typeof op.delete === 'number') {
  58. this.scroll.deleteAt(index, op.delete);
  59. return index;
  60. }
  61. return index + (op.retain || op.insert.length || 1);
  62. }, 0);
  63. this.scroll.batchEnd();
  64. return this.update(delta);
  65. }
  66. deleteText(index, length) {
  67. this.scroll.deleteAt(index, length);
  68. return this.update(new Delta().retain(index).delete(length));
  69. }
  70. formatLine(index, length, formats = {}) {
  71. this.scroll.update();
  72. Object.keys(formats).forEach((format) => {
  73. if (this.scroll.whitelist != null && !this.scroll.whitelist[format]) return;
  74. let lines = this.scroll.lines(index, Math.max(length, 1));
  75. let lengthRemaining = length;
  76. lines.forEach((line) => {
  77. let lineLength = line.length();
  78. if (!(line instanceof CodeBlock)) {
  79. line.format(format, formats[format]);
  80. } else {
  81. let codeIndex = index - line.offset(this.scroll);
  82. let codeLength = line.newlineIndex(codeIndex + lengthRemaining) - codeIndex + 1;
  83. line.formatAt(codeIndex, codeLength, format, formats[format]);
  84. }
  85. lengthRemaining -= lineLength;
  86. });
  87. });
  88. this.scroll.optimize();
  89. return this.update(new Delta().retain(index).retain(length, clone(formats)));
  90. }
  91. formatText(index, length, formats = {}) {
  92. Object.keys(formats).forEach((format) => {
  93. this.scroll.formatAt(index, length, format, formats[format]);
  94. });
  95. return this.update(new Delta().retain(index).retain(length, clone(formats)));
  96. }
  97. getContents(index, length) {
  98. return this.delta.slice(index, index + length);
  99. }
  100. getDelta() {
  101. return this.scroll.lines().reduce((delta, line) => {
  102. return delta.concat(line.delta());
  103. }, new Delta());
  104. }
  105. getFormat(index, length = 0) {
  106. let lines = [], leaves = [];
  107. if (length === 0) {
  108. this.scroll.path(index).forEach(function(path) {
  109. let [blot, ] = path;
  110. if (blot instanceof Block) {
  111. lines.push(blot);
  112. } else if (blot instanceof Parchment.Leaf) {
  113. leaves.push(blot);
  114. }
  115. });
  116. } else {
  117. lines = this.scroll.lines(index, length);
  118. leaves = this.scroll.descendants(Parchment.Leaf, index, length);
  119. }
  120. let formatsArr = [lines, leaves].map(function(blots) {
  121. if (blots.length === 0) return {};
  122. let formats = bubbleFormats(blots.shift());
  123. while (Object.keys(formats).length > 0) {
  124. let blot = blots.shift();
  125. if (blot == null) return formats;
  126. formats = combineFormats(bubbleFormats(blot), formats);
  127. }
  128. return formats;
  129. });
  130. return extend.apply(extend, formatsArr);
  131. }
  132. getText(index, length) {
  133. return this.getContents(index, length).filter(function(op) {
  134. return typeof op.insert === 'string';
  135. }).map(function(op) {
  136. return op.insert;
  137. }).join('');
  138. }
  139. insertEmbed(index, embed, value) {
  140. this.scroll.insertAt(index, embed, value);
  141. return this.update(new Delta().retain(index).insert({ [embed]: value }));
  142. }
  143. insertText(index, text, formats = {}) {
  144. text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
  145. this.scroll.insertAt(index, text);
  146. Object.keys(formats).forEach((format) => {
  147. this.scroll.formatAt(index, text.length, format, formats[format]);
  148. });
  149. return this.update(new Delta().retain(index).insert(text, clone(formats)));
  150. }
  151. isBlank() {
  152. if (this.scroll.children.length == 0) return true;
  153. if (this.scroll.children.length > 1) return false;
  154. let block = this.scroll.children.head;
  155. if (block.statics.blotName !== Block.blotName) return false;
  156. if (block.children.length > 1) return false;
  157. return block.children.head instanceof Break;
  158. }
  159. removeFormat(index, length) {
  160. let text = this.getText(index, length);
  161. let [line, offset] = this.scroll.line(index + length);
  162. let suffixLength = 0, suffix = new Delta();
  163. if (line != null) {
  164. if (!(line instanceof CodeBlock)) {
  165. suffixLength = line.length() - offset;
  166. } else {
  167. suffixLength = line.newlineIndex(offset) - offset + 1;
  168. }
  169. suffix = line.delta().slice(offset, offset + suffixLength - 1).insert('\n');
  170. }
  171. let contents = this.getContents(index, length + suffixLength);
  172. let diff = contents.diff(new Delta().insert(text).concat(suffix));
  173. let delta = new Delta().retain(index).concat(diff);
  174. return this.applyDelta(delta);
  175. }
  176. update(change, mutations = [], cursorIndex = undefined) {
  177. let oldDelta = this.delta;
  178. if (mutations.length === 1 &&
  179. mutations[0].type === 'characterData' &&
  180. mutations[0].target.data.match(ASCII) &&
  181. Parchment.find(mutations[0].target)) {
  182. // Optimization for character changes
  183. let textBlot = Parchment.find(mutations[0].target);
  184. let formats = bubbleFormats(textBlot);
  185. let index = textBlot.offset(this.scroll);
  186. let oldValue = mutations[0].oldValue.replace(CursorBlot.CONTENTS, '');
  187. let oldText = new Delta().insert(oldValue);
  188. let newText = new Delta().insert(textBlot.value());
  189. let diffDelta = new Delta().retain(index).concat(oldText.diff(newText, cursorIndex));
  190. change = diffDelta.reduce(function(delta, op) {
  191. if (op.insert) {
  192. return delta.insert(op.insert, formats);
  193. } else {
  194. return delta.push(op);
  195. }
  196. }, new Delta());
  197. this.delta = oldDelta.compose(change);
  198. } else {
  199. this.delta = this.getDelta();
  200. if (!change || !equal(oldDelta.compose(change), this.delta)) {
  201. change = oldDelta.diff(this.delta, cursorIndex);
  202. }
  203. }
  204. return change;
  205. }
  206. }
  207. function combineFormats(formats, combined) {
  208. return Object.keys(combined).reduce(function(merged, name) {
  209. if (formats[name] == null) return merged;
  210. if (combined[name] === formats[name]) {
  211. merged[name] = combined[name];
  212. } else if (Array.isArray(combined[name])) {
  213. if (combined[name].indexOf(formats[name]) < 0) {
  214. merged[name] = combined[name].concat([formats[name]]);
  215. }
  216. } else {
  217. merged[name] = [combined[name], formats[name]];
  218. }
  219. return merged;
  220. }, {});
  221. }
  222. function normalizeDelta(delta) {
  223. return delta.reduce(function(delta, op) {
  224. if (op.insert === 1) {
  225. let attributes = clone(op.attributes);
  226. delete attributes['image'];
  227. return delta.insert({ image: op.attributes.image }, attributes);
  228. }
  229. if (op.attributes != null && (op.attributes.list === true || op.attributes.bullet === true)) {
  230. op = clone(op);
  231. if (op.attributes.list) {
  232. op.attributes.list = 'ordered';
  233. } else {
  234. op.attributes.list = 'bullet';
  235. delete op.attributes.bullet;
  236. }
  237. }
  238. if (typeof op.insert === 'string') {
  239. let text = op.insert.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
  240. return delta.insert(text, op.attributes);
  241. }
  242. return delta.push(op);
  243. }, new Delta());
  244. }
  245. export default Editor;