Initial commit as of 2018-10-16
This commit is contained in:
267
assets/js/core/editor.js
Normal file
267
assets/js/core/editor.js
Normal file
@@ -0,0 +1,267 @@
|
||||
import Delta from 'quill-delta';
|
||||
import DeltaOp from 'quill-delta/lib/op';
|
||||
import Parchment from 'parchment';
|
||||
import CodeBlock from '../formats/code';
|
||||
import CursorBlot from '../blots/cursor';
|
||||
import Block, { bubbleFormats } from '../blots/block';
|
||||
import Break from '../blots/break';
|
||||
import clone from 'clone';
|
||||
import equal from 'deep-equal';
|
||||
import extend from 'extend';
|
||||
|
||||
|
||||
const ASCII = /^[ -~]*$/;
|
||||
|
||||
|
||||
class Editor {
|
||||
constructor(scroll) {
|
||||
this.scroll = scroll;
|
||||
this.delta = this.getDelta();
|
||||
}
|
||||
|
||||
applyDelta(delta) {
|
||||
let consumeNextNewline = false;
|
||||
this.scroll.update();
|
||||
let scrollLength = this.scroll.length();
|
||||
this.scroll.batchStart();
|
||||
delta = normalizeDelta(delta);
|
||||
delta.reduce((index, op) => {
|
||||
let length = op.retain || op.delete || op.insert.length || 1;
|
||||
let attributes = op.attributes || {};
|
||||
if (op.insert != null) {
|
||||
if (typeof op.insert === 'string') {
|
||||
let text = op.insert;
|
||||
if (text.endsWith('\n') && consumeNextNewline) {
|
||||
consumeNextNewline = false;
|
||||
text = text.slice(0, -1);
|
||||
}
|
||||
if (index >= scrollLength && !text.endsWith('\n')) {
|
||||
consumeNextNewline = true;
|
||||
}
|
||||
this.scroll.insertAt(index, text);
|
||||
let [line, offset] = this.scroll.line(index);
|
||||
let formats = extend({}, bubbleFormats(line));
|
||||
if (line instanceof Block) {
|
||||
let [leaf, ] = line.descendant(Parchment.Leaf, offset);
|
||||
formats = extend(formats, bubbleFormats(leaf));
|
||||
}
|
||||
attributes = DeltaOp.attributes.diff(formats, attributes) || {};
|
||||
} else if (typeof op.insert === 'object') {
|
||||
let key = Object.keys(op.insert)[0]; // There should only be one key
|
||||
if (key == null) return index;
|
||||
this.scroll.insertAt(index, key, op.insert[key]);
|
||||
}
|
||||
scrollLength += length;
|
||||
}
|
||||
Object.keys(attributes).forEach((name) => {
|
||||
this.scroll.formatAt(index, length, name, attributes[name]);
|
||||
});
|
||||
return index + length;
|
||||
}, 0);
|
||||
delta.reduce((index, op) => {
|
||||
if (typeof op.delete === 'number') {
|
||||
this.scroll.deleteAt(index, op.delete);
|
||||
return index;
|
||||
}
|
||||
return index + (op.retain || op.insert.length || 1);
|
||||
}, 0);
|
||||
this.scroll.batchEnd();
|
||||
return this.update(delta);
|
||||
}
|
||||
|
||||
deleteText(index, length) {
|
||||
this.scroll.deleteAt(index, length);
|
||||
return this.update(new Delta().retain(index).delete(length));
|
||||
}
|
||||
|
||||
formatLine(index, length, formats = {}) {
|
||||
this.scroll.update();
|
||||
Object.keys(formats).forEach((format) => {
|
||||
if (this.scroll.whitelist != null && !this.scroll.whitelist[format]) return;
|
||||
let lines = this.scroll.lines(index, Math.max(length, 1));
|
||||
let lengthRemaining = length;
|
||||
lines.forEach((line) => {
|
||||
let lineLength = line.length();
|
||||
if (!(line instanceof CodeBlock)) {
|
||||
line.format(format, formats[format]);
|
||||
} else {
|
||||
let codeIndex = index - line.offset(this.scroll);
|
||||
let codeLength = line.newlineIndex(codeIndex + lengthRemaining) - codeIndex + 1;
|
||||
line.formatAt(codeIndex, codeLength, format, formats[format]);
|
||||
}
|
||||
lengthRemaining -= lineLength;
|
||||
});
|
||||
});
|
||||
this.scroll.optimize();
|
||||
return this.update(new Delta().retain(index).retain(length, clone(formats)));
|
||||
}
|
||||
|
||||
formatText(index, length, formats = {}) {
|
||||
Object.keys(formats).forEach((format) => {
|
||||
this.scroll.formatAt(index, length, format, formats[format]);
|
||||
});
|
||||
return this.update(new Delta().retain(index).retain(length, clone(formats)));
|
||||
}
|
||||
|
||||
getContents(index, length) {
|
||||
return this.delta.slice(index, index + length);
|
||||
}
|
||||
|
||||
getDelta() {
|
||||
return this.scroll.lines().reduce((delta, line) => {
|
||||
return delta.concat(line.delta());
|
||||
}, new Delta());
|
||||
}
|
||||
|
||||
getFormat(index, length = 0) {
|
||||
let lines = [], leaves = [];
|
||||
if (length === 0) {
|
||||
this.scroll.path(index).forEach(function(path) {
|
||||
let [blot, ] = path;
|
||||
if (blot instanceof Block) {
|
||||
lines.push(blot);
|
||||
} else if (blot instanceof Parchment.Leaf) {
|
||||
leaves.push(blot);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
lines = this.scroll.lines(index, length);
|
||||
leaves = this.scroll.descendants(Parchment.Leaf, index, length);
|
||||
}
|
||||
let formatsArr = [lines, leaves].map(function(blots) {
|
||||
if (blots.length === 0) return {};
|
||||
let formats = bubbleFormats(blots.shift());
|
||||
while (Object.keys(formats).length > 0) {
|
||||
let blot = blots.shift();
|
||||
if (blot == null) return formats;
|
||||
formats = combineFormats(bubbleFormats(blot), formats);
|
||||
}
|
||||
return formats;
|
||||
});
|
||||
return extend.apply(extend, formatsArr);
|
||||
}
|
||||
|
||||
getText(index, length) {
|
||||
return this.getContents(index, length).filter(function(op) {
|
||||
return typeof op.insert === 'string';
|
||||
}).map(function(op) {
|
||||
return op.insert;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
insertEmbed(index, embed, value) {
|
||||
this.scroll.insertAt(index, embed, value);
|
||||
return this.update(new Delta().retain(index).insert({ [embed]: value }));
|
||||
}
|
||||
|
||||
insertText(index, text, formats = {}) {
|
||||
text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||
this.scroll.insertAt(index, text);
|
||||
Object.keys(formats).forEach((format) => {
|
||||
this.scroll.formatAt(index, text.length, format, formats[format]);
|
||||
});
|
||||
return this.update(new Delta().retain(index).insert(text, clone(formats)));
|
||||
}
|
||||
|
||||
isBlank() {
|
||||
if (this.scroll.children.length == 0) return true;
|
||||
if (this.scroll.children.length > 1) return false;
|
||||
let block = this.scroll.children.head;
|
||||
if (block.statics.blotName !== Block.blotName) return false;
|
||||
if (block.children.length > 1) return false;
|
||||
return block.children.head instanceof Break;
|
||||
}
|
||||
|
||||
removeFormat(index, length) {
|
||||
let text = this.getText(index, length);
|
||||
let [line, offset] = this.scroll.line(index + length);
|
||||
let suffixLength = 0, suffix = new Delta();
|
||||
if (line != null) {
|
||||
if (!(line instanceof CodeBlock)) {
|
||||
suffixLength = line.length() - offset;
|
||||
} else {
|
||||
suffixLength = line.newlineIndex(offset) - offset + 1;
|
||||
}
|
||||
suffix = line.delta().slice(offset, offset + suffixLength - 1).insert('\n');
|
||||
}
|
||||
let contents = this.getContents(index, length + suffixLength);
|
||||
let diff = contents.diff(new Delta().insert(text).concat(suffix));
|
||||
let delta = new Delta().retain(index).concat(diff);
|
||||
return this.applyDelta(delta);
|
||||
}
|
||||
|
||||
update(change, mutations = [], cursorIndex = undefined) {
|
||||
let oldDelta = this.delta;
|
||||
if (mutations.length === 1 &&
|
||||
mutations[0].type === 'characterData' &&
|
||||
mutations[0].target.data.match(ASCII) &&
|
||||
Parchment.find(mutations[0].target)) {
|
||||
// Optimization for character changes
|
||||
let textBlot = Parchment.find(mutations[0].target);
|
||||
let formats = bubbleFormats(textBlot);
|
||||
let index = textBlot.offset(this.scroll);
|
||||
let oldValue = mutations[0].oldValue.replace(CursorBlot.CONTENTS, '');
|
||||
let oldText = new Delta().insert(oldValue);
|
||||
let newText = new Delta().insert(textBlot.value());
|
||||
let diffDelta = new Delta().retain(index).concat(oldText.diff(newText, cursorIndex));
|
||||
change = diffDelta.reduce(function(delta, op) {
|
||||
if (op.insert) {
|
||||
return delta.insert(op.insert, formats);
|
||||
} else {
|
||||
return delta.push(op);
|
||||
}
|
||||
}, new Delta());
|
||||
this.delta = oldDelta.compose(change);
|
||||
} else {
|
||||
this.delta = this.getDelta();
|
||||
if (!change || !equal(oldDelta.compose(change), this.delta)) {
|
||||
change = oldDelta.diff(this.delta, cursorIndex);
|
||||
}
|
||||
}
|
||||
return change;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function combineFormats(formats, combined) {
|
||||
return Object.keys(combined).reduce(function(merged, name) {
|
||||
if (formats[name] == null) return merged;
|
||||
if (combined[name] === formats[name]) {
|
||||
merged[name] = combined[name];
|
||||
} else if (Array.isArray(combined[name])) {
|
||||
if (combined[name].indexOf(formats[name]) < 0) {
|
||||
merged[name] = combined[name].concat([formats[name]]);
|
||||
}
|
||||
} else {
|
||||
merged[name] = [combined[name], formats[name]];
|
||||
}
|
||||
return merged;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function normalizeDelta(delta) {
|
||||
return delta.reduce(function(delta, op) {
|
||||
if (op.insert === 1) {
|
||||
let attributes = clone(op.attributes);
|
||||
delete attributes['image'];
|
||||
return delta.insert({ image: op.attributes.image }, attributes);
|
||||
}
|
||||
if (op.attributes != null && (op.attributes.list === true || op.attributes.bullet === true)) {
|
||||
op = clone(op);
|
||||
if (op.attributes.list) {
|
||||
op.attributes.list = 'ordered';
|
||||
} else {
|
||||
op.attributes.list = 'bullet';
|
||||
delete op.attributes.bullet;
|
||||
}
|
||||
}
|
||||
if (typeof op.insert === 'string') {
|
||||
let text = op.insert.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||
return delta.insert(text, op.attributes);
|
||||
}
|
||||
return delta.push(op);
|
||||
}, new Delta());
|
||||
}
|
||||
|
||||
|
||||
export default Editor;
|
63
assets/js/core/emitter.js
Normal file
63
assets/js/core/emitter.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import logger from './logger';
|
||||
|
||||
let debug = logger('quill:events');
|
||||
|
||||
const EVENTS = ['selectionchange', 'mousedown', 'mouseup', 'click'];
|
||||
|
||||
EVENTS.forEach(function(eventName) {
|
||||
document.addEventListener(eventName, (...args) => {
|
||||
[].slice.call(document.querySelectorAll('.ql-container')).forEach((node) => {
|
||||
// TODO use WeakMap
|
||||
if (node.__quill && node.__quill.emitter) {
|
||||
node.__quill.emitter.handleDOM(...args);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
class Emitter extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this.listeners = {};
|
||||
this.on('error', debug.error);
|
||||
}
|
||||
|
||||
emit() {
|
||||
debug.log.apply(debug, arguments);
|
||||
super.emit.apply(this, arguments);
|
||||
}
|
||||
|
||||
handleDOM(event, ...args) {
|
||||
(this.listeners[event.type] || []).forEach(function({ node, handler }) {
|
||||
if (event.target === node || node.contains(event.target)) {
|
||||
handler(event, ...args);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
listenDOM(eventName, node, handler) {
|
||||
if (!this.listeners[eventName]) {
|
||||
this.listeners[eventName] = [];
|
||||
}
|
||||
this.listeners[eventName].push({ node, handler })
|
||||
}
|
||||
}
|
||||
|
||||
Emitter.events = {
|
||||
EDITOR_CHANGE : 'editor-change',
|
||||
SCROLL_BEFORE_UPDATE : 'scroll-before-update',
|
||||
SCROLL_OPTIMIZE : 'scroll-optimize',
|
||||
SCROLL_UPDATE : 'scroll-update',
|
||||
SELECTION_CHANGE : 'selection-change',
|
||||
TEXT_CHANGE : 'text-change'
|
||||
};
|
||||
Emitter.sources = {
|
||||
API : 'api',
|
||||
SILENT : 'silent',
|
||||
USER : 'user'
|
||||
};
|
||||
|
||||
|
||||
export default Emitter;
|
22
assets/js/core/logger.js
Normal file
22
assets/js/core/logger.js
Normal file
@@ -0,0 +1,22 @@
|
||||
let levels = ['error', 'warn', 'log', 'info'];
|
||||
let level = 'warn';
|
||||
|
||||
function debug(method, ...args) {
|
||||
if (levels.indexOf(method) <= levels.indexOf(level)) {
|
||||
console[method](...args); // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
function namespace(ns) {
|
||||
return levels.reduce(function(logger, method) {
|
||||
logger[method] = debug.bind(console, method, ns);
|
||||
return logger;
|
||||
}, {});
|
||||
}
|
||||
|
||||
debug.level = namespace.level = function(newLevel) {
|
||||
level = newLevel;
|
||||
};
|
||||
|
||||
|
||||
export default namespace;
|
10
assets/js/core/module.js
Normal file
10
assets/js/core/module.js
Normal file
@@ -0,0 +1,10 @@
|
||||
class Module {
|
||||
constructor(quill, options = {}) {
|
||||
this.quill = quill;
|
||||
this.options = options;
|
||||
}
|
||||
}
|
||||
Module.DEFAULTS = {};
|
||||
|
||||
|
||||
export default Module;
|
63
assets/js/core/polyfill.js
Normal file
63
assets/js/core/polyfill.js
Normal file
@@ -0,0 +1,63 @@
|
||||
let elem = document.createElement('div');
|
||||
elem.classList.toggle('test-class', false);
|
||||
if (elem.classList.contains('test-class')) {
|
||||
let _toggle = DOMTokenList.prototype.toggle;
|
||||
DOMTokenList.prototype.toggle = function(token, force) {
|
||||
if (arguments.length > 1 && !this.contains(token) === !force) {
|
||||
return force;
|
||||
} else {
|
||||
return _toggle.call(this, token);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!String.prototype.startsWith) {
|
||||
String.prototype.startsWith = function(searchString, position){
|
||||
position = position || 0;
|
||||
return this.substr(position, searchString.length) === searchString;
|
||||
};
|
||||
}
|
||||
|
||||
if (!String.prototype.endsWith) {
|
||||
String.prototype.endsWith = function(searchString, position) {
|
||||
var subjectString = this.toString();
|
||||
if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) {
|
||||
position = subjectString.length;
|
||||
}
|
||||
position -= searchString.length;
|
||||
var lastIndex = subjectString.indexOf(searchString, position);
|
||||
return lastIndex !== -1 && lastIndex === position;
|
||||
};
|
||||
}
|
||||
|
||||
if (!Array.prototype.find) {
|
||||
Object.defineProperty(Array.prototype, "find", {
|
||||
value: function(predicate) {
|
||||
if (this === null) {
|
||||
throw new TypeError('Array.prototype.find called on null or undefined');
|
||||
}
|
||||
if (typeof predicate !== 'function') {
|
||||
throw new TypeError('predicate must be a function');
|
||||
}
|
||||
var list = Object(this);
|
||||
var length = list.length >>> 0;
|
||||
var thisArg = arguments[1];
|
||||
var value;
|
||||
|
||||
for (var i = 0; i < length; i++) {
|
||||
value = list[i];
|
||||
if (predicate.call(thisArg, value, i, list)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
// Disable resizing in Firefox
|
||||
document.execCommand("enableObjectResizing", false, false);
|
||||
// Disable automatic linkifying in IE11
|
||||
document.execCommand("autoUrlDetect", false, false);
|
||||
});
|
506
assets/js/core/quill.js
Normal file
506
assets/js/core/quill.js
Normal file
@@ -0,0 +1,506 @@
|
||||
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(`<div class='ql-editor' style="white-space: normal;">${html}<p><br></p></div>`);
|
||||
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 };
|
355
assets/js/core/selection.js
Normal file
355
assets/js/core/selection.js
Normal file
@@ -0,0 +1,355 @@
|
||||
import Parchment from 'parchment';
|
||||
import clone from 'clone';
|
||||
import equal from 'deep-equal';
|
||||
import Emitter from './emitter';
|
||||
import logger from './logger';
|
||||
|
||||
let debug = logger('quill:selection');
|
||||
|
||||
|
||||
class Range {
|
||||
constructor(index, length = 0) {
|
||||
this.index = index;
|
||||
this.length = length;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Selection {
|
||||
constructor(scroll, emitter) {
|
||||
this.emitter = emitter;
|
||||
this.scroll = scroll;
|
||||
this.composing = false;
|
||||
this.mouseDown = false;
|
||||
this.root = this.scroll.domNode;
|
||||
this.cursor = Parchment.create('cursor', this);
|
||||
// savedRange is last non-null range
|
||||
this.lastRange = this.savedRange = new Range(0, 0);
|
||||
this.handleComposition();
|
||||
this.handleDragging();
|
||||
this.emitter.listenDOM('selectionchange', document, () => {
|
||||
if (!this.mouseDown) {
|
||||
setTimeout(this.update.bind(this, Emitter.sources.USER), 1);
|
||||
}
|
||||
});
|
||||
this.emitter.on(Emitter.events.EDITOR_CHANGE, (type, delta) => {
|
||||
if (type === Emitter.events.TEXT_CHANGE && delta.length() > 0) {
|
||||
this.update(Emitter.sources.SILENT);
|
||||
}
|
||||
});
|
||||
this.emitter.on(Emitter.events.SCROLL_BEFORE_UPDATE, () => {
|
||||
if (!this.hasFocus()) return;
|
||||
let native = this.getNativeRange();
|
||||
if (native == null) return;
|
||||
if (native.start.node === this.cursor.textNode) return; // cursor.restore() will handle
|
||||
// TODO unclear if this has negative side effects
|
||||
this.emitter.once(Emitter.events.SCROLL_UPDATE, () => {
|
||||
try {
|
||||
this.setNativeRange(native.start.node, native.start.offset, native.end.node, native.end.offset);
|
||||
} catch (ignored) {}
|
||||
});
|
||||
});
|
||||
this.emitter.on(Emitter.events.SCROLL_OPTIMIZE, (mutations, context) => {
|
||||
if (context.range) {
|
||||
const { startNode, startOffset, endNode, endOffset } = context.range;
|
||||
this.setNativeRange(startNode, startOffset, endNode, endOffset);
|
||||
}
|
||||
});
|
||||
this.update(Emitter.sources.SILENT);
|
||||
}
|
||||
|
||||
handleComposition() {
|
||||
this.root.addEventListener('compositionstart', () => {
|
||||
this.composing = true;
|
||||
});
|
||||
this.root.addEventListener('compositionend', () => {
|
||||
this.composing = false;
|
||||
if (this.cursor.parent) {
|
||||
const range = this.cursor.restore();
|
||||
if (!range) return;
|
||||
setTimeout(() => {
|
||||
this.setNativeRange(range.startNode, range.startOffset, range.endNode, range.endOffset);
|
||||
}, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleDragging() {
|
||||
this.emitter.listenDOM('mousedown', document.body, () => {
|
||||
this.mouseDown = true;
|
||||
});
|
||||
this.emitter.listenDOM('mouseup', document.body, () => {
|
||||
this.mouseDown = false;
|
||||
this.update(Emitter.sources.USER);
|
||||
});
|
||||
}
|
||||
|
||||
focus() {
|
||||
if (this.hasFocus()) return;
|
||||
this.root.focus();
|
||||
this.setRange(this.savedRange);
|
||||
}
|
||||
|
||||
format(format, value) {
|
||||
if (this.scroll.whitelist != null && !this.scroll.whitelist[format]) return;
|
||||
this.scroll.update();
|
||||
let nativeRange = this.getNativeRange();
|
||||
if (nativeRange == null || !nativeRange.native.collapsed || Parchment.query(format, Parchment.Scope.BLOCK)) return;
|
||||
if (nativeRange.start.node !== this.cursor.textNode) {
|
||||
let blot = Parchment.find(nativeRange.start.node, false);
|
||||
if (blot == null) return;
|
||||
// TODO Give blot ability to not split
|
||||
if (blot instanceof Parchment.Leaf) {
|
||||
let after = blot.split(nativeRange.start.offset);
|
||||
blot.parent.insertBefore(this.cursor, after);
|
||||
} else {
|
||||
blot.insertBefore(this.cursor, nativeRange.start.node); // Should never happen
|
||||
}
|
||||
this.cursor.attach();
|
||||
}
|
||||
this.cursor.format(format, value);
|
||||
this.scroll.optimize();
|
||||
this.setNativeRange(this.cursor.textNode, this.cursor.textNode.data.length);
|
||||
this.update();
|
||||
}
|
||||
|
||||
getBounds(index, length = 0) {
|
||||
let scrollLength = this.scroll.length();
|
||||
index = Math.min(index, scrollLength - 1);
|
||||
length = Math.min(index + length, scrollLength - 1) - index;
|
||||
let node, [leaf, offset] = this.scroll.leaf(index);
|
||||
if (leaf == null) return null;
|
||||
[node, offset] = leaf.position(offset, true);
|
||||
let range = document.createRange();
|
||||
if (length > 0) {
|
||||
range.setStart(node, offset);
|
||||
[leaf, offset] = this.scroll.leaf(index + length);
|
||||
if (leaf == null) return null;
|
||||
[node, offset] = leaf.position(offset, true);
|
||||
range.setEnd(node, offset);
|
||||
return range.getBoundingClientRect();
|
||||
} else {
|
||||
let side = 'left';
|
||||
let rect;
|
||||
if (node instanceof Text) {
|
||||
if (offset < node.data.length) {
|
||||
range.setStart(node, offset);
|
||||
range.setEnd(node, offset + 1);
|
||||
} else {
|
||||
range.setStart(node, offset - 1);
|
||||
range.setEnd(node, offset);
|
||||
side = 'right';
|
||||
}
|
||||
rect = range.getBoundingClientRect();
|
||||
} else {
|
||||
rect = leaf.domNode.getBoundingClientRect();
|
||||
if (offset > 0) side = 'right';
|
||||
}
|
||||
return {
|
||||
bottom: rect.top + rect.height,
|
||||
height: rect.height,
|
||||
left: rect[side],
|
||||
right: rect[side],
|
||||
top: rect.top,
|
||||
width: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
getNativeRange() {
|
||||
let selection = document.getSelection();
|
||||
if (selection == null || selection.rangeCount <= 0) return null;
|
||||
let nativeRange = selection.getRangeAt(0);
|
||||
if (nativeRange == null) return null;
|
||||
let range = this.normalizeNative(nativeRange);
|
||||
debug.info('getNativeRange', range);
|
||||
return range;
|
||||
}
|
||||
|
||||
getRange() {
|
||||
let normalized = this.getNativeRange();
|
||||
if (normalized == null) return [null, null];
|
||||
let range = this.normalizedToRange(normalized);
|
||||
return [range, normalized];
|
||||
}
|
||||
|
||||
hasFocus() {
|
||||
return document.activeElement === this.root;
|
||||
}
|
||||
|
||||
normalizedToRange(range) {
|
||||
let positions = [[range.start.node, range.start.offset]];
|
||||
if (!range.native.collapsed) {
|
||||
positions.push([range.end.node, range.end.offset]);
|
||||
}
|
||||
let indexes = positions.map((position) => {
|
||||
let [node, offset] = position;
|
||||
let blot = Parchment.find(node, true);
|
||||
let index = blot.offset(this.scroll);
|
||||
if (offset === 0) {
|
||||
return index;
|
||||
} else if (blot instanceof Parchment.Container) {
|
||||
return index + blot.length();
|
||||
} else {
|
||||
return index + blot.index(node, offset);
|
||||
}
|
||||
});
|
||||
let end = Math.min(Math.max(...indexes), this.scroll.length() - 1);
|
||||
let start = Math.min(end, ...indexes);
|
||||
return new Range(start, end-start);
|
||||
}
|
||||
|
||||
normalizeNative(nativeRange) {
|
||||
if (!contains(this.root, nativeRange.startContainer) ||
|
||||
(!nativeRange.collapsed && !contains(this.root, nativeRange.endContainer))) {
|
||||
return null;
|
||||
}
|
||||
let range = {
|
||||
start: { node: nativeRange.startContainer, offset: nativeRange.startOffset },
|
||||
end: { node: nativeRange.endContainer, offset: nativeRange.endOffset },
|
||||
native: nativeRange
|
||||
};
|
||||
[range.start, range.end].forEach(function(position) {
|
||||
let node = position.node, offset = position.offset;
|
||||
while (!(node instanceof Text) && node.childNodes.length > 0) {
|
||||
if (node.childNodes.length > offset) {
|
||||
node = node.childNodes[offset];
|
||||
offset = 0;
|
||||
} else if (node.childNodes.length === offset) {
|
||||
node = node.lastChild;
|
||||
offset = node instanceof Text ? node.data.length : node.childNodes.length + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
position.node = node, position.offset = offset;
|
||||
});
|
||||
return range;
|
||||
}
|
||||
|
||||
rangeToNative(range) {
|
||||
let indexes = range.collapsed ? [range.index] : [range.index, range.index + range.length];
|
||||
let args = [];
|
||||
let scrollLength = this.scroll.length();
|
||||
indexes.forEach((index, i) => {
|
||||
index = Math.min(scrollLength - 1, index);
|
||||
let node, [leaf, offset] = this.scroll.leaf(index);
|
||||
[node, offset] = leaf.position(offset, i !== 0);
|
||||
args.push(node, offset);
|
||||
});
|
||||
if (args.length < 2) {
|
||||
args = args.concat(args);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
scrollIntoView(scrollingContainer) {
|
||||
let range = this.lastRange;
|
||||
if (range == null) return;
|
||||
let bounds = this.getBounds(range.index, range.length);
|
||||
if (bounds == null) return;
|
||||
let limit = this.scroll.length()-1;
|
||||
let [first, ] = this.scroll.line(Math.min(range.index, limit));
|
||||
let last = first;
|
||||
if (range.length > 0) {
|
||||
[last, ] = this.scroll.line(Math.min(range.index + range.length, limit));
|
||||
}
|
||||
if (first == null || last == null) return;
|
||||
let scrollBounds = scrollingContainer.getBoundingClientRect();
|
||||
if (bounds.top < scrollBounds.top) {
|
||||
scrollingContainer.scrollTop -= (scrollBounds.top - bounds.top);
|
||||
} else if (bounds.bottom > scrollBounds.bottom) {
|
||||
scrollingContainer.scrollTop += (bounds.bottom - scrollBounds.bottom);
|
||||
}
|
||||
}
|
||||
|
||||
setNativeRange(startNode, startOffset, endNode = startNode, endOffset = startOffset, force = false) {
|
||||
debug.info('setNativeRange', startNode, startOffset, endNode, endOffset);
|
||||
if (startNode != null && (this.root.parentNode == null || startNode.parentNode == null || endNode.parentNode == null)) {
|
||||
return;
|
||||
}
|
||||
let selection = document.getSelection();
|
||||
if (selection == null) return;
|
||||
if (startNode != null) {
|
||||
if (!this.hasFocus()) this.root.focus();
|
||||
let native = (this.getNativeRange() || {}).native;
|
||||
if (native == null || force ||
|
||||
startNode !== native.startContainer ||
|
||||
startOffset !== native.startOffset ||
|
||||
endNode !== native.endContainer ||
|
||||
endOffset !== native.endOffset) {
|
||||
|
||||
if (startNode.tagName == "BR") {
|
||||
startOffset = [].indexOf.call(startNode.parentNode.childNodes, startNode);
|
||||
startNode = startNode.parentNode;
|
||||
}
|
||||
if (endNode.tagName == "BR") {
|
||||
endOffset = [].indexOf.call(endNode.parentNode.childNodes, endNode);
|
||||
endNode = endNode.parentNode;
|
||||
}
|
||||
let range = document.createRange();
|
||||
range.setStart(startNode, startOffset);
|
||||
range.setEnd(endNode, endOffset);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
} else {
|
||||
selection.removeAllRanges();
|
||||
this.root.blur();
|
||||
document.body.focus(); // root.blur() not enough on IE11+Travis+SauceLabs (but not local VMs)
|
||||
}
|
||||
}
|
||||
|
||||
setRange(range, force = false, source = Emitter.sources.API) {
|
||||
if (typeof force === 'string') {
|
||||
source = force;
|
||||
force = false;
|
||||
}
|
||||
debug.info('setRange', range);
|
||||
if (range != null) {
|
||||
let args = this.rangeToNative(range);
|
||||
this.setNativeRange(...args, force);
|
||||
} else {
|
||||
this.setNativeRange(null);
|
||||
}
|
||||
this.update(source);
|
||||
}
|
||||
|
||||
update(source = Emitter.sources.USER) {
|
||||
let oldRange = this.lastRange;
|
||||
let [lastRange, nativeRange] = this.getRange();
|
||||
this.lastRange = lastRange;
|
||||
if (this.lastRange != null) {
|
||||
this.savedRange = this.lastRange;
|
||||
}
|
||||
if (!equal(oldRange, this.lastRange)) {
|
||||
if (!this.composing && nativeRange != null && nativeRange.native.collapsed && nativeRange.start.node !== this.cursor.textNode) {
|
||||
this.cursor.restore();
|
||||
}
|
||||
let args = [Emitter.events.SELECTION_CHANGE, clone(this.lastRange), clone(oldRange), source];
|
||||
this.emitter.emit(Emitter.events.EDITOR_CHANGE, ...args);
|
||||
if (source !== Emitter.sources.SILENT) {
|
||||
this.emitter.emit(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function contains(parent, descendant) {
|
||||
try {
|
||||
// Firefox inserts inaccessible nodes around video elements
|
||||
descendant.parentNode;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
// IE11 has bug with Text nodes
|
||||
// https://connect.microsoft.com/IE/feedback/details/780874/node-contains-is-incorrect
|
||||
if (descendant instanceof Text) {
|
||||
descendant = descendant.parentNode;
|
||||
}
|
||||
return parent.contains(descendant);
|
||||
}
|
||||
|
||||
|
||||
export { Range, Selection as default };
|
30
assets/js/core/theme.js
Normal file
30
assets/js/core/theme.js
Normal file
@@ -0,0 +1,30 @@
|
||||
class Theme {
|
||||
constructor(quill, options) {
|
||||
this.quill = quill;
|
||||
this.options = options;
|
||||
this.modules = {};
|
||||
}
|
||||
|
||||
init() {
|
||||
Object.keys(this.options.modules).forEach((name) => {
|
||||
if (this.modules[name] == null) {
|
||||
this.addModule(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addModule(name) {
|
||||
let moduleClass = this.quill.constructor.import(`modules/${name}`);
|
||||
this.modules[name] = new moduleClass(this.quill, this.options.modules[name] || {});
|
||||
return this.modules[name];
|
||||
}
|
||||
}
|
||||
Theme.DEFAULTS = {
|
||||
modules: {}
|
||||
};
|
||||
Theme.themes = {
|
||||
'default': Theme
|
||||
};
|
||||
|
||||
|
||||
export default Theme;
|
Reference in New Issue
Block a user