UNPKG

15.6 kBJavaScriptView Raw
1import './polyfill';
2import Delta from 'quill-delta';
3import Editor from './editor';
4import Emitter from './emitter';
5import Module from './module';
6import Parchment from 'parchment';
7import Selection, { Range } from './selection';
8import extend from 'extend';
9import logger from './logger';
10import Theme from './theme';
11
12let debug = logger('quill');
13
14
15class Quill {
16 static debug(limit) {
17 if (limit === true) {
18 limit = 'log';
19 }
20 logger.level(limit);
21 }
22
23 static find(node) {
24 return node.__quill || Parchment.find(node);
25 }
26
27 static import(name) {
28 if (this.imports[name] == null) {
29 debug.error(`Cannot import ${name}. Are you sure it was registered?`);
30 }
31 return this.imports[name];
32 }
33
34 static register(path, target, overwrite = false) {
35 if (typeof path !== 'string') {
36 let name = path.attrName || path.blotName;
37 if (typeof name === 'string') {
38 // register(Blot | Attributor, overwrite)
39 this.register('formats/' + name, path, target);
40 } else {
41 Object.keys(path).forEach((key) => {
42 this.register(key, path[key], target);
43 });
44 }
45 } else {
46 if (this.imports[path] != null && !overwrite) {
47 debug.warn(`Overwriting ${path} with`, target);
48 }
49 this.imports[path] = target;
50 if ((path.startsWith('blots/') || path.startsWith('formats/')) &&
51 target.blotName !== 'abstract') {
52 Parchment.register(target);
53 } else if (path.startsWith('modules') && typeof target.register === 'function') {
54 target.register();
55 }
56 }
57 }
58
59 constructor(container, options = {}) {
60 this.options = expandConfig(container, options);
61 this.container = this.options.container;
62 if (this.container == null) {
63 return debug.error('Invalid Quill container', container);
64 }
65 if (this.options.debug) {
66 Quill.debug(this.options.debug);
67 }
68 let html = this.container.innerHTML.trim();
69 this.container.classList.add('ql-container');
70 this.container.innerHTML = '';
71 this.container.__quill = this;
72 this.root = this.addContainer('ql-editor');
73 this.root.classList.add('ql-blank');
74 this.root.setAttribute('data-gramm', false);
75 this.scrollingContainer = this.options.scrollingContainer || this.root;
76 this.emitter = new Emitter();
77 this.scroll = Parchment.create(this.root, {
78 emitter: this.emitter,
79 whitelist: this.options.formats
80 });
81 this.editor = new Editor(this.scroll);
82 this.selection = new Selection(this.scroll, this.emitter);
83 this.theme = new this.options.theme(this, this.options);
84 this.keyboard = this.theme.addModule('keyboard');
85 this.clipboard = this.theme.addModule('clipboard');
86 this.history = this.theme.addModule('history');
87 this.theme.init();
88 this.emitter.on(Emitter.events.EDITOR_CHANGE, (type) => {
89 if (type === Emitter.events.TEXT_CHANGE) {
90 this.root.classList.toggle('ql-blank', this.editor.isBlank());
91 }
92 });
93 this.emitter.on(Emitter.events.SCROLL_UPDATE, (source, mutations) => {
94 let range = this.selection.lastRange;
95 let index = range && range.length === 0 ? range.index : undefined;
96 modify.call(this, () => {
97 return this.editor.update(null, mutations, index);
98 }, source);
99 });
100 let contents = this.clipboard.convert(`<div class='ql-editor' style="white-space: normal;">${html}<p><br></p></div>`);
101 this.setContents(contents);
102 this.history.clear();
103 if (this.options.placeholder) {
104 this.root.setAttribute('data-placeholder', this.options.placeholder);
105 }
106 if (this.options.readOnly) {
107 this.disable();
108 }
109 }
110
111 addContainer(container, refNode = null) {
112 if (typeof container === 'string') {
113 let className = container;
114 container = document.createElement('div');
115 container.classList.add(className);
116 }
117 this.container.insertBefore(container, refNode);
118 return container;
119 }
120
121 blur() {
122 this.selection.setRange(null);
123 }
124
125 deleteText(index, length, source) {
126 [index, length, , source] = overload(index, length, source);
127 return modify.call(this, () => {
128 return this.editor.deleteText(index, length);
129 }, source, index, -1*length);
130 }
131
132 disable() {
133 this.enable(false);
134 }
135
136 enable(enabled = true) {
137 this.scroll.enable(enabled);
138 this.container.classList.toggle('ql-disabled', !enabled);
139 }
140
141 focus() {
142 let scrollTop = this.scrollingContainer.scrollTop;
143 this.selection.focus();
144 this.scrollingContainer.scrollTop = scrollTop;
145 this.scrollIntoView();
146 }
147
148 format(name, value, source = Emitter.sources.API) {
149 return modify.call(this, () => {
150 let range = this.getSelection(true);
151 let change = new Delta();
152 if (range == null) {
153 return change;
154 } else if (Parchment.query(name, Parchment.Scope.BLOCK)) {
155 change = this.editor.formatLine(range.index, range.length, { [name]: value });
156 } else if (range.length === 0) {
157 this.selection.format(name, value);
158 return change;
159 } else {
160 change = this.editor.formatText(range.index, range.length, { [name]: value });
161 }
162 this.setSelection(range, Emitter.sources.SILENT);
163 return change;
164 }, source);
165 }
166
167 formatLine(index, length, name, value, source) {
168 let formats;
169 [index, length, formats, source] = overload(index, length, name, value, source);
170 return modify.call(this, () => {
171 return this.editor.formatLine(index, length, formats);
172 }, source, index, 0);
173 }
174
175 formatText(index, length, name, value, source) {
176 let formats;
177 [index, length, formats, source] = overload(index, length, name, value, source);
178 return modify.call(this, () => {
179 return this.editor.formatText(index, length, formats);
180 }, source, index, 0);
181 }
182
183 getBounds(index, length = 0) {
184 let bounds;
185 if (typeof index === 'number') {
186 bounds = this.selection.getBounds(index, length);
187 } else {
188 bounds = this.selection.getBounds(index.index, index.length);
189 }
190 let containerBounds = this.container.getBoundingClientRect();
191 return {
192 bottom: bounds.bottom - containerBounds.top,
193 height: bounds.height,
194 left: bounds.left - containerBounds.left,
195 right: bounds.right - containerBounds.left,
196 top: bounds.top - containerBounds.top,
197 width: bounds.width
198 };
199 }
200
201 getContents(index = 0, length = this.getLength() - index) {
202 [index, length] = overload(index, length);
203 return this.editor.getContents(index, length);
204 }
205
206 getFormat(index = this.getSelection(true), length = 0) {
207 if (typeof index === 'number') {
208 return this.editor.getFormat(index, length);
209 } else {
210 return this.editor.getFormat(index.index, index.length);
211 }
212 }
213
214 getIndex(blot) {
215 return blot.offset(this.scroll);
216 }
217
218 getLength() {
219 return this.scroll.length();
220 }
221
222 getLeaf(index) {
223 return this.scroll.leaf(index);
224 }
225
226 getLine(index) {
227 return this.scroll.line(index);
228 }
229
230 getLines(index = 0, length = Number.MAX_VALUE) {
231 if (typeof index !== 'number') {
232 return this.scroll.lines(index.index, index.length);
233 } else {
234 return this.scroll.lines(index, length);
235 }
236 }
237
238 getModule(name) {
239 return this.theme.modules[name];
240 }
241
242 getSelection(focus = false) {
243 if (focus) this.focus();
244 this.update(); // Make sure we access getRange with editor in consistent state
245 return this.selection.getRange()[0];
246 }
247
248 getText(index = 0, length = this.getLength() - index) {
249 [index, length] = overload(index, length);
250 return this.editor.getText(index, length);
251 }
252
253 hasFocus() {
254 return this.selection.hasFocus();
255 }
256
257 insertEmbed(index, embed, value, source = Quill.sources.API) {
258 return modify.call(this, () => {
259 return this.editor.insertEmbed(index, embed, value);
260 }, source, index);
261 }
262
263 insertText(index, text, name, value, source) {
264 let formats;
265 [index, , formats, source] = overload(index, 0, name, value, source);
266 return modify.call(this, () => {
267 return this.editor.insertText(index, text, formats);
268 }, source, index, text.length);
269 }
270
271 isEnabled() {
272 return !this.container.classList.contains('ql-disabled');
273 }
274
275 off() {
276 return this.emitter.off.apply(this.emitter, arguments);
277 }
278
279 on() {
280 return this.emitter.on.apply(this.emitter, arguments);
281 }
282
283 once() {
284 return this.emitter.once.apply(this.emitter, arguments);
285 }
286
287 pasteHTML(index, html, source) {
288 this.clipboard.dangerouslyPasteHTML(index, html, source);
289 }
290
291 removeFormat(index, length, source) {
292 [index, length, , source] = overload(index, length, source);
293 return modify.call(this, () => {
294 return this.editor.removeFormat(index, length);
295 }, source, index);
296 }
297
298 scrollIntoView() {
299 this.selection.scrollIntoView(this.scrollingContainer);
300 }
301
302 setContents(delta, source = Emitter.sources.API) {
303 return modify.call(this, () => {
304 delta = new Delta(delta);
305 let length = this.getLength();
306 let deleted = this.editor.deleteText(0, length);
307 let applied = this.editor.applyDelta(delta);
308 let lastOp = applied.ops[applied.ops.length - 1];
309 if (lastOp != null && typeof(lastOp.insert) === 'string' && lastOp.insert[lastOp.insert.length-1] === '\n') {
310 this.editor.deleteText(this.getLength() - 1, 1);
311 applied.delete(1);
312 }
313 let ret = deleted.compose(applied);
314 return ret;
315 }, source);
316 }
317
318 setSelection(index, length, source) {
319 if (index == null) {
320 this.selection.setRange(null, length || Quill.sources.API);
321 } else {
322 [index, length, , source] = overload(index, length, source);
323 this.selection.setRange(new Range(index, length), source);
324 if (source !== Emitter.sources.SILENT) {
325 this.selection.scrollIntoView(this.scrollingContainer);
326 }
327 }
328 }
329
330 setText(text, source = Emitter.sources.API) {
331 let delta = new Delta().insert(text);
332 return this.setContents(delta, source);
333 }
334
335 update(source = Emitter.sources.USER) {
336 let change = this.scroll.update(source); // Will update selection before selection.update() does if text changes
337 this.selection.update(source);
338 return change;
339 }
340
341 updateContents(delta, source = Emitter.sources.API) {
342 return modify.call(this, () => {
343 delta = new Delta(delta);
344 return this.editor.applyDelta(delta, source);
345 }, source, true);
346 }
347}
348Quill.DEFAULTS = {
349 bounds: null,
350 formats: null,
351 modules: {},
352 placeholder: '',
353 readOnly: false,
354 scrollingContainer: null,
355 strict: true,
356 theme: 'default'
357};
358Quill.events = Emitter.events;
359Quill.sources = Emitter.sources;
360// eslint-disable-next-line no-undef
361Quill.version = typeof(QUILL_VERSION) === 'undefined' ? 'dev' : QUILL_VERSION;
362
363Quill.imports = {
364 'delta' : Delta,
365 'parchment' : Parchment,
366 'core/module' : Module,
367 'core/theme' : Theme
368};
369
370
371function expandConfig(container, userConfig) {
372 userConfig = extend(true, {
373 container: container,
374 modules: {
375 clipboard: true,
376 keyboard: true,
377 history: true
378 }
379 }, userConfig);
380 if (!userConfig.theme || userConfig.theme === Quill.DEFAULTS.theme) {
381 userConfig.theme = Theme;
382 } else {
383 userConfig.theme = Quill.import(`themes/${userConfig.theme}`);
384 if (userConfig.theme == null) {
385 throw new Error(`Invalid theme ${userConfig.theme}. Did you register it?`);
386 }
387 }
388 let themeConfig = extend(true, {}, userConfig.theme.DEFAULTS);
389 [themeConfig, userConfig].forEach(function(config) {
390 config.modules = config.modules || {};
391 Object.keys(config.modules).forEach(function(module) {
392 if (config.modules[module] === true) {
393 config.modules[module] = {};
394 }
395 });
396 });
397 let moduleNames = Object.keys(themeConfig.modules).concat(Object.keys(userConfig.modules));
398 let moduleConfig = moduleNames.reduce(function(config, name) {
399 let moduleClass = Quill.import(`modules/${name}`);
400 if (moduleClass == null) {
401 debug.error(`Cannot load ${name} module. Are you sure you registered it?`);
402 } else {
403 config[name] = moduleClass.DEFAULTS || {};
404 }
405 return config;
406 }, {});
407 // Special case toolbar shorthand
408 if (userConfig.modules != null && userConfig.modules.toolbar &&
409 userConfig.modules.toolbar.constructor !== Object) {
410 userConfig.modules.toolbar = {
411 container: userConfig.modules.toolbar
412 };
413 }
414 userConfig = extend(true, {}, Quill.DEFAULTS, { modules: moduleConfig }, themeConfig, userConfig);
415 ['bounds', 'container', 'scrollingContainer'].forEach(function(key) {
416 if (typeof userConfig[key] === 'string') {
417 userConfig[key] = document.querySelector(userConfig[key]);
418 }
419 });
420 userConfig.modules = Object.keys(userConfig.modules).reduce(function(config, name) {
421 if (userConfig.modules[name]) {
422 config[name] = userConfig.modules[name];
423 }
424 return config;
425 }, {});
426 return userConfig;
427}
428
429// Handle selection preservation and TEXT_CHANGE emission
430// common to modification APIs
431function modify(modifier, source, index, shift) {
432 if (this.options.strict && !this.isEnabled() && source === Emitter.sources.USER) {
433 return new Delta();
434 }
435 let range = index == null ? null : this.getSelection();
436 let oldDelta = this.editor.delta;
437 let change = modifier();
438 if (range != null) {
439 if (index === true) index = range.index;
440 if (shift == null) {
441 range = shiftRange(range, change, source);
442 } else if (shift !== 0) {
443 range = shiftRange(range, index, shift, source);
444 }
445 this.setSelection(range, Emitter.sources.SILENT);
446 }
447 if (change.length() > 0) {
448 let args = [Emitter.events.TEXT_CHANGE, change, oldDelta, source];
449 this.emitter.emit(Emitter.events.EDITOR_CHANGE, ...args);
450 if (source !== Emitter.sources.SILENT) {
451 this.emitter.emit(...args);
452 }
453 }
454 return change;
455}
456
457function overload(index, length, name, value, source) {
458 let formats = {};
459 if (typeof index.index === 'number' && typeof index.length === 'number') {
460 // Allow for throwaway end (used by insertText/insertEmbed)
461 if (typeof length !== 'number') {
462 source = value, value = name, name = length, length = index.length, index = index.index;
463 } else {
464 length = index.length, index = index.index;
465 }
466 } else if (typeof length !== 'number') {
467 source = value, value = name, name = length, length = 0;
468 }
469 // Handle format being object, two format name/value strings or excluded
470 if (typeof name === 'object') {
471 formats = name;
472 source = value;
473 } else if (typeof name === 'string') {
474 if (value != null) {
475 formats[name] = value;
476 } else {
477 source = name;
478 }
479 }
480 // Handle optional source
481 source = source || Emitter.sources.API;
482 return [index, length, formats, source];
483}
484
485function shiftRange(range, index, length, source) {
486 if (range == null) return null;
487 let start, end;
488 if (index instanceof Delta) {
489 [start, end] = [range.index, range.index + range.length].map(function(pos) {
490 return index.transformPosition(pos, source !== Emitter.sources.USER);
491 });
492 } else {
493 [start, end] = [range.index, range.index + range.length].map(function(pos) {
494 if (pos < index || (pos === index && source === Emitter.sources.USER)) return pos;
495 if (length >= 0) {
496 return pos + length;
497 } else {
498 return Math.max(index, pos + length);
499 }
500 });
501 }
502 return new Range(start, end - start);
503}
504
505
506export { expandConfig, overload, Quill as default };