Archived
1
0

Initial commit as of 2018-10-16

This commit is contained in:
Marcel
2018-10-16 18:28:42 +02:00
commit 29d7c2ffdc
3601 changed files with 358427 additions and 0 deletions

View 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>&nbsp;</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 };

View 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 };

View 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 };

View 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 };

View 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};

View 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 };