2b971da6da3f7af40407719eec21fc1f15642a9243deab13f9a283458a0f709245f52b4a5c142f920b10bd0a6b04c79f23f01df74bfcd02d49a09362a13af6 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. import { cloneDeep, isEqual, merge } from 'lodash-es';
  2. import { LeafBlot, EmbedBlot, Scope, ParentBlot } from 'parchment';
  3. import Delta, { AttributeMap, Op } from 'quill-delta';
  4. import Block, { BlockEmbed, bubbleFormats } from '../blots/block.js';
  5. import Break from '../blots/break.js';
  6. import CursorBlot from '../blots/cursor.js';
  7. import TextBlot, { escapeText } from '../blots/text.js';
  8. import { Range } from './selection.js';
  9. const ASCII = /^[ -~]*$/;
  10. class Editor {
  11. constructor(scroll) {
  12. this.scroll = scroll;
  13. this.delta = this.getDelta();
  14. }
  15. applyDelta(delta) {
  16. this.scroll.update();
  17. let scrollLength = this.scroll.length();
  18. this.scroll.batchStart();
  19. const normalizedDelta = normalizeDelta(delta);
  20. const deleteDelta = new Delta();
  21. const normalizedOps = splitOpLines(normalizedDelta.ops.slice());
  22. normalizedOps.reduce((index, op) => {
  23. const length = Op.length(op);
  24. let attributes = op.attributes || {};
  25. let isImplicitNewlinePrepended = false;
  26. let isImplicitNewlineAppended = false;
  27. if (op.insert != null) {
  28. deleteDelta.retain(length);
  29. if (typeof op.insert === 'string') {
  30. const text = op.insert;
  31. isImplicitNewlineAppended = !text.endsWith('\n') && (scrollLength <= index || !!this.scroll.descendant(BlockEmbed, index)[0]);
  32. this.scroll.insertAt(index, text);
  33. const [line, offset] = this.scroll.line(index);
  34. let formats = merge({}, bubbleFormats(line));
  35. if (line instanceof Block) {
  36. const [leaf] = line.descendant(LeafBlot, offset);
  37. if (leaf) {
  38. formats = merge(formats, bubbleFormats(leaf));
  39. }
  40. }
  41. attributes = AttributeMap.diff(formats, attributes) || {};
  42. } else if (typeof op.insert === 'object') {
  43. const key = Object.keys(op.insert)[0]; // There should only be one key
  44. if (key == null) return index;
  45. const isInlineEmbed = this.scroll.query(key, Scope.INLINE) != null;
  46. if (isInlineEmbed) {
  47. if (scrollLength <= index || !!this.scroll.descendant(BlockEmbed, index)[0]) {
  48. isImplicitNewlineAppended = true;
  49. }
  50. } else if (index > 0) {
  51. const [leaf, offset] = this.scroll.descendant(LeafBlot, index - 1);
  52. if (leaf instanceof TextBlot) {
  53. const text = leaf.value();
  54. if (text[offset] !== '\n') {
  55. isImplicitNewlinePrepended = true;
  56. }
  57. } else if (leaf instanceof EmbedBlot && leaf.statics.scope === Scope.INLINE_BLOT) {
  58. isImplicitNewlinePrepended = true;
  59. }
  60. }
  61. this.scroll.insertAt(index, key, op.insert[key]);
  62. if (isInlineEmbed) {
  63. const [leaf] = this.scroll.descendant(LeafBlot, index);
  64. if (leaf) {
  65. const formats = merge({}, bubbleFormats(leaf));
  66. attributes = AttributeMap.diff(formats, attributes) || {};
  67. }
  68. }
  69. }
  70. scrollLength += length;
  71. } else {
  72. deleteDelta.push(op);
  73. if (op.retain !== null && typeof op.retain === 'object') {
  74. const key = Object.keys(op.retain)[0];
  75. if (key == null) return index;
  76. this.scroll.updateEmbedAt(index, key, op.retain[key]);
  77. }
  78. }
  79. Object.keys(attributes).forEach(name => {
  80. this.scroll.formatAt(index, length, name, attributes[name]);
  81. });
  82. const prependedLength = isImplicitNewlinePrepended ? 1 : 0;
  83. const addedLength = isImplicitNewlineAppended ? 1 : 0;
  84. scrollLength += prependedLength + addedLength;
  85. deleteDelta.retain(prependedLength);
  86. deleteDelta.delete(addedLength);
  87. return index + length + prependedLength + addedLength;
  88. }, 0);
  89. deleteDelta.reduce((index, op) => {
  90. if (typeof op.delete === 'number') {
  91. this.scroll.deleteAt(index, op.delete);
  92. return index;
  93. }
  94. return index + Op.length(op);
  95. }, 0);
  96. this.scroll.batchEnd();
  97. this.scroll.optimize();
  98. return this.update(normalizedDelta);
  99. }
  100. deleteText(index, length) {
  101. this.scroll.deleteAt(index, length);
  102. return this.update(new Delta().retain(index).delete(length));
  103. }
  104. formatLine(index, length) {
  105. let formats = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
  106. this.scroll.update();
  107. Object.keys(formats).forEach(format => {
  108. this.scroll.lines(index, Math.max(length, 1)).forEach(line => {
  109. line.format(format, formats[format]);
  110. });
  111. });
  112. this.scroll.optimize();
  113. const delta = new Delta().retain(index).retain(length, cloneDeep(formats));
  114. return this.update(delta);
  115. }
  116. formatText(index, length) {
  117. let formats = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
  118. Object.keys(formats).forEach(format => {
  119. this.scroll.formatAt(index, length, format, formats[format]);
  120. });
  121. const delta = new Delta().retain(index).retain(length, cloneDeep(formats));
  122. return this.update(delta);
  123. }
  124. getContents(index, length) {
  125. return this.delta.slice(index, index + length);
  126. }
  127. getDelta() {
  128. return this.scroll.lines().reduce((delta, line) => {
  129. return delta.concat(line.delta());
  130. }, new Delta());
  131. }
  132. getFormat(index) {
  133. let length = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
  134. let lines = [];
  135. let leaves = [];
  136. if (length === 0) {
  137. this.scroll.path(index).forEach(path => {
  138. const [blot] = path;
  139. if (blot instanceof Block) {
  140. lines.push(blot);
  141. } else if (blot instanceof LeafBlot) {
  142. leaves.push(blot);
  143. }
  144. });
  145. } else {
  146. lines = this.scroll.lines(index, length);
  147. leaves = this.scroll.descendants(LeafBlot, index, length);
  148. }
  149. const [lineFormats, leafFormats] = [lines, leaves].map(blots => {
  150. const blot = blots.shift();
  151. if (blot == null) return {};
  152. let formats = bubbleFormats(blot);
  153. while (Object.keys(formats).length > 0) {
  154. const blot = blots.shift();
  155. if (blot == null) return formats;
  156. formats = combineFormats(bubbleFormats(blot), formats);
  157. }
  158. return formats;
  159. });
  160. return {
  161. ...lineFormats,
  162. ...leafFormats
  163. };
  164. }
  165. getHTML(index, length) {
  166. const [line, lineOffset] = this.scroll.line(index);
  167. if (line) {
  168. const lineLength = line.length();
  169. const isWithinLine = line.length() >= lineOffset + length;
  170. if (isWithinLine && !(lineOffset === 0 && length === lineLength)) {
  171. return convertHTML(line, lineOffset, length, true);
  172. }
  173. return convertHTML(this.scroll, index, length, true);
  174. }
  175. return '';
  176. }
  177. getText(index, length) {
  178. return this.getContents(index, length).filter(op => typeof op.insert === 'string').map(op => op.insert).join('');
  179. }
  180. insertContents(index, contents) {
  181. const normalizedDelta = normalizeDelta(contents);
  182. const change = new Delta().retain(index).concat(normalizedDelta);
  183. this.scroll.insertContents(index, normalizedDelta);
  184. return this.update(change);
  185. }
  186. insertEmbed(index, embed, value) {
  187. this.scroll.insertAt(index, embed, value);
  188. return this.update(new Delta().retain(index).insert({
  189. [embed]: value
  190. }));
  191. }
  192. insertText(index, text) {
  193. let formats = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
  194. text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
  195. this.scroll.insertAt(index, text);
  196. Object.keys(formats).forEach(format => {
  197. this.scroll.formatAt(index, text.length, format, formats[format]);
  198. });
  199. return this.update(new Delta().retain(index).insert(text, cloneDeep(formats)));
  200. }
  201. isBlank() {
  202. if (this.scroll.children.length === 0) return true;
  203. if (this.scroll.children.length > 1) return false;
  204. const blot = this.scroll.children.head;
  205. if (blot?.statics.blotName !== Block.blotName) return false;
  206. const block = blot;
  207. if (block.children.length > 1) return false;
  208. return block.children.head instanceof Break;
  209. }
  210. removeFormat(index, length) {
  211. const text = this.getText(index, length);
  212. const [line, offset] = this.scroll.line(index + length);
  213. let suffixLength = 0;
  214. let suffix = new Delta();
  215. if (line != null) {
  216. suffixLength = line.length() - offset;
  217. suffix = line.delta().slice(offset, offset + suffixLength - 1).insert('\n');
  218. }
  219. const contents = this.getContents(index, length + suffixLength);
  220. const diff = contents.diff(new Delta().insert(text).concat(suffix));
  221. const delta = new Delta().retain(index).concat(diff);
  222. return this.applyDelta(delta);
  223. }
  224. update(change) {
  225. let mutations = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
  226. let selectionInfo = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : undefined;
  227. const oldDelta = this.delta;
  228. if (mutations.length === 1 && mutations[0].type === 'characterData' &&
  229. // @ts-expect-error Fix me later
  230. mutations[0].target.data.match(ASCII) && this.scroll.find(mutations[0].target)) {
  231. // Optimization for character changes
  232. const textBlot = this.scroll.find(mutations[0].target);
  233. const formats = bubbleFormats(textBlot);
  234. const index = textBlot.offset(this.scroll);
  235. // @ts-expect-error Fix me later
  236. const oldValue = mutations[0].oldValue.replace(CursorBlot.CONTENTS, '');
  237. const oldText = new Delta().insert(oldValue);
  238. // @ts-expect-error
  239. const newText = new Delta().insert(textBlot.value());
  240. const relativeSelectionInfo = selectionInfo && {
  241. oldRange: shiftRange(selectionInfo.oldRange, -index),
  242. newRange: shiftRange(selectionInfo.newRange, -index)
  243. };
  244. const diffDelta = new Delta().retain(index).concat(oldText.diff(newText, relativeSelectionInfo));
  245. change = diffDelta.reduce((delta, op) => {
  246. if (op.insert) {
  247. return delta.insert(op.insert, formats);
  248. }
  249. return delta.push(op);
  250. }, new Delta());
  251. this.delta = oldDelta.compose(change);
  252. } else {
  253. this.delta = this.getDelta();
  254. if (!change || !isEqual(oldDelta.compose(change), this.delta)) {
  255. change = oldDelta.diff(this.delta, selectionInfo);
  256. }
  257. }
  258. return change;
  259. }
  260. }
  261. function convertListHTML(items, lastIndent, types) {
  262. if (items.length === 0) {
  263. const [endTag] = getListType(types.pop());
  264. if (lastIndent <= 0) {
  265. return `</li></${endTag}>`;
  266. }
  267. return `</li></${endTag}>${convertListHTML([], lastIndent - 1, types)}`;
  268. }
  269. const [{
  270. child,
  271. offset,
  272. length,
  273. indent,
  274. type
  275. }, ...rest] = items;
  276. const [tag, attribute] = getListType(type);
  277. if (indent > lastIndent) {
  278. types.push(type);
  279. if (indent === lastIndent + 1) {
  280. return `<${tag}><li${attribute}>${convertHTML(child, offset, length)}${convertListHTML(rest, indent, types)}`;
  281. }
  282. return `<${tag}><li>${convertListHTML(items, lastIndent + 1, types)}`;
  283. }
  284. const previousType = types[types.length - 1];
  285. if (indent === lastIndent && type === previousType) {
  286. return `</li><li${attribute}>${convertHTML(child, offset, length)}${convertListHTML(rest, indent, types)}`;
  287. }
  288. const [endTag] = getListType(types.pop());
  289. return `</li></${endTag}>${convertListHTML(items, lastIndent - 1, types)}`;
  290. }
  291. function convertHTML(blot, index, length) {
  292. let isRoot = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
  293. if ('html' in blot && typeof blot.html === 'function') {
  294. return blot.html(index, length);
  295. }
  296. if (blot instanceof TextBlot) {
  297. return escapeText(blot.value().slice(index, index + length));
  298. }
  299. if (blot instanceof ParentBlot) {
  300. // TODO fix API
  301. if (blot.statics.blotName === 'list-container') {
  302. const items = [];
  303. blot.children.forEachAt(index, length, (child, offset, childLength) => {
  304. const formats = 'formats' in child && typeof child.formats === 'function' ? child.formats() : {};
  305. items.push({
  306. child,
  307. offset,
  308. length: childLength,
  309. indent: formats.indent || 0,
  310. type: formats.list
  311. });
  312. });
  313. return convertListHTML(items, -1, []);
  314. }
  315. const parts = [];
  316. blot.children.forEachAt(index, length, (child, offset, childLength) => {
  317. parts.push(convertHTML(child, offset, childLength));
  318. });
  319. if (isRoot || blot.statics.blotName === 'list') {
  320. return parts.join('');
  321. }
  322. const {
  323. outerHTML,
  324. innerHTML
  325. } = blot.domNode;
  326. const [start, end] = outerHTML.split(`>${innerHTML}<`);
  327. // TODO cleanup
  328. if (start === '<table') {
  329. return `<table style="border: 1px solid #000;">${parts.join('')}<${end}`;
  330. }
  331. return `${start}>${parts.join('')}<${end}`;
  332. }
  333. return blot.domNode instanceof Element ? blot.domNode.outerHTML : '';
  334. }
  335. function combineFormats(formats, combined) {
  336. return Object.keys(combined).reduce((merged, name) => {
  337. if (formats[name] == null) return merged;
  338. const combinedValue = combined[name];
  339. if (combinedValue === formats[name]) {
  340. merged[name] = combinedValue;
  341. } else if (Array.isArray(combinedValue)) {
  342. if (combinedValue.indexOf(formats[name]) < 0) {
  343. merged[name] = combinedValue.concat([formats[name]]);
  344. } else {
  345. // If style already exists, don't add to an array, but don't lose other styles
  346. merged[name] = combinedValue;
  347. }
  348. } else {
  349. merged[name] = [combinedValue, formats[name]];
  350. }
  351. return merged;
  352. }, {});
  353. }
  354. function getListType(type) {
  355. const tag = type === 'ordered' ? 'ol' : 'ul';
  356. switch (type) {
  357. case 'checked':
  358. return [tag, ' data-list="checked"'];
  359. case 'unchecked':
  360. return [tag, ' data-list="unchecked"'];
  361. default:
  362. return [tag, ''];
  363. }
  364. }
  365. function normalizeDelta(delta) {
  366. return delta.reduce((normalizedDelta, op) => {
  367. if (typeof op.insert === 'string') {
  368. const text = op.insert.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
  369. return normalizedDelta.insert(text, op.attributes);
  370. }
  371. return normalizedDelta.push(op);
  372. }, new Delta());
  373. }
  374. function shiftRange(_ref, amount) {
  375. let {
  376. index,
  377. length
  378. } = _ref;
  379. return new Range(index + amount, length);
  380. }
  381. function splitOpLines(ops) {
  382. const split = [];
  383. ops.forEach(op => {
  384. if (typeof op.insert === 'string') {
  385. const lines = op.insert.split('\n');
  386. lines.forEach((line, index) => {
  387. if (index) split.push({
  388. insert: '\n',
  389. attributes: op.attributes
  390. });
  391. if (line) split.push({
  392. insert: line,
  393. attributes: op.attributes
  394. });
  395. });
  396. } else {
  397. split.push(op);
  398. }
  399. });
  400. return split;
  401. }
  402. export default Editor;
  403. //# sourceMappingURL=editor.js.map