editor.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. import cloneDeep from 'lodash.clonedeep';
  2. import isEqual from 'lodash.isequal';
  3. import merge from 'lodash.merge';
  4. import Delta, { AttributeMap } from 'quill-delta';
  5. import { LeafBlot } from 'parchment';
  6. import { Range } from './selection';
  7. import CursorBlot from '../blots/cursor';
  8. import Block, { BlockEmbed, bubbleFormats } from '../blots/block';
  9. import Break from '../blots/break';
  10. import TextBlot, { escapeText } from '../blots/text';
  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. const normalizedDelta = normalizeDelta(delta);
  23. normalizedDelta.reduce((index, op) => {
  24. const 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 (
  34. (index >= scrollLength ||
  35. this.scroll.descendant(BlockEmbed, index)[0]) &&
  36. !text.endsWith('\n')
  37. ) {
  38. consumeNextNewline = true;
  39. }
  40. this.scroll.insertAt(index, text);
  41. const [line, offset] = this.scroll.line(index);
  42. let formats = merge({}, bubbleFormats(line));
  43. if (line instanceof Block) {
  44. const [leaf] = line.descendant(LeafBlot, offset);
  45. formats = merge(formats, bubbleFormats(leaf));
  46. }
  47. attributes = AttributeMap.diff(formats, attributes) || {};
  48. } else if (typeof op.insert === 'object') {
  49. const key = Object.keys(op.insert)[0]; // There should only be one key
  50. if (key == null) return index;
  51. this.scroll.insertAt(index, key, op.insert[key]);
  52. }
  53. scrollLength += length;
  54. }
  55. Object.keys(attributes).forEach(name => {
  56. this.scroll.formatAt(index, length, name, attributes[name]);
  57. });
  58. return index + length;
  59. }, 0);
  60. normalizedDelta.reduce((index, op) => {
  61. if (typeof op.delete === 'number') {
  62. this.scroll.deleteAt(index, op.delete);
  63. return index;
  64. }
  65. return index + (op.retain || op.insert.length || 1);
  66. }, 0);
  67. this.scroll.batchEnd();
  68. this.scroll.optimize();
  69. return this.update(normalizedDelta);
  70. }
  71. deleteText(index, length) {
  72. this.scroll.deleteAt(index, length);
  73. return this.update(new Delta().retain(index).delete(length));
  74. }
  75. formatLine(index, length, formats = {}) {
  76. this.scroll.update();
  77. Object.keys(formats).forEach(format => {
  78. this.scroll.lines(index, Math.max(length, 1)).forEach(line => {
  79. line.format(format, formats[format]);
  80. });
  81. });
  82. this.scroll.optimize();
  83. const delta = new Delta().retain(index).retain(length, cloneDeep(formats));
  84. return this.update(delta);
  85. }
  86. formatText(index, length, formats = {}) {
  87. Object.keys(formats).forEach(format => {
  88. this.scroll.formatAt(index, length, format, formats[format]);
  89. });
  90. const delta = new Delta().retain(index).retain(length, cloneDeep(formats));
  91. return this.update(delta);
  92. }
  93. getContents(index, length) {
  94. return this.delta.slice(index, index + length);
  95. }
  96. getDelta() {
  97. return this.scroll.lines().reduce((delta, line) => {
  98. return delta.concat(line.delta());
  99. }, new Delta());
  100. }
  101. getFormat(index, length = 0) {
  102. let lines = [];
  103. let leaves = [];
  104. if (length === 0) {
  105. this.scroll.path(index).forEach(path => {
  106. const [blot] = path;
  107. if (blot instanceof Block) {
  108. lines.push(blot);
  109. } else if (blot instanceof LeafBlot) {
  110. leaves.push(blot);
  111. }
  112. });
  113. } else {
  114. lines = this.scroll.lines(index, length);
  115. leaves = this.scroll.descendants(LeafBlot, index, length);
  116. }
  117. [lines, leaves] = [lines, leaves].map(blots => {
  118. if (blots.length === 0) return {};
  119. let formats = bubbleFormats(blots.shift());
  120. while (Object.keys(formats).length > 0) {
  121. const blot = blots.shift();
  122. if (blot == null) return formats;
  123. formats = combineFormats(bubbleFormats(blot), formats);
  124. }
  125. return formats;
  126. });
  127. return { ...lines, ...leaves };
  128. }
  129. getHTML(index, length) {
  130. const [line, lineOffset] = this.scroll.line(index);
  131. if (line.length() >= lineOffset + length) {
  132. return convertHTML(line, lineOffset, length, true);
  133. }
  134. return convertHTML(this.scroll, index, length, true);
  135. }
  136. getText(index, length) {
  137. return this.getContents(index, length)
  138. .filter(op => typeof op.insert === 'string')
  139. .map(op => op.insert)
  140. .join('');
  141. }
  142. insertEmbed(index, embed, value) {
  143. this.scroll.insertAt(index, embed, value);
  144. return this.update(new Delta().retain(index).insert({ [embed]: value }));
  145. }
  146. insertText(index, text, formats = {}) {
  147. text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
  148. this.scroll.insertAt(index, text);
  149. Object.keys(formats).forEach(format => {
  150. this.scroll.formatAt(index, text.length, format, formats[format]);
  151. });
  152. return this.update(
  153. new Delta().retain(index).insert(text, cloneDeep(formats)),
  154. );
  155. }
  156. isBlank() {
  157. if (this.scroll.children.length === 0) return true;
  158. if (this.scroll.children.length > 1) return false;
  159. const block = this.scroll.children.head;
  160. if (block.statics.blotName !== Block.blotName) return false;
  161. if (block.children.length > 1) return false;
  162. return block.children.head instanceof Break;
  163. }
  164. removeFormat(index, length) {
  165. const text = this.getText(index, length);
  166. const [line, offset] = this.scroll.line(index + length);
  167. let suffixLength = 0;
  168. let suffix = new Delta();
  169. if (line != null) {
  170. suffixLength = line.length() - offset;
  171. suffix = line
  172. .delta()
  173. .slice(offset, offset + suffixLength - 1)
  174. .insert('\n');
  175. }
  176. const contents = this.getContents(index, length + suffixLength);
  177. const diff = contents.diff(new Delta().insert(text).concat(suffix));
  178. const delta = new Delta().retain(index).concat(diff);
  179. return this.applyDelta(delta);
  180. }
  181. update(change, mutations = [], selectionInfo = undefined) {
  182. const oldDelta = this.delta;
  183. if (
  184. mutations.length === 1 &&
  185. mutations[0].type === 'characterData' &&
  186. mutations[0].target.data.match(ASCII) &&
  187. this.scroll.find(mutations[0].target)
  188. ) {
  189. // Optimization for character changes
  190. const textBlot = this.scroll.find(mutations[0].target);
  191. const formats = bubbleFormats(textBlot);
  192. const index = textBlot.offset(this.scroll);
  193. const oldValue = mutations[0].oldValue.replace(CursorBlot.CONTENTS, '');
  194. const oldText = new Delta().insert(oldValue);
  195. const newText = new Delta().insert(textBlot.value());
  196. const relativeSelectionInfo = selectionInfo && {
  197. oldRange: shiftRange(selectionInfo.oldRange, -index),
  198. newRange: shiftRange(selectionInfo.newRange, -index),
  199. };
  200. const diffDelta = new Delta()
  201. .retain(index)
  202. .concat(oldText.diff(newText, relativeSelectionInfo));
  203. change = diffDelta.reduce((delta, op) => {
  204. if (op.insert) {
  205. return delta.insert(op.insert, formats);
  206. }
  207. return delta.push(op);
  208. }, new Delta());
  209. this.delta = oldDelta.compose(change);
  210. } else {
  211. this.delta = this.getDelta();
  212. if (!change || !isEqual(oldDelta.compose(change), this.delta)) {
  213. change = oldDelta.diff(this.delta, selectionInfo);
  214. }
  215. }
  216. return change;
  217. }
  218. }
  219. function convertListHTML(items, lastIndent, types) {
  220. if (items.length === 0) {
  221. const [endTag] = getListType(types.pop());
  222. if (lastIndent <= 0) {
  223. return `</li></${endTag}>`;
  224. }
  225. return `</li></${endTag}>${convertListHTML([], lastIndent - 1, types)}`;
  226. }
  227. const [{ child, offset, length, indent, type }, ...rest] = items;
  228. const [tag, attribute] = getListType(type);
  229. if (indent > lastIndent) {
  230. types.push(type);
  231. if (indent === lastIndent + 1) {
  232. return `<${tag}><li${attribute}>${convertHTML(
  233. child,
  234. offset,
  235. length,
  236. )}${convertListHTML(rest, indent, types)}`;
  237. }
  238. return `<${tag}><li>${convertListHTML(items, lastIndent + 1, types)}`;
  239. }
  240. const previousType = types[types.length - 1];
  241. if (indent === lastIndent && type === previousType) {
  242. return `</li><li${attribute}>${convertHTML(
  243. child,
  244. offset,
  245. length,
  246. )}${convertListHTML(rest, indent, types)}`;
  247. }
  248. const [endTag] = getListType(types.pop());
  249. return `</li></${endTag}>${convertListHTML(items, lastIndent - 1, types)}`;
  250. }
  251. function convertHTML(blot, index, length, isRoot = false) {
  252. if (typeof blot.html === 'function') {
  253. return blot.html(index, length);
  254. }
  255. if (blot instanceof TextBlot) {
  256. return escapeText(blot.value().slice(index, index + length));
  257. }
  258. if (blot.children) {
  259. // TODO fix API
  260. if (blot.statics.blotName === 'list-container') {
  261. const items = [];
  262. blot.children.forEachAt(index, length, (child, offset, childLength) => {
  263. const formats = child.formats();
  264. items.push({
  265. child,
  266. offset,
  267. length: childLength,
  268. indent: formats.indent || 0,
  269. type: formats.list,
  270. });
  271. });
  272. return convertListHTML(items, -1, []);
  273. }
  274. const parts = [];
  275. blot.children.forEachAt(index, length, (child, offset, childLength) => {
  276. parts.push(convertHTML(child, offset, childLength));
  277. });
  278. if (isRoot || blot.statics.blotName === 'list') {
  279. return parts.join('');
  280. }
  281. const { outerHTML, innerHTML } = blot.domNode;
  282. const [start, end] = outerHTML.split(`>${innerHTML}<`);
  283. // TODO cleanup
  284. if (start === '<table') {
  285. return `<table style="border: 1px solid #000;">${parts.join('')}<${end}`;
  286. }
  287. return `${start}>${parts.join('')}<${end}`;
  288. }
  289. return blot.domNode.outerHTML;
  290. }
  291. function combineFormats(formats, combined) {
  292. return Object.keys(combined).reduce((merged, name) => {
  293. if (formats[name] == null) return merged;
  294. if (combined[name] === formats[name]) {
  295. merged[name] = combined[name];
  296. } else if (Array.isArray(combined[name])) {
  297. if (combined[name].indexOf(formats[name]) < 0) {
  298. merged[name] = combined[name].concat([formats[name]]);
  299. }
  300. } else {
  301. merged[name] = [combined[name], formats[name]];
  302. }
  303. return merged;
  304. }, {});
  305. }
  306. function getListType(type) {
  307. const tag = type === 'ordered' ? 'ol' : 'ul';
  308. switch (type) {
  309. case 'checked':
  310. return [tag, ' data-list="checked"'];
  311. case 'unchecked':
  312. return [tag, ' data-list="unchecked"'];
  313. default:
  314. return [tag, ''];
  315. }
  316. }
  317. function normalizeDelta(delta) {
  318. return delta.reduce((normalizedDelta, op) => {
  319. if (typeof op.insert === 'string') {
  320. const text = op.insert.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
  321. return normalizedDelta.insert(text, op.attributes);
  322. }
  323. return normalizedDelta.push(op);
  324. }, new Delta());
  325. }
  326. function shiftRange({ index, length }, amount) {
  327. return new Range(index + amount, length);
  328. }
  329. export default Editor;