1 | import './polyfill';
|
2 | import Delta from 'quill-delta';
|
3 | import Editor from './editor';
|
4 | import Emitter from './emitter';
|
5 | import Module from './module';
|
6 | import Parchment from 'parchment';
|
7 | import Selection, { Range } from './selection';
|
8 | import extend from 'extend';
|
9 | import logger from './logger';
|
10 | import Theme from './theme';
|
11 |
|
12 | let debug = logger('quill');
|
13 |
|
14 |
|
15 | class 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 |
|
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();
|
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);
|
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 | }
|
348 | Quill.DEFAULTS = {
|
349 | bounds: null,
|
350 | formats: null,
|
351 | modules: {},
|
352 | placeholder: '',
|
353 | readOnly: false,
|
354 | scrollingContainer: null,
|
355 | strict: true,
|
356 | theme: 'default'
|
357 | };
|
358 | Quill.events = Emitter.events;
|
359 | Quill.sources = Emitter.sources;
|
360 |
|
361 | Quill.version = typeof(QUILL_VERSION) === 'undefined' ? 'dev' : QUILL_VERSION;
|
362 |
|
363 | Quill.imports = {
|
364 | 'delta' : Delta,
|
365 | 'parchment' : Parchment,
|
366 | 'core/module' : Module,
|
367 | 'core/theme' : Theme
|
368 | };
|
369 |
|
370 |
|
371 | function 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 |
|
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 |
|
430 |
|
431 | function 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 |
|
457 | function overload(index, length, name, value, source) {
|
458 | let formats = {};
|
459 | if (typeof index.index === 'number' && typeof index.length === 'number') {
|
460 |
|
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 |
|
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 |
|
481 | source = source || Emitter.sources.API;
|
482 | return [index, length, formats, source];
|
483 | }
|
484 |
|
485 | function 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 |
|
506 | export { expandConfig, overload, Quill as default };
|