UNPKG

11.8 kBJavaScriptView Raw
1import { ContainerBlot, LeafBlot, Scope, ScrollBlot } from 'parchment';
2import Delta, { AttributeMap, Op } from 'quill-delta';
3import Emitter from '../core/emitter.js';
4import Block, { BlockEmbed, bubbleFormats } from './block.js';
5import Break from './break.js';
6import Container from './container.js';
7function isLine(blot) {
8 return blot instanceof Block || blot instanceof BlockEmbed;
9}
10function isUpdatable(blot) {
11 return typeof blot.updateContent === 'function';
12}
13class Scroll extends ScrollBlot {
14 static blotName = 'scroll';
15 static className = 'ql-editor';
16 static tagName = 'DIV';
17 static defaultChild = Block;
18 static allowedChildren = [Block, BlockEmbed, Container];
19 constructor(registry, domNode, _ref) {
20 let {
21 emitter
22 } = _ref;
23 super(registry, domNode);
24 this.emitter = emitter;
25 this.batch = false;
26 this.optimize();
27 this.enable();
28 this.domNode.addEventListener('dragstart', e => this.handleDragStart(e));
29 }
30 batchStart() {
31 if (!Array.isArray(this.batch)) {
32 this.batch = [];
33 }
34 }
35 batchEnd() {
36 if (!this.batch) return;
37 const mutations = this.batch;
38 this.batch = false;
39 this.update(mutations);
40 }
41 emitMount(blot) {
42 this.emitter.emit(Emitter.events.SCROLL_BLOT_MOUNT, blot);
43 }
44 emitUnmount(blot) {
45 this.emitter.emit(Emitter.events.SCROLL_BLOT_UNMOUNT, blot);
46 }
47 emitEmbedUpdate(blot, change) {
48 this.emitter.emit(Emitter.events.SCROLL_EMBED_UPDATE, blot, change);
49 }
50 deleteAt(index, length) {
51 const [first, offset] = this.line(index);
52 const [last] = this.line(index + length);
53 super.deleteAt(index, length);
54 if (last != null && first !== last && offset > 0) {
55 if (first instanceof BlockEmbed || last instanceof BlockEmbed) {
56 this.optimize();
57 return;
58 }
59 const ref = last.children.head instanceof Break ? null : last.children.head;
60 // @ts-expect-error
61 first.moveChildren(last, ref);
62 // @ts-expect-error
63 first.remove();
64 }
65 this.optimize();
66 }
67 enable() {
68 let enabled = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
69 this.domNode.setAttribute('contenteditable', enabled ? 'true' : 'false');
70 }
71 formatAt(index, length, format, value) {
72 super.formatAt(index, length, format, value);
73 this.optimize();
74 }
75 insertAt(index, value, def) {
76 if (index >= this.length()) {
77 if (def == null || this.scroll.query(value, Scope.BLOCK) == null) {
78 const blot = this.scroll.create(this.statics.defaultChild.blotName);
79 this.appendChild(blot);
80 if (def == null && value.endsWith('\n')) {
81 blot.insertAt(0, value.slice(0, -1), def);
82 } else {
83 blot.insertAt(0, value, def);
84 }
85 } else {
86 const embed = this.scroll.create(value, def);
87 this.appendChild(embed);
88 }
89 } else {
90 super.insertAt(index, value, def);
91 }
92 this.optimize();
93 }
94 insertBefore(blot, ref) {
95 if (blot.statics.scope === Scope.INLINE_BLOT) {
96 const wrapper = this.scroll.create(this.statics.defaultChild.blotName);
97 wrapper.appendChild(blot);
98 super.insertBefore(wrapper, ref);
99 } else {
100 super.insertBefore(blot, ref);
101 }
102 }
103 insertContents(index, delta) {
104 const renderBlocks = this.deltaToRenderBlocks(delta.concat(new Delta().insert('\n')));
105 const last = renderBlocks.pop();
106 if (last == null) return;
107 this.batchStart();
108 const first = renderBlocks.shift();
109 if (first) {
110 const shouldInsertNewlineChar = first.type === 'block' && (first.delta.length() === 0 || !this.descendant(BlockEmbed, index)[0] && index < this.length());
111 const delta = first.type === 'block' ? first.delta : new Delta().insert({
112 [first.key]: first.value
113 });
114 insertInlineContents(this, index, delta);
115 const newlineCharLength = first.type === 'block' ? 1 : 0;
116 const lineEndIndex = index + delta.length() + newlineCharLength;
117 if (shouldInsertNewlineChar) {
118 this.insertAt(lineEndIndex - 1, '\n');
119 }
120 const formats = bubbleFormats(this.line(index)[0]);
121 const attributes = AttributeMap.diff(formats, first.attributes) || {};
122 Object.keys(attributes).forEach(name => {
123 this.formatAt(lineEndIndex - 1, 1, name, attributes[name]);
124 });
125 index = lineEndIndex;
126 }
127 let [refBlot, refBlotOffset] = this.children.find(index);
128 if (renderBlocks.length) {
129 if (refBlot) {
130 refBlot = refBlot.split(refBlotOffset);
131 refBlotOffset = 0;
132 }
133 renderBlocks.forEach(renderBlock => {
134 if (renderBlock.type === 'block') {
135 const block = this.createBlock(renderBlock.attributes, refBlot || undefined);
136 insertInlineContents(block, 0, renderBlock.delta);
137 } else {
138 const blockEmbed = this.create(renderBlock.key, renderBlock.value);
139 this.insertBefore(blockEmbed, refBlot || undefined);
140 Object.keys(renderBlock.attributes).forEach(name => {
141 blockEmbed.format(name, renderBlock.attributes[name]);
142 });
143 }
144 });
145 }
146 if (last.type === 'block' && last.delta.length()) {
147 const offset = refBlot ? refBlot.offset(refBlot.scroll) + refBlotOffset : this.length();
148 insertInlineContents(this, offset, last.delta);
149 }
150 this.batchEnd();
151 this.optimize();
152 }
153 isEnabled() {
154 return this.domNode.getAttribute('contenteditable') === 'true';
155 }
156 leaf(index) {
157 const last = this.path(index).pop();
158 if (!last) {
159 return [null, -1];
160 }
161 const [blot, offset] = last;
162 return blot instanceof LeafBlot ? [blot, offset] : [null, -1];
163 }
164 line(index) {
165 if (index === this.length()) {
166 return this.line(index - 1);
167 }
168 // @ts-expect-error TODO: make descendant() generic
169 return this.descendant(isLine, index);
170 }
171 lines() {
172 let index = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
173 let length = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Number.MAX_VALUE;
174 const getLines = (blot, blotIndex, blotLength) => {
175 let lines = [];
176 let lengthLeft = blotLength;
177 blot.children.forEachAt(blotIndex, blotLength, (child, childIndex, childLength) => {
178 if (isLine(child)) {
179 lines.push(child);
180 } else if (child instanceof ContainerBlot) {
181 lines = lines.concat(getLines(child, childIndex, lengthLeft));
182 }
183 lengthLeft -= childLength;
184 });
185 return lines;
186 };
187 return getLines(this, index, length);
188 }
189 optimize() {
190 let mutations = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
191 let context = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
192 if (this.batch) return;
193 super.optimize(mutations, context);
194 if (mutations.length > 0) {
195 this.emitter.emit(Emitter.events.SCROLL_OPTIMIZE, mutations, context);
196 }
197 }
198 path(index) {
199 return super.path(index).slice(1); // Exclude self
200 }
201 remove() {
202 // Never remove self
203 }
204 update(mutations) {
205 if (this.batch) {
206 if (Array.isArray(mutations)) {
207 this.batch = this.batch.concat(mutations);
208 }
209 return;
210 }
211 let source = Emitter.sources.USER;
212 if (typeof mutations === 'string') {
213 source = mutations;
214 }
215 if (!Array.isArray(mutations)) {
216 mutations = this.observer.takeRecords();
217 }
218 mutations = mutations.filter(_ref2 => {
219 let {
220 target
221 } = _ref2;
222 const blot = this.find(target, true);
223 return blot && !isUpdatable(blot);
224 });
225 if (mutations.length > 0) {
226 this.emitter.emit(Emitter.events.SCROLL_BEFORE_UPDATE, source, mutations);
227 }
228 super.update(mutations.concat([])); // pass copy
229 if (mutations.length > 0) {
230 this.emitter.emit(Emitter.events.SCROLL_UPDATE, source, mutations);
231 }
232 }
233 updateEmbedAt(index, key, change) {
234 // Currently it only supports top-level embeds (BlockEmbed).
235 // We can update `ParentBlot` in parchment to support inline embeds.
236 const [blot] = this.descendant(b => b instanceof BlockEmbed, index);
237 if (blot && blot.statics.blotName === key && isUpdatable(blot)) {
238 blot.updateContent(change);
239 }
240 }
241 handleDragStart(event) {
242 event.preventDefault();
243 }
244 deltaToRenderBlocks(delta) {
245 const renderBlocks = [];
246 let currentBlockDelta = new Delta();
247 delta.forEach(op => {
248 const insert = op?.insert;
249 if (!insert) return;
250 if (typeof insert === 'string') {
251 const splitted = insert.split('\n');
252 splitted.slice(0, -1).forEach(text => {
253 currentBlockDelta.insert(text, op.attributes);
254 renderBlocks.push({
255 type: 'block',
256 delta: currentBlockDelta,
257 attributes: op.attributes ?? {}
258 });
259 currentBlockDelta = new Delta();
260 });
261 const last = splitted[splitted.length - 1];
262 if (last) {
263 currentBlockDelta.insert(last, op.attributes);
264 }
265 } else {
266 const key = Object.keys(insert)[0];
267 if (!key) return;
268 if (this.query(key, Scope.INLINE)) {
269 currentBlockDelta.push(op);
270 } else {
271 if (currentBlockDelta.length()) {
272 renderBlocks.push({
273 type: 'block',
274 delta: currentBlockDelta,
275 attributes: {}
276 });
277 }
278 currentBlockDelta = new Delta();
279 renderBlocks.push({
280 type: 'blockEmbed',
281 key,
282 value: insert[key],
283 attributes: op.attributes ?? {}
284 });
285 }
286 }
287 });
288 if (currentBlockDelta.length()) {
289 renderBlocks.push({
290 type: 'block',
291 delta: currentBlockDelta,
292 attributes: {}
293 });
294 }
295 return renderBlocks;
296 }
297 createBlock(attributes, refBlot) {
298 let blotName;
299 const formats = {};
300 Object.entries(attributes).forEach(_ref3 => {
301 let [key, value] = _ref3;
302 const isBlockBlot = this.query(key, Scope.BLOCK & Scope.BLOT) != null;
303 if (isBlockBlot) {
304 blotName = key;
305 } else {
306 formats[key] = value;
307 }
308 });
309 const block = this.create(blotName || this.statics.defaultChild.blotName, blotName ? attributes[blotName] : undefined);
310 this.insertBefore(block, refBlot || undefined);
311 const length = block.length();
312 Object.entries(formats).forEach(_ref4 => {
313 let [key, value] = _ref4;
314 block.formatAt(0, length, key, value);
315 });
316 return block;
317 }
318}
319function insertInlineContents(parent, index, inlineContents) {
320 inlineContents.reduce((index, op) => {
321 const length = Op.length(op);
322 let attributes = op.attributes || {};
323 if (op.insert != null) {
324 if (typeof op.insert === 'string') {
325 const text = op.insert;
326 parent.insertAt(index, text);
327 const [leaf] = parent.descendant(LeafBlot, index);
328 const formats = bubbleFormats(leaf);
329 attributes = AttributeMap.diff(formats, attributes) || {};
330 } else if (typeof op.insert === 'object') {
331 const key = Object.keys(op.insert)[0]; // There should only be one key
332 if (key == null) return index;
333 parent.insertAt(index, key, op.insert[key]);
334 const isInlineEmbed = parent.scroll.query(key, Scope.INLINE) != null;
335 if (isInlineEmbed) {
336 const [leaf] = parent.descendant(LeafBlot, index);
337 const formats = bubbleFormats(leaf);
338 attributes = AttributeMap.diff(formats, attributes) || {};
339 }
340 }
341 }
342 Object.keys(attributes).forEach(key => {
343 parent.formatAt(index, length, key, attributes[key]);
344 });
345 return index + length;
346 }, index);
347}
348export default Scroll;
349//# sourceMappingURL=scroll.js.map
\No newline at end of file