import './polyfill';
import Delta from 'quill-delta';
import Editor from './editor';
import Emitter from './emitter';
import Module from './module';
import Parchment from 'parchment';
import Selection, { Range } from './selection';
import extend from 'extend';
import logger from './logger';
import Theme from './theme';
let debug = logger('quill');
class Quill {
static debug(limit) {
if (limit === true) {
limit = 'log';
}
logger.level(limit);
}
static find(node) {
return node.__quill || Parchment.find(node);
}
static import(name) {
if (this.imports[name] == null) {
debug.error(`Cannot import ${name}. Are you sure it was registered?`);
}
return this.imports[name];
}
static register(path, target, overwrite = false) {
if (typeof path !== 'string') {
let name = path.attrName || path.blotName;
if (typeof name === 'string') {
// register(Blot | Attributor, overwrite)
this.register('formats/' + name, path, target);
} else {
Object.keys(path).forEach((key) => {
this.register(key, path[key], target);
});
}
} else {
if (this.imports[path] != null && !overwrite) {
debug.warn(`Overwriting ${path} with`, target);
}
this.imports[path] = target;
if ((path.startsWith('blots/') || path.startsWith('formats/')) &&
target.blotName !== 'abstract') {
Parchment.register(target);
} else if (path.startsWith('modules') && typeof target.register === 'function') {
target.register();
}
}
}
constructor(container, options = {}) {
this.options = expandConfig(container, options);
this.container = this.options.container;
if (this.container == null) {
return debug.error('Invalid Quill container', container);
}
if (this.options.debug) {
Quill.debug(this.options.debug);
}
let html = this.container.innerHTML.trim();
this.container.classList.add('ql-container');
this.container.innerHTML = '';
this.container.__quill = this;
this.root = this.addContainer('ql-editor');
this.root.classList.add('ql-blank');
this.root.setAttribute('data-gramm', false);
this.scrollingContainer = this.options.scrollingContainer || this.root;
this.emitter = new Emitter();
this.scroll = Parchment.create(this.root, {
emitter: this.emitter,
whitelist: this.options.formats
});
this.editor = new Editor(this.scroll);
this.selection = new Selection(this.scroll, this.emitter);
this.theme = new this.options.theme(this, this.options);
this.keyboard = this.theme.addModule('keyboard');
this.clipboard = this.theme.addModule('clipboard');
this.history = this.theme.addModule('history');
this.theme.init();
this.emitter.on(Emitter.events.EDITOR_CHANGE, (type) => {
if (type === Emitter.events.TEXT_CHANGE) {
this.root.classList.toggle('ql-blank', this.editor.isBlank());
}
});
this.emitter.on(Emitter.events.SCROLL_UPDATE, (source, mutations) => {
let range = this.selection.lastRange;
let index = range && range.length === 0 ? range.index : undefined;
modify.call(this, () => {
return this.editor.update(null, mutations, index);
}, source);
});
let contents = this.clipboard.convert(`
`);
this.setContents(contents);
this.history.clear();
if (this.options.placeholder) {
this.root.setAttribute('data-placeholder', this.options.placeholder);
}
if (this.options.readOnly) {
this.disable();
}
}
addContainer(container, refNode = null) {
if (typeof container === 'string') {
let className = container;
container = document.createElement('div');
container.classList.add(className);
}
this.container.insertBefore(container, refNode);
return container;
}
blur() {
this.selection.setRange(null);
}
deleteText(index, length, source) {
[index, length, , source] = overload(index, length, source);
return modify.call(this, () => {
return this.editor.deleteText(index, length);
}, source, index, -1*length);
}
disable() {
this.enable(false);
}
enable(enabled = true) {
this.scroll.enable(enabled);
this.container.classList.toggle('ql-disabled', !enabled);
}
focus() {
let scrollTop = this.scrollingContainer.scrollTop;
this.selection.focus();
this.scrollingContainer.scrollTop = scrollTop;
this.scrollIntoView();
}
format(name, value, source = Emitter.sources.API) {
return modify.call(this, () => {
let range = this.getSelection(true);
let change = new Delta();
if (range == null) {
return change;
} else if (Parchment.query(name, Parchment.Scope.BLOCK)) {
change = this.editor.formatLine(range.index, range.length, { [name]: value });
} else if (range.length === 0) {
this.selection.format(name, value);
return change;
} else {
change = this.editor.formatText(range.index, range.length, { [name]: value });
}
this.setSelection(range, Emitter.sources.SILENT);
return change;
}, source);
}
formatLine(index, length, name, value, source) {
let formats;
[index, length, formats, source] = overload(index, length, name, value, source);
return modify.call(this, () => {
return this.editor.formatLine(index, length, formats);
}, source, index, 0);
}
formatText(index, length, name, value, source) {
let formats;
[index, length, formats, source] = overload(index, length, name, value, source);
return modify.call(this, () => {
return this.editor.formatText(index, length, formats);
}, source, index, 0);
}
getBounds(index, length = 0) {
let bounds;
if (typeof index === 'number') {
bounds = this.selection.getBounds(index, length);
} else {
bounds = this.selection.getBounds(index.index, index.length);
}
let containerBounds = this.container.getBoundingClientRect();
return {
bottom: bounds.bottom - containerBounds.top,
height: bounds.height,
left: bounds.left - containerBounds.left,
right: bounds.right - containerBounds.left,
top: bounds.top - containerBounds.top,
width: bounds.width
};
}
getContents(index = 0, length = this.getLength() - index) {
[index, length] = overload(index, length);
return this.editor.getContents(index, length);
}
getFormat(index = this.getSelection(true), length = 0) {
if (typeof index === 'number') {
return this.editor.getFormat(index, length);
} else {
return this.editor.getFormat(index.index, index.length);
}
}
getIndex(blot) {
return blot.offset(this.scroll);
}
getLength() {
return this.scroll.length();
}
getLeaf(index) {
return this.scroll.leaf(index);
}
getLine(index) {
return this.scroll.line(index);
}
getLines(index = 0, length = Number.MAX_VALUE) {
if (typeof index !== 'number') {
return this.scroll.lines(index.index, index.length);
} else {
return this.scroll.lines(index, length);
}
}
getModule(name) {
return this.theme.modules[name];
}
getSelection(focus = false) {
if (focus) this.focus();
this.update(); // Make sure we access getRange with editor in consistent state
return this.selection.getRange()[0];
}
getText(index = 0, length = this.getLength() - index) {
[index, length] = overload(index, length);
return this.editor.getText(index, length);
}
hasFocus() {
return this.selection.hasFocus();
}
insertEmbed(index, embed, value, source = Quill.sources.API) {
return modify.call(this, () => {
return this.editor.insertEmbed(index, embed, value);
}, source, index);
}
insertText(index, text, name, value, source) {
let formats;
[index, , formats, source] = overload(index, 0, name, value, source);
return modify.call(this, () => {
return this.editor.insertText(index, text, formats);
}, source, index, text.length);
}
isEnabled() {
return !this.container.classList.contains('ql-disabled');
}
off() {
return this.emitter.off.apply(this.emitter, arguments);
}
on() {
return this.emitter.on.apply(this.emitter, arguments);
}
once() {
return this.emitter.once.apply(this.emitter, arguments);
}
pasteHTML(index, html, source) {
this.clipboard.dangerouslyPasteHTML(index, html, source);
}
removeFormat(index, length, source) {
[index, length, , source] = overload(index, length, source);
return modify.call(this, () => {
return this.editor.removeFormat(index, length);
}, source, index);
}
scrollIntoView() {
this.selection.scrollIntoView(this.scrollingContainer);
}
setContents(delta, source = Emitter.sources.API) {
return modify.call(this, () => {
delta = new Delta(delta);
let length = this.getLength();
let deleted = this.editor.deleteText(0, length);
let applied = this.editor.applyDelta(delta);
let lastOp = applied.ops[applied.ops.length - 1];
if (lastOp != null && typeof(lastOp.insert) === 'string' && lastOp.insert[lastOp.insert.length-1] === '\n') {
this.editor.deleteText(this.getLength() - 1, 1);
applied.delete(1);
}
let ret = deleted.compose(applied);
return ret;
}, source);
}
setSelection(index, length, source) {
if (index == null) {
this.selection.setRange(null, length || Quill.sources.API);
} else {
[index, length, , source] = overload(index, length, source);
this.selection.setRange(new Range(index, length), source);
if (source !== Emitter.sources.SILENT) {
this.selection.scrollIntoView(this.scrollingContainer);
}
}
}
setText(text, source = Emitter.sources.API) {
let delta = new Delta().insert(text);
return this.setContents(delta, source);
}
update(source = Emitter.sources.USER) {
let change = this.scroll.update(source); // Will update selection before selection.update() does if text changes
this.selection.update(source);
return change;
}
updateContents(delta, source = Emitter.sources.API) {
return modify.call(this, () => {
delta = new Delta(delta);
return this.editor.applyDelta(delta, source);
}, source, true);
}
}
Quill.DEFAULTS = {
bounds: null,
formats: null,
modules: {},
placeholder: '',
readOnly: false,
scrollingContainer: null,
strict: true,
theme: 'default'
};
Quill.events = Emitter.events;
Quill.sources = Emitter.sources;
// eslint-disable-next-line no-undef
Quill.version = typeof(QUILL_VERSION) === 'undefined' ? 'dev' : QUILL_VERSION;
Quill.imports = {
'delta' : Delta,
'parchment' : Parchment,
'core/module' : Module,
'core/theme' : Theme
};
function expandConfig(container, userConfig) {
userConfig = extend(true, {
container: container,
modules: {
clipboard: true,
keyboard: true,
history: true
}
}, userConfig);
if (!userConfig.theme || userConfig.theme === Quill.DEFAULTS.theme) {
userConfig.theme = Theme;
} else {
userConfig.theme = Quill.import(`themes/${userConfig.theme}`);
if (userConfig.theme == null) {
throw new Error(`Invalid theme ${userConfig.theme}. Did you register it?`);
}
}
let themeConfig = extend(true, {}, userConfig.theme.DEFAULTS);
[themeConfig, userConfig].forEach(function(config) {
config.modules = config.modules || {};
Object.keys(config.modules).forEach(function(module) {
if (config.modules[module] === true) {
config.modules[module] = {};
}
});
});
let moduleNames = Object.keys(themeConfig.modules).concat(Object.keys(userConfig.modules));
let moduleConfig = moduleNames.reduce(function(config, name) {
let moduleClass = Quill.import(`modules/${name}`);
if (moduleClass == null) {
debug.error(`Cannot load ${name} module. Are you sure you registered it?`);
} else {
config[name] = moduleClass.DEFAULTS || {};
}
return config;
}, {});
// Special case toolbar shorthand
if (userConfig.modules != null && userConfig.modules.toolbar &&
userConfig.modules.toolbar.constructor !== Object) {
userConfig.modules.toolbar = {
container: userConfig.modules.toolbar
};
}
userConfig = extend(true, {}, Quill.DEFAULTS, { modules: moduleConfig }, themeConfig, userConfig);
['bounds', 'container', 'scrollingContainer'].forEach(function(key) {
if (typeof userConfig[key] === 'string') {
userConfig[key] = document.querySelector(userConfig[key]);
}
});
userConfig.modules = Object.keys(userConfig.modules).reduce(function(config, name) {
if (userConfig.modules[name]) {
config[name] = userConfig.modules[name];
}
return config;
}, {});
return userConfig;
}
// Handle selection preservation and TEXT_CHANGE emission
// common to modification APIs
function modify(modifier, source, index, shift) {
if (this.options.strict && !this.isEnabled() && source === Emitter.sources.USER) {
return new Delta();
}
let range = index == null ? null : this.getSelection();
let oldDelta = this.editor.delta;
let change = modifier();
if (range != null) {
if (index === true) index = range.index;
if (shift == null) {
range = shiftRange(range, change, source);
} else if (shift !== 0) {
range = shiftRange(range, index, shift, source);
}
this.setSelection(range, Emitter.sources.SILENT);
}
if (change.length() > 0) {
let args = [Emitter.events.TEXT_CHANGE, change, oldDelta, source];
this.emitter.emit(Emitter.events.EDITOR_CHANGE, ...args);
if (source !== Emitter.sources.SILENT) {
this.emitter.emit(...args);
}
}
return change;
}
function overload(index, length, name, value, source) {
let formats = {};
if (typeof index.index === 'number' && typeof index.length === 'number') {
// Allow for throwaway end (used by insertText/insertEmbed)
if (typeof length !== 'number') {
source = value, value = name, name = length, length = index.length, index = index.index;
} else {
length = index.length, index = index.index;
}
} else if (typeof length !== 'number') {
source = value, value = name, name = length, length = 0;
}
// Handle format being object, two format name/value strings or excluded
if (typeof name === 'object') {
formats = name;
source = value;
} else if (typeof name === 'string') {
if (value != null) {
formats[name] = value;
} else {
source = name;
}
}
// Handle optional source
source = source || Emitter.sources.API;
return [index, length, formats, source];
}
function shiftRange(range, index, length, source) {
if (range == null) return null;
let start, end;
if (index instanceof Delta) {
[start, end] = [range.index, range.index + range.length].map(function(pos) {
return index.transformPosition(pos, source !== Emitter.sources.USER);
});
} else {
[start, end] = [range.index, range.index + range.length].map(function(pos) {
if (pos < index || (pos === index && source === Emitter.sources.USER)) return pos;
if (length >= 0) {
return pos + length;
} else {
return Math.max(index, pos + length);
}
});
}
return new Range(start, end - start);
}
export { expandConfig, overload, Quill as default };