Initial commit as of 2018-10-16
This commit is contained in:
358
assets/js/modules/clipboard.js
Normal file
358
assets/js/modules/clipboard.js
Normal file
@@ -0,0 +1,358 @@
|
||||
import extend from 'extend';
|
||||
import Delta from 'quill-delta';
|
||||
import Parchment from 'parchment';
|
||||
import Quill from '../core/quill';
|
||||
import logger from '../core/logger';
|
||||
import Module from '../core/module';
|
||||
|
||||
import { AlignAttribute, AlignStyle } from '../formats/align';
|
||||
import { BackgroundStyle } from '../formats/background';
|
||||
import CodeBlock from '../formats/code';
|
||||
import { ColorStyle } from '../formats/color';
|
||||
import { DirectionAttribute, DirectionStyle } from '../formats/direction';
|
||||
import { FontStyle } from '../formats/font';
|
||||
import { SizeStyle } from '../formats/size';
|
||||
|
||||
let debug = logger('quill:clipboard');
|
||||
|
||||
|
||||
const DOM_KEY = '__ql-matcher';
|
||||
|
||||
const CLIPBOARD_CONFIG = [
|
||||
[Node.TEXT_NODE, matchText],
|
||||
[Node.TEXT_NODE, matchNewline],
|
||||
['br', matchBreak],
|
||||
[Node.ELEMENT_NODE, matchNewline],
|
||||
[Node.ELEMENT_NODE, matchBlot],
|
||||
[Node.ELEMENT_NODE, matchSpacing],
|
||||
[Node.ELEMENT_NODE, matchAttributor],
|
||||
[Node.ELEMENT_NODE, matchStyles],
|
||||
['li', matchIndent],
|
||||
['b', matchAlias.bind(matchAlias, 'bold')],
|
||||
['i', matchAlias.bind(matchAlias, 'italic')],
|
||||
['style', matchIgnore]
|
||||
];
|
||||
|
||||
const ATTRIBUTE_ATTRIBUTORS = [
|
||||
AlignAttribute,
|
||||
DirectionAttribute
|
||||
].reduce(function(memo, attr) {
|
||||
memo[attr.keyName] = attr;
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
const STYLE_ATTRIBUTORS = [
|
||||
AlignStyle,
|
||||
BackgroundStyle,
|
||||
ColorStyle,
|
||||
DirectionStyle,
|
||||
FontStyle,
|
||||
SizeStyle
|
||||
].reduce(function(memo, attr) {
|
||||
memo[attr.keyName] = attr;
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
|
||||
class Clipboard extends Module {
|
||||
constructor(quill, options) {
|
||||
super(quill, options);
|
||||
this.quill.root.addEventListener('paste', this.onPaste.bind(this));
|
||||
this.container = this.quill.addContainer('ql-clipboard');
|
||||
this.container.setAttribute('contenteditable', true);
|
||||
this.container.setAttribute('tabindex', -1);
|
||||
this.matchers = [];
|
||||
CLIPBOARD_CONFIG.concat(this.options.matchers).forEach(([selector, matcher]) => {
|
||||
if (!options.matchVisual && matcher === matchSpacing) return;
|
||||
this.addMatcher(selector, matcher);
|
||||
});
|
||||
}
|
||||
|
||||
addMatcher(selector, matcher) {
|
||||
this.matchers.push([selector, matcher]);
|
||||
}
|
||||
|
||||
convert(html) {
|
||||
if (typeof html === 'string') {
|
||||
this.container.innerHTML = html.replace(/\>\r?\n +\</g, '><'); // Remove spaces between tags
|
||||
return this.convert();
|
||||
}
|
||||
const formats = this.quill.getFormat(this.quill.selection.savedRange.index);
|
||||
if (formats[CodeBlock.blotName]) {
|
||||
const text = this.container.innerText;
|
||||
this.container.innerHTML = '';
|
||||
return new Delta().insert(text, { [CodeBlock.blotName]: formats[CodeBlock.blotName] });
|
||||
}
|
||||
let [elementMatchers, textMatchers] = this.prepareMatching();
|
||||
let delta = traverse(this.container, elementMatchers, textMatchers);
|
||||
// Remove trailing newline
|
||||
if (deltaEndsWith(delta, '\n') && delta.ops[delta.ops.length - 1].attributes == null) {
|
||||
delta = delta.compose(new Delta().retain(delta.length() - 1).delete(1));
|
||||
}
|
||||
debug.log('convert', this.container.innerHTML, delta);
|
||||
this.container.innerHTML = '';
|
||||
return delta;
|
||||
}
|
||||
|
||||
dangerouslyPasteHTML(index, html, source = Quill.sources.API) {
|
||||
if (typeof index === 'string') {
|
||||
this.quill.setContents(this.convert(index), html);
|
||||
this.quill.setSelection(0, Quill.sources.SILENT);
|
||||
} else {
|
||||
let paste = this.convert(html);
|
||||
this.quill.updateContents(new Delta().retain(index).concat(paste), source);
|
||||
this.quill.setSelection(index + paste.length(), Quill.sources.SILENT);
|
||||
}
|
||||
}
|
||||
|
||||
onPaste(e) {
|
||||
if (e.defaultPrevented || !this.quill.isEnabled()) return;
|
||||
let range = this.quill.getSelection();
|
||||
let delta = new Delta().retain(range.index);
|
||||
let scrollTop = this.quill.scrollingContainer.scrollTop;
|
||||
this.container.focus();
|
||||
this.quill.selection.update(Quill.sources.SILENT);
|
||||
setTimeout(() => {
|
||||
delta = delta.concat(this.convert()).delete(range.length);
|
||||
this.quill.updateContents(delta, Quill.sources.USER);
|
||||
// range.length contributes to delta.length()
|
||||
this.quill.setSelection(delta.length() - range.length, Quill.sources.SILENT);
|
||||
this.quill.scrollingContainer.scrollTop = scrollTop;
|
||||
this.quill.focus();
|
||||
}, 1);
|
||||
}
|
||||
|
||||
prepareMatching() {
|
||||
let elementMatchers = [], textMatchers = [];
|
||||
this.matchers.forEach((pair) => {
|
||||
let [selector, matcher] = pair;
|
||||
switch (selector) {
|
||||
case Node.TEXT_NODE:
|
||||
textMatchers.push(matcher);
|
||||
break;
|
||||
case Node.ELEMENT_NODE:
|
||||
elementMatchers.push(matcher);
|
||||
break;
|
||||
default:
|
||||
[].forEach.call(this.container.querySelectorAll(selector), (node) => {
|
||||
// TODO use weakmap
|
||||
node[DOM_KEY] = node[DOM_KEY] || [];
|
||||
node[DOM_KEY].push(matcher);
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
return [elementMatchers, textMatchers];
|
||||
}
|
||||
}
|
||||
Clipboard.DEFAULTS = {
|
||||
matchers: [],
|
||||
matchVisual: true
|
||||
};
|
||||
|
||||
|
||||
function applyFormat(delta, format, value) {
|
||||
if (typeof format === 'object') {
|
||||
return Object.keys(format).reduce(function(delta, key) {
|
||||
return applyFormat(delta, key, format[key]);
|
||||
}, delta);
|
||||
} else {
|
||||
return delta.reduce(function(delta, op) {
|
||||
if (op.attributes && op.attributes[format]) {
|
||||
return delta.push(op);
|
||||
} else {
|
||||
return delta.insert(op.insert, extend({}, {[format]: value}, op.attributes));
|
||||
}
|
||||
}, new Delta());
|
||||
}
|
||||
}
|
||||
|
||||
function computeStyle(node) {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return {};
|
||||
const DOM_KEY = '__ql-computed-style';
|
||||
return node[DOM_KEY] || (node[DOM_KEY] = window.getComputedStyle(node));
|
||||
}
|
||||
|
||||
function deltaEndsWith(delta, text) {
|
||||
let endText = "";
|
||||
for (let i = delta.ops.length - 1; i >= 0 && endText.length < text.length; --i) {
|
||||
let op = delta.ops[i];
|
||||
if (typeof op.insert !== 'string') break;
|
||||
endText = op.insert + endText;
|
||||
}
|
||||
return endText.slice(-1*text.length) === text;
|
||||
}
|
||||
|
||||
function isLine(node) {
|
||||
if (node.childNodes.length === 0) return false; // Exclude embed blocks
|
||||
let style = computeStyle(node);
|
||||
return ['block', 'list-item'].indexOf(style.display) > -1;
|
||||
}
|
||||
|
||||
function traverse(node, elementMatchers, textMatchers) { // Post-order
|
||||
if (node.nodeType === node.TEXT_NODE) {
|
||||
return textMatchers.reduce(function(delta, matcher) {
|
||||
return matcher(node, delta);
|
||||
}, new Delta());
|
||||
} else if (node.nodeType === node.ELEMENT_NODE) {
|
||||
return [].reduce.call(node.childNodes || [], (delta, childNode) => {
|
||||
let childrenDelta = traverse(childNode, elementMatchers, textMatchers);
|
||||
if (childNode.nodeType === node.ELEMENT_NODE) {
|
||||
childrenDelta = elementMatchers.reduce(function(childrenDelta, matcher) {
|
||||
return matcher(childNode, childrenDelta);
|
||||
}, childrenDelta);
|
||||
childrenDelta = (childNode[DOM_KEY] || []).reduce(function(childrenDelta, matcher) {
|
||||
return matcher(childNode, childrenDelta);
|
||||
}, childrenDelta);
|
||||
}
|
||||
return delta.concat(childrenDelta);
|
||||
}, new Delta());
|
||||
} else {
|
||||
return new Delta();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function matchAlias(format, node, delta) {
|
||||
return applyFormat(delta, format, true);
|
||||
}
|
||||
|
||||
function matchAttributor(node, delta) {
|
||||
let attributes = Parchment.Attributor.Attribute.keys(node);
|
||||
let classes = Parchment.Attributor.Class.keys(node);
|
||||
let styles = Parchment.Attributor.Style.keys(node);
|
||||
let formats = {};
|
||||
attributes.concat(classes).concat(styles).forEach((name) => {
|
||||
let attr = Parchment.query(name, Parchment.Scope.ATTRIBUTE);
|
||||
if (attr != null) {
|
||||
formats[attr.attrName] = attr.value(node);
|
||||
if (formats[attr.attrName]) return;
|
||||
}
|
||||
attr = ATTRIBUTE_ATTRIBUTORS[name];
|
||||
if (attr != null && (attr.attrName === name || attr.keyName === name)) {
|
||||
formats[attr.attrName] = attr.value(node) || undefined;
|
||||
}
|
||||
attr = STYLE_ATTRIBUTORS[name]
|
||||
if (attr != null && (attr.attrName === name || attr.keyName === name)) {
|
||||
attr = STYLE_ATTRIBUTORS[name];
|
||||
formats[attr.attrName] = attr.value(node) || undefined;
|
||||
}
|
||||
});
|
||||
if (Object.keys(formats).length > 0) {
|
||||
delta = applyFormat(delta, formats);
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
function matchBlot(node, delta) {
|
||||
let match = Parchment.query(node);
|
||||
if (match == null) return delta;
|
||||
if (match.prototype instanceof Parchment.Embed) {
|
||||
let embed = {};
|
||||
let value = match.value(node);
|
||||
if (value != null) {
|
||||
embed[match.blotName] = value;
|
||||
delta = new Delta().insert(embed, match.formats(node));
|
||||
}
|
||||
} else if (typeof match.formats === 'function') {
|
||||
delta = applyFormat(delta, match.blotName, match.formats(node));
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
function matchBreak(node, delta) {
|
||||
if (!deltaEndsWith(delta, '\n')) {
|
||||
delta.insert('\n');
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
function matchIgnore() {
|
||||
return new Delta();
|
||||
}
|
||||
|
||||
function matchIndent(node, delta) {
|
||||
let match = Parchment.query(node);
|
||||
if (match == null || match.blotName !== 'list-item' || !deltaEndsWith(delta, '\n')) {
|
||||
return delta;
|
||||
}
|
||||
let indent = -1, parent = node.parentNode;
|
||||
while (!parent.classList.contains('ql-clipboard')) {
|
||||
if ((Parchment.query(parent) || {}).blotName === 'list') {
|
||||
indent += 1;
|
||||
}
|
||||
parent = parent.parentNode;
|
||||
}
|
||||
if (indent <= 0) return delta;
|
||||
return delta.compose(new Delta().retain(delta.length() - 1).retain(1, { indent: indent}));
|
||||
}
|
||||
|
||||
function matchNewline(node, delta) {
|
||||
if (!deltaEndsWith(delta, '\n')) {
|
||||
if (isLine(node) || (delta.length() > 0 && node.nextSibling && isLine(node.nextSibling))) {
|
||||
delta.insert('\n');
|
||||
}
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
function matchSpacing(node, delta) {
|
||||
if (isLine(node) && node.nextElementSibling != null && !deltaEndsWith(delta, '\n\n')) {
|
||||
let nodeHeight = node.offsetHeight + parseFloat(computeStyle(node).marginTop) + parseFloat(computeStyle(node).marginBottom);
|
||||
if (node.nextElementSibling.offsetTop > node.offsetTop + nodeHeight*1.5) {
|
||||
delta.insert('\n');
|
||||
}
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
function matchStyles(node, delta) {
|
||||
let formats = {};
|
||||
let style = node.style || {};
|
||||
if (style.fontStyle && computeStyle(node).fontStyle === 'italic') {
|
||||
formats.italic = true;
|
||||
}
|
||||
if (style.fontWeight && (computeStyle(node).fontWeight.startsWith('bold') ||
|
||||
parseInt(computeStyle(node).fontWeight) >= 700)) {
|
||||
formats.bold = true;
|
||||
}
|
||||
if (Object.keys(formats).length > 0) {
|
||||
delta = applyFormat(delta, formats);
|
||||
}
|
||||
if (parseFloat(style.textIndent || 0) > 0) { // Could be 0.5in
|
||||
delta = new Delta().insert('\t').concat(delta);
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
function matchText(node, delta) {
|
||||
let text = node.data;
|
||||
// Word represents empty line with <o:p> </o:p>
|
||||
if (node.parentNode.tagName === 'O:P') {
|
||||
return delta.insert(text.trim());
|
||||
}
|
||||
if (text.trim().length === 0 && node.parentNode.classList.contains('ql-clipboard')) {
|
||||
return delta;
|
||||
}
|
||||
if (!computeStyle(node.parentNode).whiteSpace.startsWith('pre')) {
|
||||
// eslint-disable-next-line func-style
|
||||
let replacer = function(collapse, match) {
|
||||
match = match.replace(/[^\u00a0]/g, ''); // \u00a0 is nbsp;
|
||||
return match.length < 1 && collapse ? ' ' : match;
|
||||
};
|
||||
text = text.replace(/\r\n/g, ' ').replace(/\n/g, ' ');
|
||||
text = text.replace(/\s\s+/g, replacer.bind(replacer, true)); // collapse whitespace
|
||||
if ((node.previousSibling == null && isLine(node.parentNode)) ||
|
||||
(node.previousSibling != null && isLine(node.previousSibling))) {
|
||||
text = text.replace(/^\s+/, replacer.bind(replacer, false));
|
||||
}
|
||||
if ((node.nextSibling == null && isLine(node.parentNode)) ||
|
||||
(node.nextSibling != null && isLine(node.nextSibling))) {
|
||||
text = text.replace(/\s+$/, replacer.bind(replacer, false));
|
||||
}
|
||||
}
|
||||
return delta.insert(text);
|
||||
}
|
||||
|
||||
|
||||
export { Clipboard as default, matchAttributor, matchBlot, matchNewline, matchSpacing, matchText };
|
42
assets/js/modules/formula.js
Normal file
42
assets/js/modules/formula.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import Embed from '../blots/embed';
|
||||
import Quill from '../core/quill';
|
||||
import Module from '../core/module';
|
||||
|
||||
|
||||
class FormulaBlot extends Embed {
|
||||
static create(value) {
|
||||
let node = super.create(value);
|
||||
if (typeof value === 'string') {
|
||||
window.katex.render(value, node, {
|
||||
throwOnError: false,
|
||||
errorColor: '#f00'
|
||||
});
|
||||
node.setAttribute('data-value', value);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
static value(domNode) {
|
||||
return domNode.getAttribute('data-value');
|
||||
}
|
||||
}
|
||||
FormulaBlot.blotName = 'formula';
|
||||
FormulaBlot.className = 'ql-formula';
|
||||
FormulaBlot.tagName = 'SPAN';
|
||||
|
||||
|
||||
class Formula extends Module {
|
||||
static register() {
|
||||
Quill.register(FormulaBlot, true);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
if (window.katex == null) {
|
||||
throw new Error('Formula module requires KaTeX.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export { FormulaBlot, Formula as default };
|
120
assets/js/modules/history.js
Normal file
120
assets/js/modules/history.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import Parchment from 'parchment';
|
||||
import Quill from '../core/quill';
|
||||
import Module from '../core/module';
|
||||
|
||||
|
||||
class History extends Module {
|
||||
constructor(quill, options) {
|
||||
super(quill, options);
|
||||
this.lastRecorded = 0;
|
||||
this.ignoreChange = false;
|
||||
this.clear();
|
||||
this.quill.on(Quill.events.EDITOR_CHANGE, (eventName, delta, oldDelta, source) => {
|
||||
if (eventName !== Quill.events.TEXT_CHANGE || this.ignoreChange) return;
|
||||
if (!this.options.userOnly || source === Quill.sources.USER) {
|
||||
this.record(delta, oldDelta);
|
||||
} else {
|
||||
this.transform(delta);
|
||||
}
|
||||
});
|
||||
this.quill.keyboard.addBinding({ key: 'Z', shortKey: true }, this.undo.bind(this));
|
||||
this.quill.keyboard.addBinding({ key: 'Z', shortKey: true, shiftKey: true }, this.redo.bind(this));
|
||||
if (/Win/i.test(navigator.platform)) {
|
||||
this.quill.keyboard.addBinding({ key: 'Y', shortKey: true }, this.redo.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
change(source, dest) {
|
||||
if (this.stack[source].length === 0) return;
|
||||
let delta = this.stack[source].pop();
|
||||
this.stack[dest].push(delta);
|
||||
this.lastRecorded = 0;
|
||||
this.ignoreChange = true;
|
||||
this.quill.updateContents(delta[source], Quill.sources.USER);
|
||||
this.ignoreChange = false;
|
||||
let index = getLastChangeIndex(delta[source]);
|
||||
this.quill.setSelection(index);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.stack = { undo: [], redo: [] };
|
||||
}
|
||||
|
||||
cutoff() {
|
||||
this.lastRecorded = 0;
|
||||
}
|
||||
|
||||
record(changeDelta, oldDelta) {
|
||||
if (changeDelta.ops.length === 0) return;
|
||||
this.stack.redo = [];
|
||||
let undoDelta = this.quill.getContents().diff(oldDelta);
|
||||
let timestamp = Date.now();
|
||||
if (this.lastRecorded + this.options.delay > timestamp && this.stack.undo.length > 0) {
|
||||
let delta = this.stack.undo.pop();
|
||||
undoDelta = undoDelta.compose(delta.undo);
|
||||
changeDelta = delta.redo.compose(changeDelta);
|
||||
} else {
|
||||
this.lastRecorded = timestamp;
|
||||
}
|
||||
this.stack.undo.push({
|
||||
redo: changeDelta,
|
||||
undo: undoDelta
|
||||
});
|
||||
if (this.stack.undo.length > this.options.maxStack) {
|
||||
this.stack.undo.shift();
|
||||
}
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.change('redo', 'undo');
|
||||
}
|
||||
|
||||
transform(delta) {
|
||||
this.stack.undo.forEach(function(change) {
|
||||
change.undo = delta.transform(change.undo, true);
|
||||
change.redo = delta.transform(change.redo, true);
|
||||
});
|
||||
this.stack.redo.forEach(function(change) {
|
||||
change.undo = delta.transform(change.undo, true);
|
||||
change.redo = delta.transform(change.redo, true);
|
||||
});
|
||||
}
|
||||
|
||||
undo() {
|
||||
this.change('undo', 'redo');
|
||||
}
|
||||
}
|
||||
History.DEFAULTS = {
|
||||
delay: 1000,
|
||||
maxStack: 100,
|
||||
userOnly: false
|
||||
};
|
||||
|
||||
function endsWithNewlineChange(delta) {
|
||||
let lastOp = delta.ops[delta.ops.length - 1];
|
||||
if (lastOp == null) return false;
|
||||
if (lastOp.insert != null) {
|
||||
return typeof lastOp.insert === 'string' && lastOp.insert.endsWith('\n');
|
||||
}
|
||||
if (lastOp.attributes != null) {
|
||||
return Object.keys(lastOp.attributes).some(function(attr) {
|
||||
return Parchment.query(attr, Parchment.Scope.BLOCK) != null;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getLastChangeIndex(delta) {
|
||||
let deleteLength = delta.reduce(function(length, op) {
|
||||
length += (op.delete || 0);
|
||||
return length;
|
||||
}, 0);
|
||||
let changeIndex = delta.length() - deleteLength;
|
||||
if (endsWithNewlineChange(delta)) {
|
||||
changeIndex -= 1;
|
||||
}
|
||||
return changeIndex;
|
||||
}
|
||||
|
||||
|
||||
export { History as default, getLastChangeIndex };
|
494
assets/js/modules/keyboard.js
Normal file
494
assets/js/modules/keyboard.js
Normal file
@@ -0,0 +1,494 @@
|
||||
import clone from 'clone';
|
||||
import equal from 'deep-equal';
|
||||
import extend from 'extend';
|
||||
import Delta from 'quill-delta';
|
||||
import DeltaOp from 'quill-delta/lib/op';
|
||||
import Parchment from 'parchment';
|
||||
import Quill from '../core/quill';
|
||||
import logger from '../core/logger';
|
||||
import Module from '../core/module';
|
||||
|
||||
let debug = logger('quill:keyboard');
|
||||
|
||||
const SHORTKEY = /Mac/i.test(navigator.platform) ? 'metaKey' : 'ctrlKey';
|
||||
|
||||
|
||||
class Keyboard extends Module {
|
||||
static match(evt, binding) {
|
||||
binding = normalize(binding);
|
||||
if (['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].some(function(key) {
|
||||
return (!!binding[key] !== evt[key] && binding[key] !== null);
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
return binding.key === (evt.which || evt.keyCode);
|
||||
}
|
||||
|
||||
constructor(quill, options) {
|
||||
super(quill, options);
|
||||
this.bindings = {};
|
||||
Object.keys(this.options.bindings).forEach((name) => {
|
||||
if (name === 'list autofill' &&
|
||||
quill.scroll.whitelist != null &&
|
||||
!quill.scroll.whitelist['list']) {
|
||||
return;
|
||||
}
|
||||
if (this.options.bindings[name]) {
|
||||
this.addBinding(this.options.bindings[name]);
|
||||
}
|
||||
});
|
||||
this.addBinding({ key: Keyboard.keys.ENTER, shiftKey: null }, handleEnter);
|
||||
this.addBinding({ key: Keyboard.keys.ENTER, metaKey: null, ctrlKey: null, altKey: null }, function() {});
|
||||
if (/Firefox/i.test(navigator.userAgent)) {
|
||||
// Need to handle delete and backspace for Firefox in the general case #1171
|
||||
this.addBinding({ key: Keyboard.keys.BACKSPACE }, { collapsed: true }, handleBackspace);
|
||||
this.addBinding({ key: Keyboard.keys.DELETE }, { collapsed: true }, handleDelete);
|
||||
} else {
|
||||
this.addBinding({ key: Keyboard.keys.BACKSPACE }, { collapsed: true, prefix: /^.?$/ }, handleBackspace);
|
||||
this.addBinding({ key: Keyboard.keys.DELETE }, { collapsed: true, suffix: /^.?$/ }, handleDelete);
|
||||
}
|
||||
this.addBinding({ key: Keyboard.keys.BACKSPACE }, { collapsed: false }, handleDeleteRange);
|
||||
this.addBinding({ key: Keyboard.keys.DELETE }, { collapsed: false }, handleDeleteRange);
|
||||
this.addBinding({ key: Keyboard.keys.BACKSPACE, altKey: null, ctrlKey: null, metaKey: null, shiftKey: null },
|
||||
{ collapsed: true, offset: 0 },
|
||||
handleBackspace);
|
||||
this.listen();
|
||||
}
|
||||
|
||||
addBinding(key, context = {}, handler = {}) {
|
||||
let binding = normalize(key);
|
||||
if (binding == null || binding.key == null) {
|
||||
return debug.warn('Attempted to add invalid keyboard binding', binding);
|
||||
}
|
||||
if (typeof context === 'function') {
|
||||
context = { handler: context };
|
||||
}
|
||||
if (typeof handler === 'function') {
|
||||
handler = { handler: handler };
|
||||
}
|
||||
binding = extend(binding, context, handler);
|
||||
this.bindings[binding.key] = this.bindings[binding.key] || [];
|
||||
this.bindings[binding.key].push(binding);
|
||||
}
|
||||
|
||||
listen() {
|
||||
this.quill.root.addEventListener('keydown', (evt) => {
|
||||
if (evt.defaultPrevented) return;
|
||||
let which = evt.which || evt.keyCode;
|
||||
let bindings = (this.bindings[which] || []).filter(function(binding) {
|
||||
return Keyboard.match(evt, binding);
|
||||
});
|
||||
if (bindings.length === 0) return;
|
||||
let range = this.quill.getSelection();
|
||||
if (range == null || !this.quill.hasFocus()) return;
|
||||
let [line, offset] = this.quill.getLine(range.index);
|
||||
let [leafStart, offsetStart] = this.quill.getLeaf(range.index);
|
||||
let [leafEnd, offsetEnd] = range.length === 0 ? [leafStart, offsetStart] : this.quill.getLeaf(range.index + range.length);
|
||||
let prefixText = leafStart instanceof Parchment.Text ? leafStart.value().slice(0, offsetStart) : '';
|
||||
let suffixText = leafEnd instanceof Parchment.Text ? leafEnd.value().slice(offsetEnd) : '';
|
||||
let curContext = {
|
||||
collapsed: range.length === 0,
|
||||
empty: range.length === 0 && line.length() <= 1,
|
||||
format: this.quill.getFormat(range),
|
||||
offset: offset,
|
||||
prefix: prefixText,
|
||||
suffix: suffixText
|
||||
};
|
||||
let prevented = bindings.some((binding) => {
|
||||
if (binding.collapsed != null && binding.collapsed !== curContext.collapsed) return false;
|
||||
if (binding.empty != null && binding.empty !== curContext.empty) return false;
|
||||
if (binding.offset != null && binding.offset !== curContext.offset) return false;
|
||||
if (Array.isArray(binding.format)) {
|
||||
// any format is present
|
||||
if (binding.format.every(function(name) {
|
||||
return curContext.format[name] == null;
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
} else if (typeof binding.format === 'object') {
|
||||
// all formats must match
|
||||
if (!Object.keys(binding.format).every(function(name) {
|
||||
if (binding.format[name] === true) return curContext.format[name] != null;
|
||||
if (binding.format[name] === false) return curContext.format[name] == null;
|
||||
return equal(binding.format[name], curContext.format[name]);
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (binding.prefix != null && !binding.prefix.test(curContext.prefix)) return false;
|
||||
if (binding.suffix != null && !binding.suffix.test(curContext.suffix)) return false;
|
||||
return binding.handler.call(this, range, curContext) !== true;
|
||||
});
|
||||
if (prevented) {
|
||||
evt.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Keyboard.keys = {
|
||||
BACKSPACE: 8,
|
||||
TAB: 9,
|
||||
ENTER: 13,
|
||||
ESCAPE: 27,
|
||||
LEFT: 37,
|
||||
UP: 38,
|
||||
RIGHT: 39,
|
||||
DOWN: 40,
|
||||
DELETE: 46
|
||||
};
|
||||
|
||||
Keyboard.DEFAULTS = {
|
||||
bindings: {
|
||||
'bold' : makeFormatHandler('bold'),
|
||||
'italic' : makeFormatHandler('italic'),
|
||||
'underline' : makeFormatHandler('underline'),
|
||||
'indent': {
|
||||
// highlight tab or tab at beginning of list, indent or blockquote
|
||||
key: Keyboard.keys.TAB,
|
||||
format: ['blockquote', 'indent', 'list'],
|
||||
handler: function(range, context) {
|
||||
if (context.collapsed && context.offset !== 0) return true;
|
||||
this.quill.format('indent', '+1', Quill.sources.USER);
|
||||
}
|
||||
},
|
||||
'outdent': {
|
||||
key: Keyboard.keys.TAB,
|
||||
shiftKey: true,
|
||||
format: ['blockquote', 'indent', 'list'],
|
||||
// highlight tab or tab at beginning of list, indent or blockquote
|
||||
handler: function(range, context) {
|
||||
if (context.collapsed && context.offset !== 0) return true;
|
||||
this.quill.format('indent', '-1', Quill.sources.USER);
|
||||
}
|
||||
},
|
||||
'outdent backspace': {
|
||||
key: Keyboard.keys.BACKSPACE,
|
||||
collapsed: true,
|
||||
shiftKey: null,
|
||||
metaKey: null,
|
||||
ctrlKey: null,
|
||||
altKey: null,
|
||||
format: ['indent', 'list'],
|
||||
offset: 0,
|
||||
handler: function(range, context) {
|
||||
if (context.format.indent != null) {
|
||||
this.quill.format('indent', '-1', Quill.sources.USER);
|
||||
} else if (context.format.list != null) {
|
||||
this.quill.format('list', false, Quill.sources.USER);
|
||||
}
|
||||
}
|
||||
},
|
||||
'indent code-block': makeCodeBlockHandler(true),
|
||||
'outdent code-block': makeCodeBlockHandler(false),
|
||||
'remove tab': {
|
||||
key: Keyboard.keys.TAB,
|
||||
shiftKey: true,
|
||||
collapsed: true,
|
||||
prefix: /\t$/,
|
||||
handler: function(range) {
|
||||
this.quill.deleteText(range.index - 1, 1, Quill.sources.USER);
|
||||
}
|
||||
},
|
||||
'tab': {
|
||||
key: Keyboard.keys.TAB,
|
||||
handler: function(range) {
|
||||
this.quill.history.cutoff();
|
||||
let delta = new Delta().retain(range.index)
|
||||
.delete(range.length)
|
||||
.insert('\t');
|
||||
this.quill.updateContents(delta, Quill.sources.USER);
|
||||
this.quill.history.cutoff();
|
||||
this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
|
||||
}
|
||||
},
|
||||
'list empty enter': {
|
||||
key: Keyboard.keys.ENTER,
|
||||
collapsed: true,
|
||||
format: ['list'],
|
||||
empty: true,
|
||||
handler: function(range, context) {
|
||||
this.quill.format('list', false, Quill.sources.USER);
|
||||
if (context.format.indent) {
|
||||
this.quill.format('indent', false, Quill.sources.USER);
|
||||
}
|
||||
}
|
||||
},
|
||||
'checklist enter': {
|
||||
key: Keyboard.keys.ENTER,
|
||||
collapsed: true,
|
||||
format: { list: 'checked' },
|
||||
handler: function(range) {
|
||||
let [line, offset] = this.quill.getLine(range.index);
|
||||
let formats = extend({}, line.formats(), { list: 'checked' });
|
||||
let delta = new Delta().retain(range.index)
|
||||
.insert('\n', formats)
|
||||
.retain(line.length() - offset - 1)
|
||||
.retain(1, { list: 'unchecked' });
|
||||
this.quill.updateContents(delta, Quill.sources.USER);
|
||||
this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
|
||||
this.quill.scrollIntoView();
|
||||
}
|
||||
},
|
||||
'header enter': {
|
||||
key: Keyboard.keys.ENTER,
|
||||
collapsed: true,
|
||||
format: ['header'],
|
||||
suffix: /^$/,
|
||||
handler: function(range, context) {
|
||||
let [line, offset] = this.quill.getLine(range.index);
|
||||
let delta = new Delta().retain(range.index)
|
||||
.insert('\n', context.format)
|
||||
.retain(line.length() - offset - 1)
|
||||
.retain(1, { header: null });
|
||||
this.quill.updateContents(delta, Quill.sources.USER);
|
||||
this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
|
||||
this.quill.scrollIntoView();
|
||||
}
|
||||
},
|
||||
'list autofill': {
|
||||
key: ' ',
|
||||
collapsed: true,
|
||||
format: { list: false },
|
||||
prefix: /^\s*?(\d+\.|-|\*|\[ ?\]|\[x\])$/,
|
||||
handler: function(range, context) {
|
||||
let length = context.prefix.length;
|
||||
let [line, offset] = this.quill.getLine(range.index);
|
||||
if (offset > length) return true;
|
||||
let value;
|
||||
switch (context.prefix.trim()) {
|
||||
case '[]': case '[ ]':
|
||||
value = 'unchecked';
|
||||
break;
|
||||
case '[x]':
|
||||
value = 'checked';
|
||||
break;
|
||||
case '-': case '*':
|
||||
value = 'bullet';
|
||||
break;
|
||||
default:
|
||||
value = 'ordered';
|
||||
}
|
||||
this.quill.insertText(range.index, ' ', Quill.sources.USER);
|
||||
this.quill.history.cutoff();
|
||||
let delta = new Delta().retain(range.index - offset)
|
||||
.delete(length + 1)
|
||||
.retain(line.length() - 2 - offset)
|
||||
.retain(1, { list: value });
|
||||
this.quill.updateContents(delta, Quill.sources.USER);
|
||||
this.quill.history.cutoff();
|
||||
this.quill.setSelection(range.index - length, Quill.sources.SILENT);
|
||||
}
|
||||
},
|
||||
'code exit': {
|
||||
key: Keyboard.keys.ENTER,
|
||||
collapsed: true,
|
||||
format: ['code-block'],
|
||||
prefix: /\n\n$/,
|
||||
suffix: /^\s+$/,
|
||||
handler: function(range) {
|
||||
const [line, offset] = this.quill.getLine(range.index);
|
||||
const delta = new Delta()
|
||||
.retain(range.index + line.length() - offset - 2)
|
||||
.retain(1, { 'code-block': null })
|
||||
.delete(1);
|
||||
this.quill.updateContents(delta, Quill.sources.USER);
|
||||
}
|
||||
},
|
||||
'embed left': makeEmbedArrowHandler(Keyboard.keys.LEFT, false),
|
||||
'embed left shift': makeEmbedArrowHandler(Keyboard.keys.LEFT, true),
|
||||
'embed right': makeEmbedArrowHandler(Keyboard.keys.RIGHT, false),
|
||||
'embed right shift': makeEmbedArrowHandler(Keyboard.keys.RIGHT, true)
|
||||
}
|
||||
};
|
||||
|
||||
function makeEmbedArrowHandler(key, shiftKey) {
|
||||
const where = key === Keyboard.keys.LEFT ? 'prefix' : 'suffix';
|
||||
return {
|
||||
key,
|
||||
shiftKey,
|
||||
altKey: null,
|
||||
[where]: /^$/,
|
||||
handler: function(range) {
|
||||
let index = range.index;
|
||||
if (key === Keyboard.keys.RIGHT) {
|
||||
index += (range.length + 1);
|
||||
}
|
||||
const [leaf, ] = this.quill.getLeaf(index);
|
||||
if (!(leaf instanceof Parchment.Embed)) return true;
|
||||
if (key === Keyboard.keys.LEFT) {
|
||||
if (shiftKey) {
|
||||
this.quill.setSelection(range.index - 1, range.length + 1, Quill.sources.USER);
|
||||
} else {
|
||||
this.quill.setSelection(range.index - 1, Quill.sources.USER);
|
||||
}
|
||||
} else {
|
||||
if (shiftKey) {
|
||||
this.quill.setSelection(range.index, range.length + 1, Quill.sources.USER);
|
||||
} else {
|
||||
this.quill.setSelection(range.index + range.length + 1, Quill.sources.USER);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function handleBackspace(range, context) {
|
||||
if (range.index === 0 || this.quill.getLength() <= 1) return;
|
||||
let [line, ] = this.quill.getLine(range.index);
|
||||
let formats = {};
|
||||
if (context.offset === 0) {
|
||||
let [prev, ] = this.quill.getLine(range.index - 1);
|
||||
if (prev != null && prev.length() > 1) {
|
||||
let curFormats = line.formats();
|
||||
let prevFormats = this.quill.getFormat(range.index-1, 1);
|
||||
formats = DeltaOp.attributes.diff(curFormats, prevFormats) || {};
|
||||
}
|
||||
}
|
||||
// Check for astral symbols
|
||||
let length = /[\uD800-\uDBFF][\uDC00-\uDFFF]$/.test(context.prefix) ? 2 : 1;
|
||||
this.quill.deleteText(range.index-length, length, Quill.sources.USER);
|
||||
if (Object.keys(formats).length > 0) {
|
||||
this.quill.formatLine(range.index-length, length, formats, Quill.sources.USER);
|
||||
}
|
||||
this.quill.focus();
|
||||
}
|
||||
|
||||
function handleDelete(range, context) {
|
||||
// Check for astral symbols
|
||||
let length = /^[\uD800-\uDBFF][\uDC00-\uDFFF]/.test(context.suffix) ? 2 : 1;
|
||||
if (range.index >= this.quill.getLength() - length) return;
|
||||
let formats = {}, nextLength = 0;
|
||||
let [line, ] = this.quill.getLine(range.index);
|
||||
if (context.offset >= line.length() - 1) {
|
||||
let [next, ] = this.quill.getLine(range.index + 1);
|
||||
if (next) {
|
||||
let curFormats = line.formats();
|
||||
let nextFormats = this.quill.getFormat(range.index, 1);
|
||||
formats = DeltaOp.attributes.diff(curFormats, nextFormats) || {};
|
||||
nextLength = next.length();
|
||||
}
|
||||
}
|
||||
this.quill.deleteText(range.index, length, Quill.sources.USER);
|
||||
if (Object.keys(formats).length > 0) {
|
||||
this.quill.formatLine(range.index + nextLength - 1, length, formats, Quill.sources.USER);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteRange(range) {
|
||||
let lines = this.quill.getLines(range);
|
||||
let formats = {};
|
||||
if (lines.length > 1) {
|
||||
let firstFormats = lines[0].formats();
|
||||
let lastFormats = lines[lines.length - 1].formats();
|
||||
formats = DeltaOp.attributes.diff(lastFormats, firstFormats) || {};
|
||||
}
|
||||
this.quill.deleteText(range, Quill.sources.USER);
|
||||
if (Object.keys(formats).length > 0) {
|
||||
this.quill.formatLine(range.index, 1, formats, Quill.sources.USER);
|
||||
}
|
||||
this.quill.setSelection(range.index, Quill.sources.SILENT);
|
||||
this.quill.focus();
|
||||
}
|
||||
|
||||
function handleEnter(range, context) {
|
||||
if (range.length > 0) {
|
||||
this.quill.scroll.deleteAt(range.index, range.length); // So we do not trigger text-change
|
||||
}
|
||||
let lineFormats = Object.keys(context.format).reduce(function(lineFormats, format) {
|
||||
if (Parchment.query(format, Parchment.Scope.BLOCK) && !Array.isArray(context.format[format])) {
|
||||
lineFormats[format] = context.format[format];
|
||||
}
|
||||
return lineFormats;
|
||||
}, {});
|
||||
this.quill.insertText(range.index, '\n', lineFormats, Quill.sources.USER);
|
||||
// Earlier scroll.deleteAt might have messed up our selection,
|
||||
// so insertText's built in selection preservation is not reliable
|
||||
this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
|
||||
this.quill.focus();
|
||||
Object.keys(context.format).forEach((name) => {
|
||||
if (lineFormats[name] != null) return;
|
||||
if (Array.isArray(context.format[name])) return;
|
||||
if (name === 'link') return;
|
||||
this.quill.format(name, context.format[name], Quill.sources.USER);
|
||||
});
|
||||
}
|
||||
|
||||
function makeCodeBlockHandler(indent) {
|
||||
return {
|
||||
key: Keyboard.keys.TAB,
|
||||
shiftKey: !indent,
|
||||
format: {'code-block': true },
|
||||
handler: function(range) {
|
||||
let CodeBlock = Parchment.query('code-block');
|
||||
let index = range.index, length = range.length;
|
||||
let [block, offset] = this.quill.scroll.descendant(CodeBlock, index);
|
||||
if (block == null) return;
|
||||
let scrollIndex = this.quill.getIndex(block);
|
||||
let start = block.newlineIndex(offset, true) + 1;
|
||||
let end = block.newlineIndex(scrollIndex + offset + length);
|
||||
let lines = block.domNode.textContent.slice(start, end).split('\n');
|
||||
offset = 0;
|
||||
lines.forEach((line, i) => {
|
||||
if (indent) {
|
||||
block.insertAt(start + offset, CodeBlock.TAB);
|
||||
offset += CodeBlock.TAB.length;
|
||||
if (i === 0) {
|
||||
index += CodeBlock.TAB.length;
|
||||
} else {
|
||||
length += CodeBlock.TAB.length;
|
||||
}
|
||||
} else if (line.startsWith(CodeBlock.TAB)) {
|
||||
block.deleteAt(start + offset, CodeBlock.TAB.length);
|
||||
offset -= CodeBlock.TAB.length;
|
||||
if (i === 0) {
|
||||
index -= CodeBlock.TAB.length;
|
||||
} else {
|
||||
length -= CodeBlock.TAB.length;
|
||||
}
|
||||
}
|
||||
offset += line.length + 1;
|
||||
});
|
||||
this.quill.update(Quill.sources.USER);
|
||||
this.quill.setSelection(index, length, Quill.sources.SILENT);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function makeFormatHandler(format) {
|
||||
return {
|
||||
key: format[0].toUpperCase(),
|
||||
shortKey: true,
|
||||
handler: function(range, context) {
|
||||
this.quill.format(format, !context.format[format], Quill.sources.USER);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function normalize(binding) {
|
||||
if (typeof binding === 'string' || typeof binding === 'number') {
|
||||
return normalize({ key: binding });
|
||||
}
|
||||
if (typeof binding === 'object') {
|
||||
binding = clone(binding, false);
|
||||
}
|
||||
if (typeof binding.key === 'string') {
|
||||
if (Keyboard.keys[binding.key.toUpperCase()] != null) {
|
||||
binding.key = Keyboard.keys[binding.key.toUpperCase()];
|
||||
} else if (binding.key.length === 1) {
|
||||
binding.key = binding.key.toUpperCase().charCodeAt(0);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (binding.shortKey) {
|
||||
binding[SHORTKEY] = binding.shortKey;
|
||||
delete binding.shortKey;
|
||||
}
|
||||
return binding;
|
||||
}
|
||||
|
||||
|
||||
export { Keyboard as default, SHORTKEY };
|
81
assets/js/modules/syntax.js
Normal file
81
assets/js/modules/syntax.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import Parchment from 'parchment';
|
||||
import Quill from '../core/quill';
|
||||
import Module from '../core/module';
|
||||
import CodeBlock from '../formats/code';
|
||||
|
||||
|
||||
class SyntaxCodeBlock extends CodeBlock {
|
||||
replaceWith(block) {
|
||||
this.domNode.textContent = this.domNode.textContent;
|
||||
this.attach();
|
||||
super.replaceWith(block);
|
||||
}
|
||||
|
||||
highlight(highlight) {
|
||||
let text = this.domNode.textContent;
|
||||
if (this.cachedText !== text) {
|
||||
if (text.trim().length > 0 || this.cachedText == null) {
|
||||
this.domNode.innerHTML = highlight(text);
|
||||
this.domNode.normalize();
|
||||
this.attach();
|
||||
}
|
||||
this.cachedText = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
SyntaxCodeBlock.className = 'ql-syntax';
|
||||
|
||||
|
||||
let CodeToken = new Parchment.Attributor.Class('token', 'hljs', {
|
||||
scope: Parchment.Scope.INLINE
|
||||
});
|
||||
|
||||
|
||||
class Syntax extends Module {
|
||||
static register() {
|
||||
Quill.register(CodeToken, true);
|
||||
Quill.register(SyntaxCodeBlock, true);
|
||||
}
|
||||
|
||||
constructor(quill, options) {
|
||||
super(quill, options);
|
||||
if (typeof this.options.highlight !== 'function') {
|
||||
throw new Error('Syntax module requires highlight.js. Please include the library on the page before Quill.');
|
||||
}
|
||||
let timer = null;
|
||||
this.quill.on(Quill.events.SCROLL_OPTIMIZE, () => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
this.highlight();
|
||||
timer = null;
|
||||
}, this.options.interval);
|
||||
});
|
||||
this.highlight();
|
||||
}
|
||||
|
||||
highlight() {
|
||||
if (this.quill.selection.composing) return;
|
||||
this.quill.update(Quill.sources.USER);
|
||||
let range = this.quill.getSelection();
|
||||
this.quill.scroll.descendants(SyntaxCodeBlock).forEach((code) => {
|
||||
code.highlight(this.options.highlight);
|
||||
});
|
||||
this.quill.update(Quill.sources.SILENT);
|
||||
if (range != null) {
|
||||
this.quill.setSelection(range, Quill.sources.SILENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
Syntax.DEFAULTS = {
|
||||
highlight: (function() {
|
||||
if (window.hljs == null) return null;
|
||||
return function(text) {
|
||||
let result = window.hljs.highlightAuto(text);
|
||||
return result.value;
|
||||
};
|
||||
})(),
|
||||
interval: 1000
|
||||
};
|
||||
|
||||
|
||||
export { SyntaxCodeBlock as CodeBlock, CodeToken, Syntax as default};
|
261
assets/js/modules/toolbar.js
Normal file
261
assets/js/modules/toolbar.js
Normal file
@@ -0,0 +1,261 @@
|
||||
import Delta from 'quill-delta';
|
||||
import Parchment from 'parchment';
|
||||
import Quill from '../core/quill';
|
||||
import logger from '../core/logger';
|
||||
import Module from '../core/module';
|
||||
|
||||
let debug = logger('quill:toolbar');
|
||||
|
||||
|
||||
class Toolbar extends Module {
|
||||
constructor(quill, options) {
|
||||
super(quill, options);
|
||||
if (Array.isArray(this.options.container)) {
|
||||
let container = document.createElement('div');
|
||||
addControls(container, this.options.container);
|
||||
quill.container.parentNode.insertBefore(container, quill.container);
|
||||
this.container = container;
|
||||
} else if (typeof this.options.container === 'string') {
|
||||
this.container = document.querySelector(this.options.container);
|
||||
} else {
|
||||
this.container = this.options.container;
|
||||
}
|
||||
if (!(this.container instanceof HTMLElement)) {
|
||||
return debug.error('Container required for toolbar', this.options);
|
||||
}
|
||||
this.container.classList.add('ql-toolbar');
|
||||
this.controls = [];
|
||||
this.handlers = {};
|
||||
Object.keys(this.options.handlers).forEach((format) => {
|
||||
this.addHandler(format, this.options.handlers[format]);
|
||||
});
|
||||
[].forEach.call(this.container.querySelectorAll('button, select'), (input) => {
|
||||
this.attach(input);
|
||||
});
|
||||
this.quill.on(Quill.events.EDITOR_CHANGE, (type, range) => {
|
||||
if (type === Quill.events.SELECTION_CHANGE) {
|
||||
this.update(range);
|
||||
}
|
||||
});
|
||||
this.quill.on(Quill.events.SCROLL_OPTIMIZE, () => {
|
||||
let [range, ] = this.quill.selection.getRange(); // quill.getSelection triggers update
|
||||
this.update(range);
|
||||
});
|
||||
}
|
||||
|
||||
addHandler(format, handler) {
|
||||
this.handlers[format] = handler;
|
||||
}
|
||||
|
||||
attach(input) {
|
||||
let format = [].find.call(input.classList, (className) => {
|
||||
return className.indexOf('ql-') === 0;
|
||||
});
|
||||
if (!format) return;
|
||||
format = format.slice('ql-'.length);
|
||||
if (input.tagName === 'BUTTON') {
|
||||
input.setAttribute('type', 'button');
|
||||
}
|
||||
if (this.handlers[format] == null) {
|
||||
if (this.quill.scroll.whitelist != null && this.quill.scroll.whitelist[format] == null) {
|
||||
debug.warn('ignoring attaching to disabled format', format, input);
|
||||
return;
|
||||
}
|
||||
if (Parchment.query(format) == null) {
|
||||
debug.warn('ignoring attaching to nonexistent format', format, input);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let eventName = input.tagName === 'SELECT' ? 'change' : 'click';
|
||||
input.addEventListener(eventName, (e) => {
|
||||
let value;
|
||||
if (input.tagName === 'SELECT') {
|
||||
if (input.selectedIndex < 0) return;
|
||||
let selected = input.options[input.selectedIndex];
|
||||
if (selected.hasAttribute('selected')) {
|
||||
value = false;
|
||||
} else {
|
||||
value = selected.value || false;
|
||||
}
|
||||
} else {
|
||||
if (input.classList.contains('ql-active')) {
|
||||
value = false;
|
||||
} else {
|
||||
value = input.value || !input.hasAttribute('value');
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
this.quill.focus();
|
||||
let [range, ] = this.quill.selection.getRange();
|
||||
if (this.handlers[format] != null) {
|
||||
this.handlers[format].call(this, value);
|
||||
} else if (Parchment.query(format).prototype instanceof Parchment.Embed) {
|
||||
value = prompt(`Enter ${format}`);
|
||||
if (!value) return;
|
||||
this.quill.updateContents(new Delta()
|
||||
.retain(range.index)
|
||||
.delete(range.length)
|
||||
.insert({ [format]: value })
|
||||
, Quill.sources.USER);
|
||||
} else {
|
||||
this.quill.format(format, value, Quill.sources.USER);
|
||||
}
|
||||
this.update(range);
|
||||
});
|
||||
// TODO use weakmap
|
||||
this.controls.push([format, input]);
|
||||
}
|
||||
|
||||
update(range) {
|
||||
let formats = range == null ? {} : this.quill.getFormat(range);
|
||||
this.controls.forEach(function(pair) {
|
||||
let [format, input] = pair;
|
||||
if (input.tagName === 'SELECT') {
|
||||
let option;
|
||||
if (range == null) {
|
||||
option = null;
|
||||
} else if (formats[format] == null) {
|
||||
option = input.querySelector('option[selected]');
|
||||
} else if (!Array.isArray(formats[format])) {
|
||||
let value = formats[format];
|
||||
if (typeof value === 'string') {
|
||||
value = value.replace(/\"/g, '\\"');
|
||||
}
|
||||
option = input.querySelector(`option[value="${value}"]`);
|
||||
}
|
||||
if (option == null) {
|
||||
input.value = ''; // TODO make configurable?
|
||||
input.selectedIndex = -1;
|
||||
} else {
|
||||
option.selected = true;
|
||||
}
|
||||
} else {
|
||||
if (range == null) {
|
||||
input.classList.remove('ql-active');
|
||||
} else if (input.hasAttribute('value')) {
|
||||
// both being null should match (default values)
|
||||
// '1' should match with 1 (headers)
|
||||
let isActive = formats[format] === input.getAttribute('value') ||
|
||||
(formats[format] != null && formats[format].toString() === input.getAttribute('value')) ||
|
||||
(formats[format] == null && !input.getAttribute('value'));
|
||||
input.classList.toggle('ql-active', isActive);
|
||||
} else {
|
||||
input.classList.toggle('ql-active', formats[format] != null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Toolbar.DEFAULTS = {};
|
||||
|
||||
|
||||
function addButton(container, format, value) {
|
||||
let input = document.createElement('button');
|
||||
input.setAttribute('type', 'button');
|
||||
input.classList.add('ql-' + format);
|
||||
if (value != null) {
|
||||
input.value = value;
|
||||
}
|
||||
container.appendChild(input);
|
||||
}
|
||||
|
||||
function addControls(container, groups) {
|
||||
if (!Array.isArray(groups[0])) {
|
||||
groups = [groups];
|
||||
}
|
||||
groups.forEach(function(controls) {
|
||||
let group = document.createElement('span');
|
||||
group.classList.add('ql-formats');
|
||||
controls.forEach(function(control) {
|
||||
if (typeof control === 'string') {
|
||||
addButton(group, control);
|
||||
} else {
|
||||
let format = Object.keys(control)[0];
|
||||
let value = control[format];
|
||||
if (Array.isArray(value)) {
|
||||
addSelect(group, format, value);
|
||||
} else {
|
||||
addButton(group, format, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
container.appendChild(group);
|
||||
});
|
||||
}
|
||||
|
||||
function addSelect(container, format, values) {
|
||||
let input = document.createElement('select');
|
||||
input.classList.add('ql-' + format);
|
||||
values.forEach(function(value) {
|
||||
let option = document.createElement('option');
|
||||
if (value !== false) {
|
||||
option.setAttribute('value', value);
|
||||
} else {
|
||||
option.setAttribute('selected', 'selected');
|
||||
}
|
||||
input.appendChild(option);
|
||||
});
|
||||
container.appendChild(input);
|
||||
}
|
||||
|
||||
Toolbar.DEFAULTS = {
|
||||
container: null,
|
||||
handlers: {
|
||||
clean: function() {
|
||||
let range = this.quill.getSelection();
|
||||
if (range == null) return;
|
||||
if (range.length == 0) {
|
||||
let formats = this.quill.getFormat();
|
||||
Object.keys(formats).forEach((name) => {
|
||||
// Clean functionality in existing apps only clean inline formats
|
||||
if (Parchment.query(name, Parchment.Scope.INLINE) != null) {
|
||||
this.quill.format(name, false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.quill.removeFormat(range, Quill.sources.USER);
|
||||
}
|
||||
},
|
||||
direction: function(value) {
|
||||
let align = this.quill.getFormat()['align'];
|
||||
if (value === 'rtl' && align == null) {
|
||||
this.quill.format('align', 'right', Quill.sources.USER);
|
||||
} else if (!value && align === 'right') {
|
||||
this.quill.format('align', false, Quill.sources.USER);
|
||||
}
|
||||
this.quill.format('direction', value, Quill.sources.USER);
|
||||
},
|
||||
indent: function(value) {
|
||||
let range = this.quill.getSelection();
|
||||
let formats = this.quill.getFormat(range);
|
||||
let indent = parseInt(formats.indent || 0);
|
||||
if (value === '+1' || value === '-1') {
|
||||
let modifier = (value === '+1') ? 1 : -1;
|
||||
if (formats.direction === 'rtl') modifier *= -1;
|
||||
this.quill.format('indent', indent + modifier, Quill.sources.USER);
|
||||
}
|
||||
},
|
||||
link: function(value) {
|
||||
if (value === true) {
|
||||
value = prompt('Enter link URL:');
|
||||
}
|
||||
this.quill.format('link', value, Quill.sources.USER);
|
||||
},
|
||||
list: function(value) {
|
||||
let range = this.quill.getSelection();
|
||||
let formats = this.quill.getFormat(range);
|
||||
if (value === 'check') {
|
||||
if (formats['list'] === 'checked' || formats['list'] === 'unchecked') {
|
||||
this.quill.format('list', false, Quill.sources.USER);
|
||||
} else {
|
||||
this.quill.format('list', 'unchecked', Quill.sources.USER);
|
||||
}
|
||||
} else {
|
||||
this.quill.format('list', value, Quill.sources.USER);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export { Toolbar as default, addControls };
|
Reference in New Issue
Block a user