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];
|
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 |
|
230 | mutations[0].target.data.match(ASCII) && this.scroll.find(mutations[0].target)) {
|
231 |
|
232 | const textBlot = this.scroll.find(mutations[0].target);
|
233 | const formats = bubbleFormats(textBlot);
|
234 | const index = textBlot.offset(this.scroll);
|
235 |
|
236 | const oldValue = mutations[0].oldValue.replace(CursorBlot.CONTENTS, '');
|
237 | const oldText = new Delta().insert(oldValue);
|
238 |
|
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 |
|
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 |
|
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 |
|
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 |
|
\ | No newline at end of file |