import { merge } from 'lodash-es';
import * as Parchment from 'parchment';
import Delta from 'quill-delta';
import Editor from './editor.js';
import Emitter from './emitter.js';
import instances from './instances.js';
import logger from './logger.js';
import Module from './module.js';
import Selection, { Range } from './selection.js';
import Composition from './composition.js';
import Theme from './theme.js';
import scrollRectIntoView from './utils/scrollRectIntoView.js';
import createRegistryWithFormats from './utils/createRegistryWithFormats.js';
const debug = logger('quill');
const globalRegistry = new Parchment.Registry();
Parchment.ParentBlot.uiClass = 'ql-ui';
/**
* Options for initializing a Quill instance
*/
/**
* Similar to QuillOptions, but with all properties expanded to their default values,
* and all selectors resolved to HTMLElements.
*/
class Quill {
static DEFAULTS = {
bounds: null,
modules: {
clipboard: true,
keyboard: true,
history: true,
uploader: true
},
placeholder: '',
readOnly: false,
registry: globalRegistry,
theme: 'default'
};
static events = Emitter.events;
static sources = Emitter.sources;
static version = typeof "2.0.2" === 'undefined' ? 'dev' : "2.0.2";
static imports = {
delta: Delta,
parchment: Parchment,
'core/module': Module,
'core/theme': Theme
};
static debug(limit) {
if (limit === true) {
limit = 'log';
}
logger.level(limit);
}
static find(node) {
let bubble = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
return instances.get(node) || globalRegistry.find(node, bubble);
}
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() {
if (typeof (arguments.length <= 0 ? undefined : arguments[0]) !== 'string') {
const target = arguments.length <= 0 ? undefined : arguments[0];
const overwrite = !!(arguments.length <= 1 ? undefined : arguments[1]);
const name = 'attrName' in target ? target.attrName : target.blotName;
if (typeof name === 'string') {
// Shortcut for formats:
// register(Blot | Attributor, overwrite)
this.register(`formats/${name}`, target, overwrite);
} else {
Object.keys(target).forEach(key => {
this.register(key, target[key], overwrite);
});
}
} else {
const path = arguments.length <= 0 ? undefined : arguments[0];
const target = arguments.length <= 1 ? undefined : arguments[1];
const overwrite = !!(arguments.length <= 2 ? undefined : arguments[2]);
if (this.imports[path] != null && !overwrite) {
debug.warn(`Overwriting ${path} with`, target);
}
this.imports[path] = target;
if ((path.startsWith('blots/') || path.startsWith('formats/')) && target && typeof target !== 'boolean' && target.blotName !== 'abstract') {
globalRegistry.register(target);
}
if (typeof target.register === 'function') {
target.register(globalRegistry);
}
}
}
constructor(container) {
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
this.options = expandConfig(container, options);
this.container = this.options.container;
if (this.container == null) {
debug.error('Invalid Quill container', container);
return;
}
if (this.options.debug) {
Quill.debug(this.options.debug);
}
const html = this.container.innerHTML.trim();
this.container.classList.add('ql-container');
this.container.innerHTML = '';
instances.set(this.container, this);
this.root = this.addContainer('ql-editor');
this.root.classList.add('ql-blank');
this.emitter = new Emitter();
const scrollBlotName = Parchment.ScrollBlot.blotName;
const ScrollBlot = this.options.registry.query(scrollBlotName);
if (!ScrollBlot || !('blotName' in ScrollBlot)) {
throw new Error(`Cannot initialize Quill without "${scrollBlotName}" blot`);
}
this.scroll = new ScrollBlot(this.options.registry, this.root, {
emitter: this.emitter
});
this.editor = new Editor(this.scroll);
this.selection = new Selection(this.scroll, this.emitter);
this.composition = new Composition(this.scroll, this.emitter);
this.theme = new this.options.theme(this, this.options); // eslint-disable-line new-cap
this.keyboard = this.theme.addModule('keyboard');
this.clipboard = this.theme.addModule('clipboard');
this.history = this.theme.addModule('history');
this.uploader = this.theme.addModule('uploader');
this.theme.addModule('input');
this.theme.addModule('uiNode');
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) => {
const oldRange = this.selection.lastRange;
const [newRange] = this.selection.getRange();
const selectionInfo = oldRange && newRange ? {
oldRange,
newRange
} : undefined;
modify.call(this, () => this.editor.update(null, mutations, selectionInfo), source);
});
this.emitter.on(Emitter.events.SCROLL_EMBED_UPDATE, (blot, delta) => {
const oldRange = this.selection.lastRange;
const [newRange] = this.selection.getRange();
const selectionInfo = oldRange && newRange ? {
oldRange,
newRange
} : undefined;
modify.call(this, () => {
const change = new Delta().retain(blot.offset(this)).retain({
[blot.statics.blotName]: delta
});
return this.editor.update(change, [], selectionInfo);
}, Quill.sources.USER);
});
if (html) {
const contents = this.clipboard.convert({
html: `${html}
`,
text: '\n'
});
this.setContents(contents);
}
this.history.clear();
if (this.options.placeholder) {
this.root.setAttribute('data-placeholder', this.options.placeholder);
}
if (this.options.readOnly) {
this.disable();
}
this.allowReadOnlyEdits = false;
}
addContainer(container) {
let refNode = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
if (typeof container === 'string') {
const 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) {
// @ts-expect-error
[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);
}
editReadOnly(modifier) {
this.allowReadOnlyEdits = true;
const value = modifier();
this.allowReadOnlyEdits = false;
return value;
}
enable() {
let enabled = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
this.scroll.enable(enabled);
this.container.classList.toggle('ql-disabled', !enabled);
}
focus() {
let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
this.selection.focus();
if (!options.preventScroll) {
this.scrollSelectionIntoView();
}
}
format(name, value) {
let source = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : Emitter.sources.API;
return modify.call(this, () => {
const range = this.getSelection(true);
let change = new Delta();
if (range == null) return change;
if (this.scroll.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;
// eslint-disable-next-line prefer-const
[index, length, formats, source] = overload(index, length,
// @ts-expect-error
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;
// eslint-disable-next-line prefer-const
[index, length, formats, source] = overload(
// @ts-expect-error
index, length, name, value, source);
return modify.call(this, () => {
return this.editor.formatText(index, length, formats);
}, source, index, 0);
}
getBounds(index) {
let length = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
let bounds = null;
if (typeof index === 'number') {
bounds = this.selection.getBounds(index, length);
} else {
bounds = this.selection.getBounds(index.index, index.length);
}
if (!bounds) return null;
const 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() {
let index = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
let length = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.getLength() - index;
[index, length] = overload(index, length);
return this.editor.getContents(index, length);
}
getFormat() {
let index = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.getSelection(true);
let length = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
if (typeof index === 'number') {
return this.editor.getFormat(index, length);
}
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() {
let index = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
let length = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Number.MAX_VALUE;
if (typeof index !== 'number') {
return this.scroll.lines(index.index, index.length);
}
return this.scroll.lines(index, length);
}
getModule(name) {
return this.theme.modules[name];
}
getSelection() {
let focus = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
if (focus) this.focus();
this.update(); // Make sure we access getRange with editor in consistent state
return this.selection.getRange()[0];
}
getSemanticHTML() {
let index = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
let length = arguments.length > 1 ? arguments[1] : undefined;
if (typeof index === 'number') {
length = length ?? this.getLength() - index;
}
// @ts-expect-error
[index, length] = overload(index, length);
return this.editor.getHTML(index, length);
}
getText() {
let index = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
let length = arguments.length > 1 ? arguments[1] : undefined;
if (typeof index === 'number') {
length = length ?? this.getLength() - index;
}
// @ts-expect-error
[index, length] = overload(index, length);
return this.editor.getText(index, length);
}
hasFocus() {
return this.selection.hasFocus();
}
insertEmbed(index, embed, value) {
let source = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : Quill.sources.API;
return modify.call(this, () => {
return this.editor.insertEmbed(index, embed, value);
}, source, index);
}
insertText(index, text, name, value, source) {
let formats;
// eslint-disable-next-line prefer-const
// @ts-expect-error
[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.scroll.isEnabled();
}
off() {
return this.emitter.off(...arguments);
}
on() {
return this.emitter.on(...arguments);
}
once() {
return this.emitter.once(...arguments);
}
removeFormat(index, length, source) {
[index, length,, source] = overload(index, length, source);
return modify.call(this, () => {
return this.editor.removeFormat(index, length);
}, source, index);
}
scrollRectIntoView(rect) {
scrollRectIntoView(this.root, rect);
}
/**
* @deprecated Use Quill#scrollSelectionIntoView() instead.
*/
scrollIntoView() {
console.warn('Quill#scrollIntoView() has been deprecated and will be removed in the near future. Please use Quill#scrollSelectionIntoView() instead.');
this.scrollSelectionIntoView();
}
/**
* Scroll the current selection into the visible area.
* If the selection is already visible, no scrolling will occur.
*/
scrollSelectionIntoView() {
const range = this.selection.lastRange;
const bounds = range && this.selection.getBounds(range.index, range.length);
if (bounds) {
this.scrollRectIntoView(bounds);
}
}
setContents(delta) {
let source = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Emitter.sources.API;
return modify.call(this, () => {
delta = new Delta(delta);
const length = this.getLength();
// Quill will set empty editor to \n
const delete1 = this.editor.deleteText(0, length);
const applied = this.editor.insertContents(0, delta);
// Remove extra \n from empty editor initialization
const delete2 = this.editor.deleteText(this.getLength() - 1, 1);
return delete1.compose(applied).compose(delete2);
}, source);
}
setSelection(index, length, source) {
if (index == null) {
// @ts-expect-error https://github.com/microsoft/TypeScript/issues/22609
this.selection.setRange(null, length || Quill.sources.API);
} else {
// @ts-expect-error
[index, length,, source] = overload(index, length, source);
this.selection.setRange(new Range(Math.max(0, index), length), source);
if (source !== Emitter.sources.SILENT) {
this.scrollSelectionIntoView();
}
}
}
setText(text) {
let source = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Emitter.sources.API;
const delta = new Delta().insert(text);
return this.setContents(delta, source);
}
update() {
let source = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : Emitter.sources.USER;
const change = this.scroll.update(source); // Will update selection before selection.update() does if text changes
this.selection.update(source);
// TODO this is usually undefined
return change;
}
updateContents(delta) {
let source = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Emitter.sources.API;
return modify.call(this, () => {
delta = new Delta(delta);
return this.editor.applyDelta(delta);
}, source, true);
}
}
function resolveSelector(selector) {
return typeof selector === 'string' ? document.querySelector(selector) : selector;
}
function expandModuleConfig(config) {
return Object.entries(config ?? {}).reduce((expanded, _ref) => {
let [key, value] = _ref;
return {
...expanded,
[key]: value === true ? {} : value
};
}, {});
}
function omitUndefinedValuesFromOptions(obj) {
return Object.fromEntries(Object.entries(obj).filter(entry => entry[1] !== undefined));
}
function expandConfig(containerOrSelector, options) {
const container = resolveSelector(containerOrSelector);
if (!container) {
throw new Error('Invalid Quill container');
}
const shouldUseDefaultTheme = !options.theme || options.theme === Quill.DEFAULTS.theme;
const theme = shouldUseDefaultTheme ? Theme : Quill.import(`themes/${options.theme}`);
if (!theme) {
throw new Error(`Invalid theme ${options.theme}. Did you register it?`);
}
const {
modules: quillModuleDefaults,
...quillDefaults
} = Quill.DEFAULTS;
const {
modules: themeModuleDefaults,
...themeDefaults
} = theme.DEFAULTS;
let userModuleOptions = expandModuleConfig(options.modules);
// Special case toolbar shorthand
if (userModuleOptions != null && userModuleOptions.toolbar && userModuleOptions.toolbar.constructor !== Object) {
userModuleOptions = {
...userModuleOptions,
toolbar: {
container: userModuleOptions.toolbar
}
};
}
const modules = merge({}, expandModuleConfig(quillModuleDefaults), expandModuleConfig(themeModuleDefaults), userModuleOptions);
const config = {
...quillDefaults,
...omitUndefinedValuesFromOptions(themeDefaults),
...omitUndefinedValuesFromOptions(options)
};
let registry = options.registry;
if (registry) {
if (options.formats) {
debug.warn('Ignoring "formats" option because "registry" is specified');
}
} else {
registry = options.formats ? createRegistryWithFormats(options.formats, config.registry, debug) : config.registry;
}
return {
...config,
registry,
container,
theme,
modules: Object.entries(modules).reduce((modulesWithDefaults, _ref2) => {
let [name, value] = _ref2;
if (!value) return modulesWithDefaults;
const moduleClass = Quill.import(`modules/${name}`);
if (moduleClass == null) {
debug.error(`Cannot load ${name} module. Are you sure you registered it?`);
return modulesWithDefaults;
}
return {
...modulesWithDefaults,
// @ts-expect-error
[name]: merge({}, moduleClass.DEFAULTS || {}, value)
};
}, {}),
bounds: resolveSelector(config.bounds)
};
}
// Handle selection preservation and TEXT_CHANGE emission
// common to modification APIs
function modify(modifier, source, index, shift) {
if (!this.isEnabled() && source === Emitter.sources.USER && !this.allowReadOnlyEdits) {
return new Delta();
}
let range = index == null ? null : this.getSelection();
const oldDelta = this.editor.delta;
const change = modifier();
if (range != null) {
if (index === true) {
index = range.index; // eslint-disable-line prefer-destructuring
}
if (shift == null) {
range = shiftRange(range, change, source);
} else if (shift !== 0) {
// @ts-expect-error index should always be number
range = shiftRange(range, index, shift, source);
}
this.setSelection(range, Emitter.sources.SILENT);
}
if (change.length() > 0) {
const 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 = {};
// @ts-expect-error
if (typeof index.index === 'number' && typeof index.length === 'number') {
// Allow for throwaway end (used by insertText/insertEmbed)
if (typeof length !== 'number') {
// @ts-expect-error
source = value;
value = name;
name = length;
// @ts-expect-error
length = index.length; // eslint-disable-line prefer-destructuring
// @ts-expect-error
index = index.index; // eslint-disable-line prefer-destructuring
} else {
// @ts-expect-error
length = index.length; // eslint-disable-line prefer-destructuring
// @ts-expect-error
index = index.index; // eslint-disable-line prefer-destructuring
}
} else if (typeof length !== 'number') {
// @ts-expect-error
source = value;
value = name;
name = length;
length = 0;
}
// Handle format being object, two format name/value strings or excluded
if (typeof name === 'object') {
// @ts-expect-error Fix me later
formats = name;
// @ts-expect-error
source = value;
} else if (typeof name === 'string') {
if (value != null) {
formats[name] = value;
} else {
// @ts-expect-error
source = name;
}
}
// Handle optional source
source = source || Emitter.sources.API;
// @ts-expect-error
return [index, length, formats, source];
}
function shiftRange(range, index, lengthOrSource, source) {
const length = typeof lengthOrSource === 'number' ? lengthOrSource : 0;
if (range == null) return null;
let start;
let end;
// @ts-expect-error -- TODO: add a better type guard around `index`
if (index && typeof index.transformPosition === 'function') {
[start, end] = [range.index, range.index + range.length].map(pos =>
// @ts-expect-error -- TODO: add a better type guard around `index`
index.transformPosition(pos, source !== Emitter.sources.USER));
} else {
[start, end] = [range.index, range.index + range.length].map(pos => {
// @ts-expect-error -- TODO: add a better type guard around `index`
if (pos < index || pos === index && source === Emitter.sources.USER) return pos;
if (length >= 0) {
return pos + length;
}
// @ts-expect-error -- TODO: add a better type guard around `index`
return Math.max(index, pos + length);
});
}
return new Range(start, end - start);
}
export { Parchment, Range };
export { globalRegistry, expandConfig, overload, Quill as default };
//# sourceMappingURL=quill.js.map