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