UNPKG

9.08 kBJavaScriptView Raw
1import Delta from 'quill-delta';
2import DeltaOp from 'quill-delta/lib/op';
3import Parchment from 'parchment';
4import CodeBlock from '../formats/code';
5import CursorBlot from '../blots/cursor';
6import Block, { bubbleFormats } from '../blots/block';
7import Break from '../blots/break';
8import clone from 'clone';
9import equal from 'deep-equal';
10import extend from 'extend';
11
12
13const ASCII = /^[ -~]*$/;
14
15
16class 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]; // There should only be one key
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 // Optimization for character changes
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
226function 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
242function 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
267export default Editor;