UNPKG

14.9 kBJavaScriptView Raw
1import { cloneDeep, isEqual, merge } from 'lodash-es';
2import { LeafBlot, EmbedBlot, Scope, ParentBlot } from 'parchment';
3import Delta, { AttributeMap, Op } from 'quill-delta';
4import Block, { BlockEmbed, bubbleFormats } from '../blots/block.js';
5import Break from '../blots/break.js';
6import CursorBlot from '../blots/cursor.js';
7import TextBlot, { escapeText } from '../blots/text.js';
8import { Range } from './selection.js';
9const ASCII = /^[ -~]*$/;
10class 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}
261function 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}
291function 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}
335function 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}
354function 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}
365function 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}
374function shiftRange(_ref, amount) {
375 let {
376 index,
377 length
378 } = _ref;
379 return new Range(index + amount, length);
380}
381function 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}
402export default Editor;
403//# sourceMappingURL=editor.js.map
\No newline at end of file