first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:33:39 +03:00
commit 4362c3b83f
1991 changed files with 285411 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
import type { ScrollBlot } from 'parchment';
import Delta from 'quill-delta';
import type { EmitterSource } from '../core/emitter.js';
import Module from '../core/module.js';
import Quill from '../core/quill.js';
import type { Range } from '../core/selection.js';
type Selector = string | Node['TEXT_NODE'] | Node['ELEMENT_NODE'];
type Matcher = (node: Node, delta: Delta, scroll: ScrollBlot) => Delta;
interface ClipboardOptions {
matchers: [Selector, Matcher][];
}
declare class Clipboard extends Module<ClipboardOptions> {
static DEFAULTS: ClipboardOptions;
matchers: [Selector, Matcher][];
constructor(quill: Quill, options: Partial<ClipboardOptions>);
addMatcher(selector: Selector, matcher: Matcher): void;
convert({ html, text }: {
html?: string;
text?: string;
}, formats?: Record<string, unknown>): Delta;
protected normalizeHTML(doc: Document): void;
protected convertHTML(html: string): Delta;
dangerouslyPasteHTML(html: string, source?: EmitterSource): void;
dangerouslyPasteHTML(index: number, html: string, source?: EmitterSource): void;
onCaptureCopy(e: ClipboardEvent, isCut?: boolean): void;
private normalizeURIList;
onCapturePaste(e: ClipboardEvent): void;
onCopy(range: Range, isCut: boolean): {
html: string;
text: string;
};
onPaste(range: Range, { text, html }: {
text?: string;
html?: string;
}): void;
prepareMatching(container: Element, nodeMatches: WeakMap<Node, Matcher[]>): Matcher[][];
}
declare function traverse(scroll: ScrollBlot, node: ChildNode, elementMatchers: Matcher[], textMatchers: Matcher[], nodeMatches: WeakMap<Node, Matcher[]>): Delta;
declare function matchAttributor(node: HTMLElement, delta: Delta, scroll: ScrollBlot): Delta;
declare function matchBlot(node: Node, delta: Delta, scroll: ScrollBlot): Delta;
declare function matchNewline(node: Node, delta: Delta, scroll: ScrollBlot): Delta;
declare function matchText(node: HTMLElement, delta: Delta, scroll: ScrollBlot): Delta;
export { Clipboard as default, matchAttributor, matchBlot, matchNewline, matchText, traverse, };

View File

@@ -0,0 +1,477 @@
import { Attributor, BlockBlot, ClassAttributor, EmbedBlot, Scope, StyleAttributor } from 'parchment';
import Delta from 'quill-delta';
import { BlockEmbed } from '../blots/block.js';
import logger from '../core/logger.js';
import Module from '../core/module.js';
import Quill from '../core/quill.js';
import { AlignAttribute, AlignStyle } from '../formats/align.js';
import { BackgroundStyle } from '../formats/background.js';
import CodeBlock from '../formats/code.js';
import { ColorStyle } from '../formats/color.js';
import { DirectionAttribute, DirectionStyle } from '../formats/direction.js';
import { FontStyle } from '../formats/font.js';
import { SizeStyle } from '../formats/size.js';
import { deleteRange } from './keyboard.js';
import normalizeExternalHTML from './normalizeExternalHTML/index.js';
const debug = logger('quill:clipboard');
const CLIPBOARD_CONFIG = [[Node.TEXT_NODE, matchText], [Node.TEXT_NODE, matchNewline], ['br', matchBreak], [Node.ELEMENT_NODE, matchNewline], [Node.ELEMENT_NODE, matchBlot], [Node.ELEMENT_NODE, matchAttributor], [Node.ELEMENT_NODE, matchStyles], ['li', matchIndent], ['ol, ul', matchList], ['pre', matchCodeBlock], ['tr', matchTable], ['b', createMatchAlias('bold')], ['i', createMatchAlias('italic')], ['strike', createMatchAlias('strike')], ['style', matchIgnore]];
const ATTRIBUTE_ATTRIBUTORS = [AlignAttribute, DirectionAttribute].reduce((memo, attr) => {
memo[attr.keyName] = attr;
return memo;
}, {});
const STYLE_ATTRIBUTORS = [AlignStyle, BackgroundStyle, ColorStyle, DirectionStyle, FontStyle, SizeStyle].reduce((memo, attr) => {
memo[attr.keyName] = attr;
return memo;
}, {});
class Clipboard extends Module {
static DEFAULTS = {
matchers: []
};
constructor(quill, options) {
super(quill, options);
this.quill.root.addEventListener('copy', e => this.onCaptureCopy(e, false));
this.quill.root.addEventListener('cut', e => this.onCaptureCopy(e, true));
this.quill.root.addEventListener('paste', this.onCapturePaste.bind(this));
this.matchers = [];
CLIPBOARD_CONFIG.concat(this.options.matchers ?? []).forEach(_ref => {
let [selector, matcher] = _ref;
this.addMatcher(selector, matcher);
});
}
addMatcher(selector, matcher) {
this.matchers.push([selector, matcher]);
}
convert(_ref2) {
let {
html,
text
} = _ref2;
let formats = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
if (formats[CodeBlock.blotName]) {
return new Delta().insert(text || '', {
[CodeBlock.blotName]: formats[CodeBlock.blotName]
});
}
if (!html) {
return new Delta().insert(text || '', formats);
}
const delta = this.convertHTML(html);
// Remove trailing newline
if (deltaEndsWith(delta, '\n') && (delta.ops[delta.ops.length - 1].attributes == null || formats.table)) {
return delta.compose(new Delta().retain(delta.length() - 1).delete(1));
}
return delta;
}
normalizeHTML(doc) {
normalizeExternalHTML(doc);
}
convertHTML(html) {
const doc = new DOMParser().parseFromString(html, 'text/html');
this.normalizeHTML(doc);
const container = doc.body;
const nodeMatches = new WeakMap();
const [elementMatchers, textMatchers] = this.prepareMatching(container, nodeMatches);
return traverse(this.quill.scroll, container, elementMatchers, textMatchers, nodeMatches);
}
dangerouslyPasteHTML(index, html) {
let source = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : Quill.sources.API;
if (typeof index === 'string') {
const delta = this.convert({
html: index,
text: ''
});
// @ts-expect-error
this.quill.setContents(delta, html);
this.quill.setSelection(0, Quill.sources.SILENT);
} else {
const paste = this.convert({
html,
text: ''
});
this.quill.updateContents(new Delta().retain(index).concat(paste), source);
this.quill.setSelection(index + paste.length(), Quill.sources.SILENT);
}
}
onCaptureCopy(e) {
let isCut = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
if (e.defaultPrevented) return;
e.preventDefault();
const [range] = this.quill.selection.getRange();
if (range == null) return;
const {
html,
text
} = this.onCopy(range, isCut);
e.clipboardData?.setData('text/plain', text);
e.clipboardData?.setData('text/html', html);
if (isCut) {
deleteRange({
range,
quill: this.quill
});
}
}
/*
* https://www.iana.org/assignments/media-types/text/uri-list
*/
normalizeURIList(urlList) {
return urlList.split(/\r?\n/)
// Ignore all comments
.filter(url => url[0] !== '#').join('\n');
}
onCapturePaste(e) {
if (e.defaultPrevented || !this.quill.isEnabled()) return;
e.preventDefault();
const range = this.quill.getSelection(true);
if (range == null) return;
const html = e.clipboardData?.getData('text/html');
let text = e.clipboardData?.getData('text/plain');
if (!html && !text) {
const urlList = e.clipboardData?.getData('text/uri-list');
if (urlList) {
text = this.normalizeURIList(urlList);
}
}
const files = Array.from(e.clipboardData?.files || []);
if (!html && files.length > 0) {
this.quill.uploader.upload(range, files);
return;
}
if (html && files.length > 0) {
const doc = new DOMParser().parseFromString(html, 'text/html');
if (doc.body.childElementCount === 1 && doc.body.firstElementChild?.tagName === 'IMG') {
this.quill.uploader.upload(range, files);
return;
}
}
this.onPaste(range, {
html,
text
});
}
onCopy(range) {
const text = this.quill.getText(range);
const html = this.quill.getSemanticHTML(range);
return {
html,
text
};
}
onPaste(range, _ref3) {
let {
text,
html
} = _ref3;
const formats = this.quill.getFormat(range.index);
const pastedDelta = this.convert({
text,
html
}, formats);
debug.log('onPaste', pastedDelta, {
text,
html
});
const delta = new Delta().retain(range.index).delete(range.length).concat(pastedDelta);
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.scrollSelectionIntoView();
}
prepareMatching(container, nodeMatches) {
const elementMatchers = [];
const textMatchers = [];
this.matchers.forEach(pair => {
const [selector, matcher] = pair;
switch (selector) {
case Node.TEXT_NODE:
textMatchers.push(matcher);
break;
case Node.ELEMENT_NODE:
elementMatchers.push(matcher);
break;
default:
Array.from(container.querySelectorAll(selector)).forEach(node => {
if (nodeMatches.has(node)) {
const matches = nodeMatches.get(node);
matches?.push(matcher);
} else {
nodeMatches.set(node, [matcher]);
}
});
break;
}
});
return [elementMatchers, textMatchers];
}
}
function applyFormat(delta, format, value, scroll) {
if (!scroll.query(format)) {
return delta;
}
return delta.reduce((newDelta, op) => {
if (!op.insert) return newDelta;
if (op.attributes && op.attributes[format]) {
return newDelta.push(op);
}
const formats = value ? {
[format]: value
} : {};
return newDelta.insert(op.insert, {
...formats,
...op.attributes
});
}, new Delta());
}
function deltaEndsWith(delta, text) {
let endText = '';
for (let i = delta.ops.length - 1; i >= 0 && endText.length < text.length; --i // eslint-disable-line no-plusplus
) {
const op = delta.ops[i];
if (typeof op.insert !== 'string') break;
endText = op.insert + endText;
}
return endText.slice(-1 * text.length) === text;
}
function isLine(node, scroll) {
if (!(node instanceof Element)) return false;
const match = scroll.query(node);
// @ts-expect-error
if (match && match.prototype instanceof EmbedBlot) return false;
return ['address', 'article', 'blockquote', 'canvas', 'dd', 'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'iframe', 'li', 'main', 'nav', 'ol', 'output', 'p', 'pre', 'section', 'table', 'td', 'tr', 'ul', 'video'].includes(node.tagName.toLowerCase());
}
function isBetweenInlineElements(node, scroll) {
return node.previousElementSibling && node.nextElementSibling && !isLine(node.previousElementSibling, scroll) && !isLine(node.nextElementSibling, scroll);
}
const preNodes = new WeakMap();
function isPre(node) {
if (node == null) return false;
if (!preNodes.has(node)) {
// @ts-expect-error
if (node.tagName === 'PRE') {
preNodes.set(node, true);
} else {
preNodes.set(node, isPre(node.parentNode));
}
}
return preNodes.get(node);
}
function traverse(scroll, node, elementMatchers, textMatchers, nodeMatches) {
// Post-order
if (node.nodeType === node.TEXT_NODE) {
return textMatchers.reduce((delta, matcher) => {
return matcher(node, delta, scroll);
}, new Delta());
}
if (node.nodeType === node.ELEMENT_NODE) {
return Array.from(node.childNodes || []).reduce((delta, childNode) => {
let childrenDelta = traverse(scroll, childNode, elementMatchers, textMatchers, nodeMatches);
if (childNode.nodeType === node.ELEMENT_NODE) {
childrenDelta = elementMatchers.reduce((reducedDelta, matcher) => {
return matcher(childNode, reducedDelta, scroll);
}, childrenDelta);
childrenDelta = (nodeMatches.get(childNode) || []).reduce((reducedDelta, matcher) => {
return matcher(childNode, reducedDelta, scroll);
}, childrenDelta);
}
return delta.concat(childrenDelta);
}, new Delta());
}
return new Delta();
}
function createMatchAlias(format) {
return (_node, delta, scroll) => {
return applyFormat(delta, format, true, scroll);
};
}
function matchAttributor(node, delta, scroll) {
const attributes = Attributor.keys(node);
const classes = ClassAttributor.keys(node);
const styles = StyleAttributor.keys(node);
const formats = {};
attributes.concat(classes).concat(styles).forEach(name => {
let attr = scroll.query(name, 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;
}
});
return Object.entries(formats).reduce((newDelta, _ref4) => {
let [name, value] = _ref4;
return applyFormat(newDelta, name, value, scroll);
}, delta);
}
function matchBlot(node, delta, scroll) {
const match = scroll.query(node);
if (match == null) return delta;
// @ts-expect-error
if (match.prototype instanceof EmbedBlot) {
const embed = {};
// @ts-expect-error
const value = match.value(node);
if (value != null) {
// @ts-expect-error
embed[match.blotName] = value;
// @ts-expect-error
return new Delta().insert(embed, match.formats(node, scroll));
}
} else {
// @ts-expect-error
if (match.prototype instanceof BlockBlot && !deltaEndsWith(delta, '\n')) {
delta.insert('\n');
}
if ('blotName' in match && 'formats' in match && typeof match.formats === 'function') {
return applyFormat(delta, match.blotName, match.formats(node, scroll), scroll);
}
}
return delta;
}
function matchBreak(node, delta) {
if (!deltaEndsWith(delta, '\n')) {
delta.insert('\n');
}
return delta;
}
function matchCodeBlock(node, delta, scroll) {
const match = scroll.query('code-block');
const language = match && 'formats' in match && typeof match.formats === 'function' ? match.formats(node, scroll) : true;
return applyFormat(delta, 'code-block', language, scroll);
}
function matchIgnore() {
return new Delta();
}
function matchIndent(node, delta, scroll) {
const match = scroll.query(node);
if (match == null ||
// @ts-expect-error
match.blotName !== 'list' || !deltaEndsWith(delta, '\n')) {
return delta;
}
let indent = -1;
let parent = node.parentNode;
while (parent != null) {
// @ts-expect-error
if (['OL', 'UL'].includes(parent.tagName)) {
indent += 1;
}
parent = parent.parentNode;
}
if (indent <= 0) return delta;
return delta.reduce((composed, op) => {
if (!op.insert) return composed;
if (op.attributes && typeof op.attributes.indent === 'number') {
return composed.push(op);
}
return composed.insert(op.insert, {
indent,
...(op.attributes || {})
});
}, new Delta());
}
function matchList(node, delta, scroll) {
const element = node;
let list = element.tagName === 'OL' ? 'ordered' : 'bullet';
const checkedAttr = element.getAttribute('data-checked');
if (checkedAttr) {
list = checkedAttr === 'true' ? 'checked' : 'unchecked';
}
return applyFormat(delta, 'list', list, scroll);
}
function matchNewline(node, delta, scroll) {
if (!deltaEndsWith(delta, '\n')) {
if (isLine(node, scroll) && (node.childNodes.length > 0 || node instanceof HTMLParagraphElement)) {
return delta.insert('\n');
}
if (delta.length() > 0 && node.nextSibling) {
let nextSibling = node.nextSibling;
while (nextSibling != null) {
if (isLine(nextSibling, scroll)) {
return delta.insert('\n');
}
const match = scroll.query(nextSibling);
// @ts-expect-error
if (match && match.prototype instanceof BlockEmbed) {
return delta.insert('\n');
}
nextSibling = nextSibling.firstChild;
}
}
}
return delta;
}
function matchStyles(node, delta, scroll) {
const formats = {};
const style = node.style || {};
if (style.fontStyle === 'italic') {
formats.italic = true;
}
if (style.textDecoration === 'underline') {
formats.underline = true;
}
if (style.textDecoration === 'line-through') {
formats.strike = true;
}
if (style.fontWeight?.startsWith('bold') ||
// @ts-expect-error Fix me later
parseInt(style.fontWeight, 10) >= 700) {
formats.bold = true;
}
delta = Object.entries(formats).reduce((newDelta, _ref5) => {
let [name, value] = _ref5;
return applyFormat(newDelta, name, value, scroll);
}, delta);
// @ts-expect-error
if (parseFloat(style.textIndent || 0) > 0) {
// Could be 0.5in
return new Delta().insert('\t').concat(delta);
}
return delta;
}
function matchTable(node, delta, scroll) {
const table = node.parentElement?.tagName === 'TABLE' ? node.parentElement : node.parentElement?.parentElement;
if (table != null) {
const rows = Array.from(table.querySelectorAll('tr'));
const row = rows.indexOf(node) + 1;
return applyFormat(delta, 'table', row, scroll);
}
return delta;
}
function matchText(node, delta, scroll) {
// @ts-expect-error
let text = node.data;
// Word represents empty line with <o:p>&nbsp;</o:p>
if (node.parentElement?.tagName === 'O:P') {
return delta.insert(text.trim());
}
if (!isPre(node)) {
if (text.trim().length === 0 && text.includes('\n') && !isBetweenInlineElements(node, scroll)) {
return delta;
}
// convert all non-nbsp whitespace into regular space
text = text.replace(/[^\S\u00a0]/g, ' ');
// collapse consecutive spaces into one
text = text.replace(/ {2,}/g, ' ');
if (node.previousSibling == null && node.parentElement != null && isLine(node.parentElement, scroll) || node.previousSibling instanceof Element && isLine(node.previousSibling, scroll)) {
// block structure means we don't need leading space
text = text.replace(/^ /, '');
}
if (node.nextSibling == null && node.parentElement != null && isLine(node.parentElement, scroll) || node.nextSibling instanceof Element && isLine(node.nextSibling, scroll)) {
// block structure means we don't need trailing space
text = text.replace(/ $/, '');
}
// done removing whitespace and can normalize all to regular space
text = text.replaceAll('\u00a0', ' ');
}
return delta.insert(text);
}
export { Clipboard as default, matchAttributor, matchBlot, matchNewline, matchText, traverse };
//# sourceMappingURL=clipboard.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,36 @@
import type Delta from 'quill-delta';
import Module from '../core/module.js';
import Quill from '../core/quill.js';
import type Scroll from '../blots/scroll.js';
import type { Range } from '../core/selection.js';
export interface HistoryOptions {
userOnly: boolean;
delay: number;
maxStack: number;
}
export interface StackItem {
delta: Delta;
range: Range | null;
}
interface Stack {
undo: StackItem[];
redo: StackItem[];
}
declare class History extends Module<HistoryOptions> {
static DEFAULTS: HistoryOptions;
lastRecorded: number;
ignoreChange: boolean;
stack: Stack;
currentRange: Range | null;
constructor(quill: Quill, options: Partial<HistoryOptions>);
change(source: 'undo' | 'redo', dest: 'redo' | 'undo'): void;
clear(): void;
cutoff(): void;
record(changeDelta: Delta, oldDelta: Delta): void;
redo(): void;
transform(delta: Delta): void;
undo(): void;
protected restoreSelection(stackItem: StackItem): void;
}
declare function getLastChangeIndex(scroll: Scroll, delta: Delta): number;
export { History as default, getLastChangeIndex };

View File

@@ -0,0 +1,178 @@
import { Scope } from 'parchment';
import Module from '../core/module.js';
import Quill from '../core/quill.js';
class History extends Module {
static DEFAULTS = {
delay: 1000,
maxStack: 100,
userOnly: false
};
lastRecorded = 0;
ignoreChange = false;
stack = {
undo: [],
redo: []
};
currentRange = null;
constructor(quill, options) {
super(quill, options);
this.quill.on(Quill.events.EDITOR_CHANGE, (eventName, value, oldValue, source) => {
if (eventName === Quill.events.SELECTION_CHANGE) {
if (value && source !== Quill.sources.SILENT) {
this.currentRange = value;
}
} else if (eventName === Quill.events.TEXT_CHANGE) {
if (!this.ignoreChange) {
if (!this.options.userOnly || source === Quill.sources.USER) {
this.record(value, oldValue);
} else {
this.transform(value);
}
}
this.currentRange = transformRange(this.currentRange, value);
}
});
this.quill.keyboard.addBinding({
key: 'z',
shortKey: true
}, this.undo.bind(this));
this.quill.keyboard.addBinding({
key: ['z', '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));
}
this.quill.root.addEventListener('beforeinput', event => {
if (event.inputType === 'historyUndo') {
this.undo();
event.preventDefault();
} else if (event.inputType === 'historyRedo') {
this.redo();
event.preventDefault();
}
});
}
change(source, dest) {
if (this.stack[source].length === 0) return;
const item = this.stack[source].pop();
if (!item) return;
const base = this.quill.getContents();
const inverseDelta = item.delta.invert(base);
this.stack[dest].push({
delta: inverseDelta,
range: transformRange(item.range, inverseDelta)
});
this.lastRecorded = 0;
this.ignoreChange = true;
this.quill.updateContents(item.delta, Quill.sources.USER);
this.ignoreChange = false;
this.restoreSelection(item);
}
clear() {
this.stack = {
undo: [],
redo: []
};
}
cutoff() {
this.lastRecorded = 0;
}
record(changeDelta, oldDelta) {
if (changeDelta.ops.length === 0) return;
this.stack.redo = [];
let undoDelta = changeDelta.invert(oldDelta);
let undoRange = this.currentRange;
const timestamp = Date.now();
if (
// @ts-expect-error Fix me later
this.lastRecorded + this.options.delay > timestamp && this.stack.undo.length > 0) {
const item = this.stack.undo.pop();
if (item) {
undoDelta = undoDelta.compose(item.delta);
undoRange = item.range;
}
} else {
this.lastRecorded = timestamp;
}
if (undoDelta.length() === 0) return;
this.stack.undo.push({
delta: undoDelta,
range: undoRange
});
// @ts-expect-error Fix me later
if (this.stack.undo.length > this.options.maxStack) {
this.stack.undo.shift();
}
}
redo() {
this.change('redo', 'undo');
}
transform(delta) {
transformStack(this.stack.undo, delta);
transformStack(this.stack.redo, delta);
}
undo() {
this.change('undo', 'redo');
}
restoreSelection(stackItem) {
if (stackItem.range) {
this.quill.setSelection(stackItem.range, Quill.sources.USER);
} else {
const index = getLastChangeIndex(this.quill.scroll, stackItem.delta);
this.quill.setSelection(index, Quill.sources.USER);
}
}
}
function transformStack(stack, delta) {
let remoteDelta = delta;
for (let i = stack.length - 1; i >= 0; i -= 1) {
const oldItem = stack[i];
stack[i] = {
delta: remoteDelta.transform(oldItem.delta, true),
range: oldItem.range && transformRange(oldItem.range, remoteDelta)
};
remoteDelta = oldItem.delta.transform(remoteDelta);
if (stack[i].delta.length() === 0) {
stack.splice(i, 1);
}
}
}
function endsWithNewlineChange(scroll, delta) {
const 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(attr => {
return scroll.query(attr, Scope.BLOCK) != null;
});
}
return false;
}
function getLastChangeIndex(scroll, delta) {
const deleteLength = delta.reduce((length, op) => {
return length + (op.delete || 0);
}, 0);
let changeIndex = delta.length() - deleteLength;
if (endsWithNewlineChange(scroll, delta)) {
changeIndex -= 1;
}
return changeIndex;
}
function transformRange(range, delta) {
if (!range) return range;
const start = delta.transformPosition(range.index);
const end = delta.transformPosition(range.index + range.length);
return {
index: start,
length: end - start
};
}
export { History as default, getLastChangeIndex };
//# sourceMappingURL=history.js.map

File diff suppressed because one or more lines are too long

10
public/assets/quill/modules/input.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
import Module from '../core/module.js';
import Quill from '../core/quill.js';
declare class Input extends Module {
constructor(quill: Quill, options: Record<string, never>);
private deleteRange;
private replaceText;
private handleBeforeInput;
private handleCompositionStart;
}
export default Input;

View File

@@ -0,0 +1,83 @@
import Delta from 'quill-delta';
import Module from '../core/module.js';
import Quill from '../core/quill.js';
import { deleteRange } from './keyboard.js';
const INSERT_TYPES = ['insertText', 'insertReplacementText'];
class Input extends Module {
constructor(quill, options) {
super(quill, options);
quill.root.addEventListener('beforeinput', event => {
this.handleBeforeInput(event);
});
// Gboard with English input on Android triggers `compositionstart` sometimes even
// users are not going to type anything.
if (!/Android/i.test(navigator.userAgent)) {
quill.on(Quill.events.COMPOSITION_BEFORE_START, () => {
this.handleCompositionStart();
});
}
}
deleteRange(range) {
deleteRange({
range,
quill: this.quill
});
}
replaceText(range) {
let text = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
if (range.length === 0) return false;
if (text) {
// Follow the native behavior that inherits the formats of the first character
const formats = this.quill.getFormat(range.index, 1);
this.deleteRange(range);
this.quill.updateContents(new Delta().retain(range.index).insert(text, formats), Quill.sources.USER);
} else {
this.deleteRange(range);
}
this.quill.setSelection(range.index + text.length, 0, Quill.sources.SILENT);
return true;
}
handleBeforeInput(event) {
if (this.quill.composition.isComposing || event.defaultPrevented || !INSERT_TYPES.includes(event.inputType)) {
return;
}
const staticRange = event.getTargetRanges ? event.getTargetRanges()[0] : null;
if (!staticRange || staticRange.collapsed === true) {
return;
}
const text = getPlainTextFromInputEvent(event);
if (text == null) {
return;
}
const normalized = this.quill.selection.normalizeNative(staticRange);
const range = normalized ? this.quill.selection.normalizedToRange(normalized) : null;
if (range && this.replaceText(range, text)) {
event.preventDefault();
}
}
handleCompositionStart() {
const range = this.quill.getSelection();
if (range) {
this.replaceText(range);
}
}
}
function getPlainTextFromInputEvent(event) {
// When `inputType` is "insertText":
// - `event.data` should be string (Safari uses `event.dataTransfer`).
// - `event.dataTransfer` should be null.
// When `inputType` is "insertReplacementText":
// - `event.data` should be null.
// - `event.dataTransfer` should contain "text/plain" data.
if (typeof event.data === 'string') {
return event.data;
}
if (event.dataTransfer?.types.includes('text/plain')) {
return event.dataTransfer.getData('text/plain');
}
return null;
}
export default Input;
//# sourceMappingURL=input.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,58 @@
import type { BlockBlot } from 'parchment';
import Quill from '../core/quill.js';
import Module from '../core/module.js';
import type { BlockEmbed } from '../blots/block.js';
import type { Range } from '../core/selection.js';
declare const SHORTKEY: string;
export interface Context {
collapsed: boolean;
empty: boolean;
offset: number;
prefix: string;
suffix: string;
format: Record<string, unknown>;
event: KeyboardEvent;
line: BlockEmbed | BlockBlot;
}
interface BindingObject extends Partial<Omit<Context, 'prefix' | 'suffix' | 'format'>> {
key: number | string | string[];
shortKey?: boolean | null;
shiftKey?: boolean | null;
altKey?: boolean | null;
metaKey?: boolean | null;
ctrlKey?: boolean | null;
prefix?: RegExp;
suffix?: RegExp;
format?: Record<string, unknown> | string[];
handler?: (this: {
quill: Quill;
}, range: Range, curContext: Context, binding: NormalizedBinding) => boolean | void;
}
type Binding = BindingObject | string | number;
interface NormalizedBinding extends Omit<BindingObject, 'key' | 'shortKey'> {
key: string | number;
}
interface KeyboardOptions {
bindings: Record<string, Binding>;
}
interface KeyboardOptions {
bindings: Record<string, Binding>;
}
declare class Keyboard extends Module<KeyboardOptions> {
static DEFAULTS: KeyboardOptions;
static match(evt: KeyboardEvent, binding: BindingObject): boolean;
bindings: Record<string, NormalizedBinding[]>;
constructor(quill: Quill, options: Partial<KeyboardOptions>);
addBinding(keyBinding: Binding, context?: Required<BindingObject['handler']> | Partial<Omit<BindingObject, 'key' | 'handler'>>, handler?: Required<BindingObject['handler']> | Partial<Omit<BindingObject, 'key' | 'handler'>>): void;
listen(): void;
handleBackspace(range: Range, context: Context): void;
handleDelete(range: Range, context: Context): void;
handleDeleteRange(range: Range): void;
handleEnter(range: Range, context: Context): void;
}
declare function normalize(binding: Binding): BindingObject | null;
declare function deleteRange({ quill, range }: {
quill: Quill;
range: Range;
}): void;
export { Keyboard as default, SHORTKEY, normalize, deleteRange };

View File

@@ -0,0 +1,713 @@
import { cloneDeep, isEqual } from 'lodash-es';
import Delta, { AttributeMap } from 'quill-delta';
import { EmbedBlot, Scope, TextBlot } from 'parchment';
import Quill from '../core/quill.js';
import logger from '../core/logger.js';
import Module from '../core/module.js';
const debug = logger('quill:keyboard');
const SHORTKEY = /Mac/i.test(navigator.platform) ? 'metaKey' : 'ctrlKey';
class Keyboard extends Module {
static match(evt, binding) {
if (['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].some(key => {
return !!binding[key] !== evt[key] && binding[key] !== null;
})) {
return false;
}
return binding.key === evt.key || binding.key === evt.which;
}
constructor(quill, options) {
super(quill, options);
this.bindings = {};
// @ts-expect-error Fix me later
Object.keys(this.options.bindings).forEach(name => {
// @ts-expect-error Fix me later
if (this.options.bindings[name]) {
// @ts-expect-error Fix me later
this.addBinding(this.options.bindings[name]);
}
});
this.addBinding({
key: 'Enter',
shiftKey: null
}, this.handleEnter);
this.addBinding({
key: 'Enter',
metaKey: null,
ctrlKey: null,
altKey: null
}, () => {});
if (/Firefox/i.test(navigator.userAgent)) {
// Need to handle delete and backspace for Firefox in the general case #1171
this.addBinding({
key: 'Backspace'
}, {
collapsed: true
}, this.handleBackspace);
this.addBinding({
key: 'Delete'
}, {
collapsed: true
}, this.handleDelete);
} else {
this.addBinding({
key: 'Backspace'
}, {
collapsed: true,
prefix: /^.?$/
}, this.handleBackspace);
this.addBinding({
key: 'Delete'
}, {
collapsed: true,
suffix: /^.?$/
}, this.handleDelete);
}
this.addBinding({
key: 'Backspace'
}, {
collapsed: false
}, this.handleDeleteRange);
this.addBinding({
key: 'Delete'
}, {
collapsed: false
}, this.handleDeleteRange);
this.addBinding({
key: 'Backspace',
altKey: null,
ctrlKey: null,
metaKey: null,
shiftKey: null
}, {
collapsed: true,
offset: 0
}, this.handleBackspace);
this.listen();
}
addBinding(keyBinding) {
let context = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
let handler = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
const binding = normalize(keyBinding);
if (binding == null) {
debug.warn('Attempted to add invalid keyboard binding', binding);
return;
}
if (typeof context === 'function') {
context = {
handler: context
};
}
if (typeof handler === 'function') {
handler = {
handler
};
}
const keys = Array.isArray(binding.key) ? binding.key : [binding.key];
keys.forEach(key => {
const singleBinding = {
...binding,
key,
...context,
...handler
};
this.bindings[singleBinding.key] = this.bindings[singleBinding.key] || [];
this.bindings[singleBinding.key].push(singleBinding);
});
}
listen() {
this.quill.root.addEventListener('keydown', evt => {
if (evt.defaultPrevented || evt.isComposing) return;
// evt.isComposing is false when pressing Enter/Backspace when composing in Safari
// https://bugs.webkit.org/show_bug.cgi?id=165004
const isComposing = evt.keyCode === 229 && (evt.key === 'Enter' || evt.key === 'Backspace');
if (isComposing) return;
const bindings = (this.bindings[evt.key] || []).concat(this.bindings[evt.which] || []);
const matches = bindings.filter(binding => Keyboard.match(evt, binding));
if (matches.length === 0) return;
// @ts-expect-error
const blot = Quill.find(evt.target, true);
if (blot && blot.scroll !== this.quill.scroll) return;
const range = this.quill.getSelection();
if (range == null || !this.quill.hasFocus()) return;
const [line, offset] = this.quill.getLine(range.index);
const [leafStart, offsetStart] = this.quill.getLeaf(range.index);
const [leafEnd, offsetEnd] = range.length === 0 ? [leafStart, offsetStart] : this.quill.getLeaf(range.index + range.length);
const prefixText = leafStart instanceof TextBlot ? leafStart.value().slice(0, offsetStart) : '';
const suffixText = leafEnd instanceof TextBlot ? leafEnd.value().slice(offsetEnd) : '';
const curContext = {
collapsed: range.length === 0,
// @ts-expect-error Fix me later
empty: range.length === 0 && line.length() <= 1,
format: this.quill.getFormat(range),
line,
offset,
prefix: prefixText,
suffix: suffixText,
event: evt
};
const prevented = matches.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(name => curContext.format[name] == null)) {
return false;
}
} else if (typeof binding.format === 'object') {
// all formats must match
if (!Object.keys(binding.format).every(name => {
// @ts-expect-error Fix me later
if (binding.format[name] === true) return curContext.format[name] != null;
// @ts-expect-error Fix me later
if (binding.format[name] === false) return curContext.format[name] == null;
// @ts-expect-error Fix me later
return isEqual(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;
}
// @ts-expect-error Fix me later
return binding.handler.call(this, range, curContext, binding) !== true;
});
if (prevented) {
evt.preventDefault();
}
});
}
handleBackspace(range, context) {
// Check for astral symbols
const length = /[\uD800-\uDBFF][\uDC00-\uDFFF]$/.test(context.prefix) ? 2 : 1;
if (range.index === 0 || this.quill.getLength() <= 1) return;
let formats = {};
const [line] = this.quill.getLine(range.index);
let delta = new Delta().retain(range.index - length).delete(length);
if (context.offset === 0) {
// Always deleting newline here, length always 1
const [prev] = this.quill.getLine(range.index - 1);
if (prev) {
const isPrevLineEmpty = prev.statics.blotName === 'block' && prev.length() <= 1;
if (!isPrevLineEmpty) {
// @ts-expect-error Fix me later
const curFormats = line.formats();
const prevFormats = this.quill.getFormat(range.index - 1, 1);
formats = AttributeMap.diff(curFormats, prevFormats) || {};
if (Object.keys(formats).length > 0) {
// line.length() - 1 targets \n in line, another -1 for newline being deleted
const formatDelta = new Delta()
// @ts-expect-error Fix me later
.retain(range.index + line.length() - 2).retain(1, formats);
delta = delta.compose(formatDelta);
}
}
}
}
this.quill.updateContents(delta, Quill.sources.USER);
this.quill.focus();
}
handleDelete(range, context) {
// Check for astral symbols
const length = /^[\uD800-\uDBFF][\uDC00-\uDFFF]/.test(context.suffix) ? 2 : 1;
if (range.index >= this.quill.getLength() - length) return;
let formats = {};
const [line] = this.quill.getLine(range.index);
let delta = new Delta().retain(range.index).delete(length);
// @ts-expect-error Fix me later
if (context.offset >= line.length() - 1) {
const [next] = this.quill.getLine(range.index + 1);
if (next) {
// @ts-expect-error Fix me later
const curFormats = line.formats();
const nextFormats = this.quill.getFormat(range.index, 1);
formats = AttributeMap.diff(curFormats, nextFormats) || {};
if (Object.keys(formats).length > 0) {
delta = delta.retain(next.length() - 1).retain(1, formats);
}
}
}
this.quill.updateContents(delta, Quill.sources.USER);
this.quill.focus();
}
handleDeleteRange(range) {
deleteRange({
range,
quill: this.quill
});
this.quill.focus();
}
handleEnter(range, context) {
const lineFormats = Object.keys(context.format).reduce((formats, format) => {
if (this.quill.scroll.query(format, Scope.BLOCK) && !Array.isArray(context.format[format])) {
formats[format] = context.format[format];
}
return formats;
}, {});
const delta = new Delta().retain(range.index).delete(range.length).insert('\n', lineFormats);
this.quill.updateContents(delta, Quill.sources.USER);
this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
this.quill.focus();
}
}
const defaultOptions = {
bindings: {
bold: makeFormatHandler('bold'),
italic: makeFormatHandler('italic'),
underline: makeFormatHandler('underline'),
indent: {
// highlight tab or tab at beginning of list, indent or blockquote
key: 'Tab',
format: ['blockquote', 'indent', 'list'],
handler(range, context) {
if (context.collapsed && context.offset !== 0) return true;
this.quill.format('indent', '+1', Quill.sources.USER);
return false;
}
},
outdent: {
key: 'Tab',
shiftKey: true,
format: ['blockquote', 'indent', 'list'],
// highlight tab or tab at beginning of list, indent or blockquote
handler(range, context) {
if (context.collapsed && context.offset !== 0) return true;
this.quill.format('indent', '-1', Quill.sources.USER);
return false;
}
},
'outdent backspace': {
key: 'Backspace',
collapsed: true,
shiftKey: null,
metaKey: null,
ctrlKey: null,
altKey: null,
format: ['indent', 'list'],
offset: 0,
handler(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: 'Tab',
shiftKey: true,
collapsed: true,
prefix: /\t$/,
handler(range) {
this.quill.deleteText(range.index - 1, 1, Quill.sources.USER);
}
},
tab: {
key: 'Tab',
handler(range, context) {
if (context.format.table) return true;
this.quill.history.cutoff();
const 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);
return false;
}
},
'blockquote empty enter': {
key: 'Enter',
collapsed: true,
format: ['blockquote'],
empty: true,
handler() {
this.quill.format('blockquote', false, Quill.sources.USER);
}
},
'list empty enter': {
key: 'Enter',
collapsed: true,
format: ['list'],
empty: true,
handler(range, context) {
const formats = {
list: false
};
if (context.format.indent) {
formats.indent = false;
}
this.quill.formatLine(range.index, range.length, formats, Quill.sources.USER);
}
},
'checklist enter': {
key: 'Enter',
collapsed: true,
format: {
list: 'checked'
},
handler(range) {
const [line, offset] = this.quill.getLine(range.index);
const formats = {
// @ts-expect-error Fix me later
...line.formats(),
list: 'checked'
};
const delta = new Delta().retain(range.index).insert('\n', formats)
// @ts-expect-error Fix me later
.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.scrollSelectionIntoView();
}
},
'header enter': {
key: 'Enter',
collapsed: true,
format: ['header'],
suffix: /^$/,
handler(range, context) {
const [line, offset] = this.quill.getLine(range.index);
const delta = new Delta().retain(range.index).insert('\n', context.format)
// @ts-expect-error Fix me later
.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.scrollSelectionIntoView();
}
},
'table backspace': {
key: 'Backspace',
format: ['table'],
collapsed: true,
offset: 0,
handler() {}
},
'table delete': {
key: 'Delete',
format: ['table'],
collapsed: true,
suffix: /^$/,
handler() {}
},
'table enter': {
key: 'Enter',
shiftKey: null,
format: ['table'],
handler(range) {
const module = this.quill.getModule('table');
if (module) {
// @ts-expect-error
const [table, row, cell, offset] = module.getTable(range);
const shift = tableSide(table, row, cell, offset);
if (shift == null) return;
let index = table.offset();
if (shift < 0) {
const delta = new Delta().retain(index).insert('\n');
this.quill.updateContents(delta, Quill.sources.USER);
this.quill.setSelection(range.index + 1, range.length, Quill.sources.SILENT);
} else if (shift > 0) {
index += table.length();
const delta = new Delta().retain(index).insert('\n');
this.quill.updateContents(delta, Quill.sources.USER);
this.quill.setSelection(index, Quill.sources.USER);
}
}
}
},
'table tab': {
key: 'Tab',
shiftKey: null,
format: ['table'],
handler(range, context) {
const {
event,
line: cell
} = context;
const offset = cell.offset(this.quill.scroll);
if (event.shiftKey) {
this.quill.setSelection(offset - 1, Quill.sources.USER);
} else {
this.quill.setSelection(offset + cell.length(), Quill.sources.USER);
}
}
},
'list autofill': {
key: ' ',
shiftKey: null,
collapsed: true,
format: {
'code-block': false,
blockquote: false,
table: false
},
prefix: /^\s*?(\d+\.|-|\*|\[ ?\]|\[x\])$/,
handler(range, context) {
if (this.quill.scroll.query('list') == null) return true;
const {
length
} = context.prefix;
const [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();
const delta = new Delta().retain(range.index - offset).delete(length + 1)
// @ts-expect-error Fix me later
.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);
return false;
}
},
'code exit': {
key: 'Enter',
collapsed: true,
format: ['code-block'],
prefix: /^$/,
suffix: /^\s*$/,
handler(range) {
const [line, offset] = this.quill.getLine(range.index);
let numLines = 2;
let cur = line;
while (cur != null && cur.length() <= 1 && cur.formats()['code-block']) {
// @ts-expect-error
cur = cur.prev;
numLines -= 1;
// Requisite prev lines are empty
if (numLines <= 0) {
const delta = new Delta()
// @ts-expect-error Fix me later
.retain(range.index + line.length() - offset - 2).retain(1, {
'code-block': null
}).delete(1);
this.quill.updateContents(delta, Quill.sources.USER);
this.quill.setSelection(range.index - 1, Quill.sources.SILENT);
return false;
}
}
return true;
}
},
'embed left': makeEmbedArrowHandler('ArrowLeft', false),
'embed left shift': makeEmbedArrowHandler('ArrowLeft', true),
'embed right': makeEmbedArrowHandler('ArrowRight', false),
'embed right shift': makeEmbedArrowHandler('ArrowRight', true),
'table down': makeTableArrowHandler(false),
'table up': makeTableArrowHandler(true)
}
};
Keyboard.DEFAULTS = defaultOptions;
function makeCodeBlockHandler(indent) {
return {
key: 'Tab',
shiftKey: !indent,
format: {
'code-block': true
},
handler(range, _ref) {
let {
event
} = _ref;
const CodeBlock = this.quill.scroll.query('code-block');
// @ts-expect-error
const {
TAB
} = CodeBlock;
if (range.length === 0 && !event.shiftKey) {
this.quill.insertText(range.index, TAB, Quill.sources.USER);
this.quill.setSelection(range.index + TAB.length, Quill.sources.SILENT);
return;
}
const lines = range.length === 0 ? this.quill.getLines(range.index, 1) : this.quill.getLines(range);
let {
index,
length
} = range;
lines.forEach((line, i) => {
if (indent) {
line.insertAt(0, TAB);
if (i === 0) {
index += TAB.length;
} else {
length += TAB.length;
}
// @ts-expect-error Fix me later
} else if (line.domNode.textContent.startsWith(TAB)) {
line.deleteAt(0, TAB.length);
if (i === 0) {
index -= TAB.length;
} else {
length -= TAB.length;
}
}
});
this.quill.update(Quill.sources.USER);
this.quill.setSelection(index, length, Quill.sources.SILENT);
}
};
}
function makeEmbedArrowHandler(key, shiftKey) {
const where = key === 'ArrowLeft' ? 'prefix' : 'suffix';
return {
key,
shiftKey,
altKey: null,
[where]: /^$/,
handler(range) {
let {
index
} = range;
if (key === 'ArrowRight') {
index += range.length + 1;
}
const [leaf] = this.quill.getLeaf(index);
if (!(leaf instanceof EmbedBlot)) return true;
if (key === 'ArrowLeft') {
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 makeFormatHandler(format) {
return {
key: format[0],
shortKey: true,
handler(range, context) {
this.quill.format(format, !context.format[format], Quill.sources.USER);
}
};
}
function makeTableArrowHandler(up) {
return {
key: up ? 'ArrowUp' : 'ArrowDown',
collapsed: true,
format: ['table'],
handler(range, context) {
// TODO move to table module
const key = up ? 'prev' : 'next';
const cell = context.line;
const targetRow = cell.parent[key];
if (targetRow != null) {
if (targetRow.statics.blotName === 'table-row') {
// @ts-expect-error
let targetCell = targetRow.children.head;
let cur = cell;
while (cur.prev != null) {
// @ts-expect-error
cur = cur.prev;
targetCell = targetCell.next;
}
const index = targetCell.offset(this.quill.scroll) + Math.min(context.offset, targetCell.length() - 1);
this.quill.setSelection(index, 0, Quill.sources.USER);
}
} else {
// @ts-expect-error
const targetLine = cell.table()[key];
if (targetLine != null) {
if (up) {
this.quill.setSelection(targetLine.offset(this.quill.scroll) + targetLine.length() - 1, 0, Quill.sources.USER);
} else {
this.quill.setSelection(targetLine.offset(this.quill.scroll), 0, Quill.sources.USER);
}
}
}
return false;
}
};
}
function normalize(binding) {
if (typeof binding === 'string' || typeof binding === 'number') {
binding = {
key: binding
};
} else if (typeof binding === 'object') {
binding = cloneDeep(binding);
} else {
return null;
}
if (binding.shortKey) {
binding[SHORTKEY] = binding.shortKey;
delete binding.shortKey;
}
return binding;
}
// TODO: Move into quill.ts or editor.ts
function deleteRange(_ref2) {
let {
quill,
range
} = _ref2;
const lines = quill.getLines(range);
let formats = {};
if (lines.length > 1) {
const firstFormats = lines[0].formats();
const lastFormats = lines[lines.length - 1].formats();
formats = AttributeMap.diff(lastFormats, firstFormats) || {};
}
quill.deleteText(range, Quill.sources.USER);
if (Object.keys(formats).length > 0) {
quill.formatLine(range.index, 1, formats, Quill.sources.USER);
}
quill.setSelection(range.index, Quill.sources.SILENT);
}
function tableSide(_table, row, cell, offset) {
if (row.prev == null && row.next == null) {
if (cell.prev == null && cell.next == null) {
return offset === 0 ? -1 : 1;
}
return cell.prev == null ? -1 : 1;
}
if (row.prev == null) {
return -1;
}
if (row.next == null) {
return 1;
}
return null;
}
export { Keyboard as default, SHORTKEY, normalize, deleteRange };
//# sourceMappingURL=keyboard.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
declare const normalizeExternalHTML: (doc: Document) => void;
export default normalizeExternalHTML;

View File

@@ -0,0 +1,12 @@
import googleDocs from './normalizers/googleDocs.js';
import msWord from './normalizers/msWord.js';
const NORMALIZERS = [msWord, googleDocs];
const normalizeExternalHTML = doc => {
if (doc.documentElement) {
NORMALIZERS.forEach(normalize => {
normalize(doc);
});
}
};
export default normalizeExternalHTML;
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","names":["googleDocs","msWord","NORMALIZERS","normalizeExternalHTML","doc","documentElement","forEach","normalize"],"sources":["../../../src/modules/normalizeExternalHTML/index.ts"],"sourcesContent":["import googleDocs from './normalizers/googleDocs.js';\nimport msWord from './normalizers/msWord.js';\n\nconst NORMALIZERS = [msWord, googleDocs];\n\nconst normalizeExternalHTML = (doc: Document) => {\n if (doc.documentElement) {\n NORMALIZERS.forEach((normalize) => {\n normalize(doc);\n });\n }\n};\n\nexport default normalizeExternalHTML;\n"],"mappings":"AAAA,OAAOA,UAAU,MAAM,6BAA6B;AACpD,OAAOC,MAAM,MAAM,yBAAyB;AAE5C,MAAMC,WAAW,GAAG,CAACD,MAAM,EAAED,UAAU,CAAC;AAExC,MAAMG,qBAAqB,GAAIC,GAAa,IAAK;EAC/C,IAAIA,GAAG,CAACC,eAAe,EAAE;IACvBH,WAAW,CAACI,OAAO,CAAEC,SAAS,IAAK;MACjCA,SAAS,CAACH,GAAG,CAAC;IAChB,CAAC,CAAC;EACJ;AACF,CAAC;AAED,eAAeD,qBAAqB","ignoreList":[]}

View File

@@ -0,0 +1 @@
export default function normalize(doc: Document): void;

View File

@@ -0,0 +1,24 @@
const normalWeightRegexp = /font-weight:\s*normal/;
const blockTagNames = ['P', 'OL', 'UL'];
const isBlockElement = element => {
return element && blockTagNames.includes(element.tagName);
};
const normalizeEmptyLines = doc => {
Array.from(doc.querySelectorAll('br')).filter(br => isBlockElement(br.previousElementSibling) && isBlockElement(br.nextElementSibling)).forEach(br => {
br.parentNode?.removeChild(br);
});
};
const normalizeFontWeight = doc => {
Array.from(doc.querySelectorAll('b[style*="font-weight"]')).filter(node => node.getAttribute('style')?.match(normalWeightRegexp)).forEach(node => {
const fragment = doc.createDocumentFragment();
fragment.append(...node.childNodes);
node.parentNode?.replaceChild(fragment, node);
});
};
export default function normalize(doc) {
if (doc.querySelector('[id^="docs-internal-guid-"]')) {
normalizeFontWeight(doc);
normalizeEmptyLines(doc);
}
}
//# sourceMappingURL=googleDocs.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"googleDocs.js","names":["normalWeightRegexp","blockTagNames","isBlockElement","element","includes","tagName","normalizeEmptyLines","doc","Array","from","querySelectorAll","filter","br","previousElementSibling","nextElementSibling","forEach","parentNode","removeChild","normalizeFontWeight","node","getAttribute","match","fragment","createDocumentFragment","append","childNodes","replaceChild","normalize","querySelector"],"sources":["../../../../src/modules/normalizeExternalHTML/normalizers/googleDocs.ts"],"sourcesContent":["const normalWeightRegexp = /font-weight:\\s*normal/;\nconst blockTagNames = ['P', 'OL', 'UL'];\n\nconst isBlockElement = (element: Element | null) => {\n return element && blockTagNames.includes(element.tagName);\n};\n\nconst normalizeEmptyLines = (doc: Document) => {\n Array.from(doc.querySelectorAll('br'))\n .filter(\n (br) =>\n isBlockElement(br.previousElementSibling) &&\n isBlockElement(br.nextElementSibling),\n )\n .forEach((br) => {\n br.parentNode?.removeChild(br);\n });\n};\n\nconst normalizeFontWeight = (doc: Document) => {\n Array.from(doc.querySelectorAll('b[style*=\"font-weight\"]'))\n .filter((node) => node.getAttribute('style')?.match(normalWeightRegexp))\n .forEach((node) => {\n const fragment = doc.createDocumentFragment();\n fragment.append(...node.childNodes);\n node.parentNode?.replaceChild(fragment, node);\n });\n};\n\nexport default function normalize(doc: Document) {\n if (doc.querySelector('[id^=\"docs-internal-guid-\"]')) {\n normalizeFontWeight(doc);\n normalizeEmptyLines(doc);\n }\n}\n"],"mappings":"AAAA,MAAMA,kBAAkB,GAAG,uBAAuB;AAClD,MAAMC,aAAa,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC;AAEvC,MAAMC,cAAc,GAAIC,OAAuB,IAAK;EAClD,OAAOA,OAAO,IAAIF,aAAa,CAACG,QAAQ,CAACD,OAAO,CAACE,OAAO,CAAC;AAC3D,CAAC;AAED,MAAMC,mBAAmB,GAAIC,GAAa,IAAK;EAC7CC,KAAK,CAACC,IAAI,CAACF,GAAG,CAACG,gBAAgB,CAAC,IAAI,CAAC,CAAC,CACnCC,MAAM,CACJC,EAAE,IACDV,cAAc,CAACU,EAAE,CAACC,sBAAsB,CAAC,IACzCX,cAAc,CAACU,EAAE,CAACE,kBAAkB,CACxC,CAAC,CACAC,OAAO,CAAEH,EAAE,IAAK;IACfA,EAAE,CAACI,UAAU,EAAEC,WAAW,CAACL,EAAE,CAAC;EAChC,CAAC,CAAC;AACN,CAAC;AAED,MAAMM,mBAAmB,GAAIX,GAAa,IAAK;EAC7CC,KAAK,CAACC,IAAI,CAACF,GAAG,CAACG,gBAAgB,CAAC,yBAAyB,CAAC,CAAC,CACxDC,MAAM,CAAEQ,IAAI,IAAKA,IAAI,CAACC,YAAY,CAAC,OAAO,CAAC,EAAEC,KAAK,CAACrB,kBAAkB,CAAC,CAAC,CACvEe,OAAO,CAAEI,IAAI,IAAK;IACjB,MAAMG,QAAQ,GAAGf,GAAG,CAACgB,sBAAsB,CAAC,CAAC;IAC7CD,QAAQ,CAACE,MAAM,CAAC,GAAGL,IAAI,CAACM,UAAU,CAAC;IACnCN,IAAI,CAACH,UAAU,EAAEU,YAAY,CAACJ,QAAQ,EAAEH,IAAI,CAAC;EAC/C,CAAC,CAAC;AACN,CAAC;AAED,eAAe,SAASQ,SAASA,CAACpB,GAAa,EAAE;EAC/C,IAAIA,GAAG,CAACqB,aAAa,CAAC,6BAA6B,CAAC,EAAE;IACpDV,mBAAmB,CAACX,GAAG,CAAC;IACxBD,mBAAmB,CAACC,GAAG,CAAC;EAC1B;AACF","ignoreList":[]}

View File

@@ -0,0 +1 @@
export default function normalize(doc: Document): void;

View File

@@ -0,0 +1,87 @@
const ignoreRegexp = /\bmso-list:[^;]*ignore/i;
const idRegexp = /\bmso-list:[^;]*\bl(\d+)/i;
const indentRegexp = /\bmso-list:[^;]*\blevel(\d+)/i;
const parseListItem = (element, html) => {
const style = element.getAttribute('style');
const idMatch = style?.match(idRegexp);
if (!idMatch) {
return null;
}
const id = Number(idMatch[1]);
const indentMatch = style?.match(indentRegexp);
const indent = indentMatch ? Number(indentMatch[1]) : 1;
const typeRegexp = new RegExp(`@list l${id}:level${indent}\\s*\\{[^\\}]*mso-level-number-format:\\s*([\\w-]+)`, 'i');
const typeMatch = html.match(typeRegexp);
const type = typeMatch && typeMatch[1] === 'bullet' ? 'bullet' : 'ordered';
return {
id,
indent,
type,
element
};
};
// list items are represented as `p` tags with styles like `mso-list: l0 level1` where:
// 1. "0" in "l0" means the list item id;
// 2. "1" in "level1" means the indent level, starting from 1.
const normalizeListItem = doc => {
const msoList = Array.from(doc.querySelectorAll('[style*=mso-list]'));
const ignored = [];
const others = [];
msoList.forEach(node => {
const shouldIgnore = (node.getAttribute('style') || '').match(ignoreRegexp);
if (shouldIgnore) {
ignored.push(node);
} else {
others.push(node);
}
});
// Each list item contains a marker wrapped with "mso-list: Ignore".
ignored.forEach(node => node.parentNode?.removeChild(node));
// The list stype is not defined inline with the tag, instead, it's in the
// style tag so we need to pass the html as a string.
const html = doc.documentElement.innerHTML;
const listItems = others.map(element => parseListItem(element, html)).filter(parsed => parsed);
while (listItems.length) {
const childListItems = [];
let current = listItems.shift();
// Group continuous items into the same group (aka "ul")
while (current) {
childListItems.push(current);
current = listItems.length && listItems[0]?.element === current.element.nextElementSibling &&
// Different id means the next item doesn't belong to this group.
listItems[0].id === current.id ? listItems.shift() : null;
}
const ul = document.createElement('ul');
childListItems.forEach(listItem => {
const li = document.createElement('li');
li.setAttribute('data-list', listItem.type);
if (listItem.indent > 1) {
li.setAttribute('class', `ql-indent-${listItem.indent - 1}`);
}
li.innerHTML = listItem.element.innerHTML;
ul.appendChild(li);
});
const element = childListItems[0]?.element;
const {
parentNode
} = element ?? {};
if (element) {
parentNode?.replaceChild(ul, element);
}
childListItems.slice(1).forEach(_ref => {
let {
element: e
} = _ref;
parentNode?.removeChild(e);
});
}
};
export default function normalize(doc) {
if (doc.documentElement.getAttribute('xmlns:w') === 'urn:schemas-microsoft-com:office:word') {
normalizeListItem(doc);
}
}
//# sourceMappingURL=msWord.js.map

File diff suppressed because one or more lines are too long

50
public/assets/quill/modules/syntax.d.ts vendored Normal file
View File

@@ -0,0 +1,50 @@
import Delta from 'quill-delta';
import type { Blot, ScrollBlot } from 'parchment';
import Inline from '../blots/inline.js';
import Quill from '../core/quill.js';
import Module from '../core/module.js';
import CodeBlock, { CodeBlockContainer } from '../formats/code.js';
declare class CodeToken extends Inline {
static formats(node: Element, scroll: ScrollBlot): any;
constructor(scroll: ScrollBlot, domNode: Node, value: unknown);
format(format: string, value: unknown): void;
optimize(...args: unknown[]): void;
}
declare class SyntaxCodeBlock extends CodeBlock {
static create(value: unknown): HTMLElement;
static formats(domNode: Node): any;
static register(): void;
format(name: string, value: unknown): void;
replaceWith(name: string | Blot, value?: any): Blot;
}
declare class SyntaxCodeBlockContainer extends CodeBlockContainer {
forceNext?: boolean;
cachedText?: string | null;
attach(): void;
format(name: string, value: unknown): void;
formatAt(index: number, length: number, name: string, value: unknown): void;
highlight(highlight: (text: string, language: string) => Delta, forced?: boolean): void;
html(index: number, length: number): string;
optimize(context: Record<string, any>): void;
}
interface SyntaxOptions {
interval: number;
languages: {
key: string;
label: string;
}[];
hljs: any;
}
declare class Syntax extends Module<SyntaxOptions> {
static DEFAULTS: SyntaxOptions & {
hljs: any;
};
static register(): void;
languages: Record<string, true>;
constructor(quill: Quill, options: Partial<SyntaxOptions>);
initListener(): void;
initTimer(): void;
highlight(blot?: SyntaxCodeBlockContainer | null, force?: boolean): void;
highlightBlot(text: string, language?: string): Delta;
}
export { SyntaxCodeBlock as CodeBlock, CodeToken, Syntax as default };

View File

@@ -0,0 +1,332 @@
import Delta from 'quill-delta';
import { ClassAttributor, Scope } from 'parchment';
import Inline from '../blots/inline.js';
import Quill from '../core/quill.js';
import Module from '../core/module.js';
import { blockDelta } from '../blots/block.js';
import BreakBlot from '../blots/break.js';
import CursorBlot from '../blots/cursor.js';
import TextBlot, { escapeText } from '../blots/text.js';
import CodeBlock, { CodeBlockContainer } from '../formats/code.js';
import { traverse } from './clipboard.js';
const TokenAttributor = new ClassAttributor('code-token', 'hljs', {
scope: Scope.INLINE
});
class CodeToken extends Inline {
static formats(node, scroll) {
while (node != null && node !== scroll.domNode) {
if (node.classList && node.classList.contains(CodeBlock.className)) {
// @ts-expect-error
return super.formats(node, scroll);
}
// @ts-expect-error
node = node.parentNode;
}
return undefined;
}
constructor(scroll, domNode, value) {
// @ts-expect-error
super(scroll, domNode, value);
TokenAttributor.add(this.domNode, value);
}
format(format, value) {
if (format !== CodeToken.blotName) {
super.format(format, value);
} else if (value) {
TokenAttributor.add(this.domNode, value);
} else {
TokenAttributor.remove(this.domNode);
this.domNode.classList.remove(this.statics.className);
}
}
optimize() {
// @ts-expect-error
super.optimize(...arguments);
if (!TokenAttributor.value(this.domNode)) {
this.unwrap();
}
}
}
CodeToken.blotName = 'code-token';
CodeToken.className = 'ql-token';
class SyntaxCodeBlock extends CodeBlock {
static create(value) {
const domNode = super.create(value);
if (typeof value === 'string') {
domNode.setAttribute('data-language', value);
}
return domNode;
}
static formats(domNode) {
// @ts-expect-error
return domNode.getAttribute('data-language') || 'plain';
}
static register() {} // Syntax module will register
format(name, value) {
if (name === this.statics.blotName && value) {
// @ts-expect-error
this.domNode.setAttribute('data-language', value);
} else {
super.format(name, value);
}
}
replaceWith(name, value) {
this.formatAt(0, this.length(), CodeToken.blotName, false);
return super.replaceWith(name, value);
}
}
class SyntaxCodeBlockContainer extends CodeBlockContainer {
attach() {
super.attach();
this.forceNext = false;
// @ts-expect-error
this.scroll.emitMount(this);
}
format(name, value) {
if (name === SyntaxCodeBlock.blotName) {
this.forceNext = true;
this.children.forEach(child => {
// @ts-expect-error
child.format(name, value);
});
}
}
formatAt(index, length, name, value) {
if (name === SyntaxCodeBlock.blotName) {
this.forceNext = true;
}
super.formatAt(index, length, name, value);
}
highlight(highlight) {
let forced = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
if (this.children.head == null) return;
const nodes = Array.from(this.domNode.childNodes).filter(node => node !== this.uiNode);
const text = `${nodes.map(node => node.textContent).join('\n')}\n`;
const language = SyntaxCodeBlock.formats(this.children.head.domNode);
if (forced || this.forceNext || this.cachedText !== text) {
if (text.trim().length > 0 || this.cachedText == null) {
const oldDelta = this.children.reduce((delta, child) => {
// @ts-expect-error
return delta.concat(blockDelta(child, false));
}, new Delta());
const delta = highlight(text, language);
oldDelta.diff(delta).reduce((index, _ref) => {
let {
retain,
attributes
} = _ref;
// Should be all retains
if (!retain) return index;
if (attributes) {
Object.keys(attributes).forEach(format => {
if ([SyntaxCodeBlock.blotName, CodeToken.blotName].includes(format)) {
// @ts-expect-error
this.formatAt(index, retain, format, attributes[format]);
}
});
}
// @ts-expect-error
return index + retain;
}, 0);
}
this.cachedText = text;
this.forceNext = false;
}
}
html(index, length) {
const [codeBlock] = this.children.find(index);
const language = codeBlock ? SyntaxCodeBlock.formats(codeBlock.domNode) : 'plain';
return `<pre data-language="${language}">\n${escapeText(this.code(index, length))}\n</pre>`;
}
optimize(context) {
super.optimize(context);
if (this.parent != null && this.children.head != null && this.uiNode != null) {
const language = SyntaxCodeBlock.formats(this.children.head.domNode);
// @ts-expect-error
if (language !== this.uiNode.value) {
// @ts-expect-error
this.uiNode.value = language;
}
}
}
}
SyntaxCodeBlockContainer.allowedChildren = [SyntaxCodeBlock];
SyntaxCodeBlock.requiredContainer = SyntaxCodeBlockContainer;
SyntaxCodeBlock.allowedChildren = [CodeToken, CursorBlot, TextBlot, BreakBlot];
const highlight = (lib, language, text) => {
if (typeof lib.versionString === 'string') {
const majorVersion = lib.versionString.split('.')[0];
if (parseInt(majorVersion, 10) >= 11) {
return lib.highlight(text, {
language
}).value;
}
}
return lib.highlight(language, text).value;
};
class Syntax extends Module {
static register() {
Quill.register(CodeToken, true);
Quill.register(SyntaxCodeBlock, true);
Quill.register(SyntaxCodeBlockContainer, true);
}
constructor(quill, options) {
super(quill, options);
if (this.options.hljs == null) {
throw new Error('Syntax module requires highlight.js. Please include the library on the page before Quill.');
}
// @ts-expect-error Fix me later
this.languages = this.options.languages.reduce((memo, _ref2) => {
let {
key
} = _ref2;
memo[key] = true;
return memo;
}, {});
this.highlightBlot = this.highlightBlot.bind(this);
this.initListener();
this.initTimer();
}
initListener() {
this.quill.on(Quill.events.SCROLL_BLOT_MOUNT, blot => {
if (!(blot instanceof SyntaxCodeBlockContainer)) return;
const select = this.quill.root.ownerDocument.createElement('select');
// @ts-expect-error Fix me later
this.options.languages.forEach(_ref3 => {
let {
key,
label
} = _ref3;
const option = select.ownerDocument.createElement('option');
option.textContent = label;
option.setAttribute('value', key);
select.appendChild(option);
});
select.addEventListener('change', () => {
blot.format(SyntaxCodeBlock.blotName, select.value);
this.quill.root.focus(); // Prevent scrolling
this.highlight(blot, true);
});
if (blot.uiNode == null) {
blot.attachUI(select);
if (blot.children.head) {
select.value = SyntaxCodeBlock.formats(blot.children.head.domNode);
}
}
});
}
initTimer() {
let timer = null;
this.quill.on(Quill.events.SCROLL_OPTIMIZE, () => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
this.highlight();
timer = null;
}, this.options.interval);
});
}
highlight() {
let blot = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
let force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
if (this.quill.selection.composing) return;
this.quill.update(Quill.sources.USER);
const range = this.quill.getSelection();
const blots = blot == null ? this.quill.scroll.descendants(SyntaxCodeBlockContainer) : [blot];
blots.forEach(container => {
container.highlight(this.highlightBlot, force);
});
this.quill.update(Quill.sources.SILENT);
if (range != null) {
this.quill.setSelection(range, Quill.sources.SILENT);
}
}
highlightBlot(text) {
let language = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'plain';
language = this.languages[language] ? language : 'plain';
if (language === 'plain') {
return escapeText(text).split('\n').reduce((delta, line, i) => {
if (i !== 0) {
delta.insert('\n', {
[CodeBlock.blotName]: language
});
}
return delta.insert(line);
}, new Delta());
}
const container = this.quill.root.ownerDocument.createElement('div');
container.classList.add(CodeBlock.className);
container.innerHTML = highlight(this.options.hljs, language, text);
return traverse(this.quill.scroll, container, [(node, delta) => {
// @ts-expect-error
const value = TokenAttributor.value(node);
if (value) {
return delta.compose(new Delta().retain(delta.length(), {
[CodeToken.blotName]: value
}));
}
return delta;
}], [(node, delta) => {
// @ts-expect-error
return node.data.split('\n').reduce((memo, nodeText, i) => {
if (i !== 0) memo.insert('\n', {
[CodeBlock.blotName]: language
});
return memo.insert(nodeText);
}, delta);
}], new WeakMap());
}
}
Syntax.DEFAULTS = {
hljs: (() => {
return window.hljs;
})(),
interval: 1000,
languages: [{
key: 'plain',
label: 'Plain'
}, {
key: 'bash',
label: 'Bash'
}, {
key: 'cpp',
label: 'C++'
}, {
key: 'cs',
label: 'C#'
}, {
key: 'css',
label: 'CSS'
}, {
key: 'diff',
label: 'Diff'
}, {
key: 'xml',
label: 'HTML/XML'
}, {
key: 'java',
label: 'Java'
}, {
key: 'javascript',
label: 'JavaScript'
}, {
key: 'markdown',
label: 'Markdown'
}, {
key: 'php',
label: 'PHP'
}, {
key: 'python',
label: 'Python'
}, {
key: 'ruby',
label: 'Ruby'
}, {
key: 'sql',
label: 'SQL'
}]
};
export { SyntaxCodeBlock as CodeBlock, CodeToken, Syntax as default };
//# sourceMappingURL=syntax.js.map

File diff suppressed because one or more lines are too long

20
public/assets/quill/modules/table.d.ts vendored Normal file
View File

@@ -0,0 +1,20 @@
import Module from '../core/module.js';
import { TableCell, TableRow } from '../formats/table.js';
declare class Table extends Module {
static register(): void;
constructor(...args: ConstructorParameters<typeof Module>);
balanceTables(): void;
deleteColumn(): void;
deleteRow(): void;
deleteTable(): void;
getTable(range?: import("../core/selection.js").Range | null): [null, null, null, -1] | [Table, TableRow, TableCell, number];
insertColumn(offset: number): void;
insertColumnLeft(): void;
insertColumnRight(): void;
insertRow(offset: number): void;
insertRowAbove(): void;
insertRowBelow(): void;
insertTable(rows: number, columns: number): void;
listenBalanceCells(): void;
}
export default Table;

View File

@@ -0,0 +1,125 @@
import Delta from 'quill-delta';
import Quill from '../core/quill.js';
import Module from '../core/module.js';
import { TableCell, TableRow, TableBody, TableContainer, tableId } from '../formats/table.js';
class Table extends Module {
static register() {
Quill.register(TableCell);
Quill.register(TableRow);
Quill.register(TableBody);
Quill.register(TableContainer);
}
constructor() {
super(...arguments);
this.listenBalanceCells();
}
balanceTables() {
this.quill.scroll.descendants(TableContainer).forEach(table => {
table.balanceCells();
});
}
deleteColumn() {
const [table,, cell] = this.getTable();
if (cell == null) return;
// @ts-expect-error
table.deleteColumn(cell.cellOffset());
this.quill.update(Quill.sources.USER);
}
deleteRow() {
const [, row] = this.getTable();
if (row == null) return;
row.remove();
this.quill.update(Quill.sources.USER);
}
deleteTable() {
const [table] = this.getTable();
if (table == null) return;
// @ts-expect-error
const offset = table.offset();
// @ts-expect-error
table.remove();
this.quill.update(Quill.sources.USER);
this.quill.setSelection(offset, Quill.sources.SILENT);
}
getTable() {
let range = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.quill.getSelection();
if (range == null) return [null, null, null, -1];
const [cell, offset] = this.quill.getLine(range.index);
if (cell == null || cell.statics.blotName !== TableCell.blotName) {
return [null, null, null, -1];
}
const row = cell.parent;
const table = row.parent.parent;
// @ts-expect-error
return [table, row, cell, offset];
}
insertColumn(offset) {
const range = this.quill.getSelection();
if (!range) return;
const [table, row, cell] = this.getTable(range);
if (cell == null) return;
const column = cell.cellOffset();
table.insertColumn(column + offset);
this.quill.update(Quill.sources.USER);
let shift = row.rowOffset();
if (offset === 0) {
shift += 1;
}
this.quill.setSelection(range.index + shift, range.length, Quill.sources.SILENT);
}
insertColumnLeft() {
this.insertColumn(0);
}
insertColumnRight() {
this.insertColumn(1);
}
insertRow(offset) {
const range = this.quill.getSelection();
if (!range) return;
const [table, row, cell] = this.getTable(range);
if (cell == null) return;
const index = row.rowOffset();
table.insertRow(index + offset);
this.quill.update(Quill.sources.USER);
if (offset > 0) {
this.quill.setSelection(range, Quill.sources.SILENT);
} else {
this.quill.setSelection(range.index + row.children.length, range.length, Quill.sources.SILENT);
}
}
insertRowAbove() {
this.insertRow(0);
}
insertRowBelow() {
this.insertRow(1);
}
insertTable(rows, columns) {
const range = this.quill.getSelection();
if (range == null) return;
const delta = new Array(rows).fill(0).reduce(memo => {
const text = new Array(columns).fill('\n').join('');
return memo.insert(text, {
table: tableId()
});
}, new Delta().retain(range.index));
this.quill.updateContents(delta, Quill.sources.USER);
this.quill.setSelection(range.index, Quill.sources.SILENT);
this.balanceTables();
}
listenBalanceCells() {
this.quill.on(Quill.events.SCROLL_OPTIMIZE, mutations => {
mutations.some(mutation => {
if (['TD', 'TR', 'TBODY', 'TABLE'].includes(mutation.target.tagName)) {
this.quill.once(Quill.events.TEXT_CHANGE, (delta, old, source) => {
if (source !== Quill.sources.USER) return;
this.balanceTables();
});
return true;
}
return false;
});
});
}
}
export default Table;
//# sourceMappingURL=table.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,27 @@
import Delta from 'quill-delta';
import type { Op } from 'quill-delta';
import Module from '../core/module.js';
export type CellData = {
content?: Delta['ops'];
attributes?: Record<string, unknown>;
};
export type TableRowColumnOp = Omit<Op, 'insert'> & {
insert?: {
id: string;
};
};
export interface TableData {
rows?: Delta['ops'];
columns?: Delta['ops'];
cells?: Record<string, CellData>;
}
export declare const composePosition: (delta: Delta, index: number) => number | null;
export declare const tableHandler: {
compose(a: TableData, b: TableData, keepNull?: boolean): TableData;
transform(a: TableData, b: TableData, priority: boolean): TableData;
invert(change: TableData, base: TableData): TableData;
};
declare class TableEmbed extends Module {
static register(): void;
}
export default TableEmbed;

View File

@@ -0,0 +1,209 @@
import Delta, { OpIterator } from 'quill-delta';
import Module from '../core/module.js';
const parseCellIdentity = identity => {
const parts = identity.split(':');
return [Number(parts[0]) - 1, Number(parts[1]) - 1];
};
const stringifyCellIdentity = (row, column) => `${row + 1}:${column + 1}`;
export const composePosition = (delta, index) => {
let newIndex = index;
const thisIter = new OpIterator(delta.ops);
let offset = 0;
while (thisIter.hasNext() && offset <= newIndex) {
const length = thisIter.peekLength();
const nextType = thisIter.peekType();
thisIter.next();
switch (nextType) {
case 'delete':
if (length > newIndex - offset) {
return null;
}
newIndex -= length;
break;
case 'insert':
newIndex += length;
offset += length;
break;
default:
offset += length;
break;
}
}
return newIndex;
};
const compactCellData = _ref => {
let {
content,
attributes
} = _ref;
const data = {};
if (content.length() > 0) {
data.content = content.ops;
}
if (attributes && Object.keys(attributes).length > 0) {
data.attributes = attributes;
}
return Object.keys(data).length > 0 ? data : null;
};
const compactTableData = _ref2 => {
let {
rows,
columns,
cells
} = _ref2;
const data = {};
if (rows.length() > 0) {
data.rows = rows.ops;
}
if (columns.length() > 0) {
data.columns = columns.ops;
}
if (Object.keys(cells).length) {
data.cells = cells;
}
return data;
};
const reindexCellIdentities = (cells, _ref3) => {
let {
rows,
columns
} = _ref3;
const reindexedCells = {};
Object.keys(cells).forEach(identity => {
let [row, column] = parseCellIdentity(identity);
// @ts-expect-error Fix me later
row = composePosition(rows, row);
// @ts-expect-error Fix me later
column = composePosition(columns, column);
if (row !== null && column !== null) {
const newPosition = stringifyCellIdentity(row, column);
reindexedCells[newPosition] = cells[identity];
}
}, false);
return reindexedCells;
};
export const tableHandler = {
compose(a, b, keepNull) {
const rows = new Delta(a.rows || []).compose(new Delta(b.rows || []));
const columns = new Delta(a.columns || []).compose(new Delta(b.columns || []));
const cells = reindexCellIdentities(a.cells || {}, {
rows: new Delta(b.rows || []),
columns: new Delta(b.columns || [])
});
Object.keys(b.cells || {}).forEach(identity => {
const aCell = cells[identity] || {};
// @ts-expect-error Fix me later
const bCell = b.cells[identity];
const content = new Delta(aCell.content || []).compose(new Delta(bCell.content || []));
const attributes = Delta.AttributeMap.compose(aCell.attributes, bCell.attributes, keepNull);
const cell = compactCellData({
content,
attributes
});
if (cell) {
cells[identity] = cell;
} else {
delete cells[identity];
}
});
return compactTableData({
rows,
columns,
cells
});
},
transform(a, b, priority) {
const aDeltas = {
rows: new Delta(a.rows || []),
columns: new Delta(a.columns || [])
};
const bDeltas = {
rows: new Delta(b.rows || []),
columns: new Delta(b.columns || [])
};
const rows = aDeltas.rows.transform(bDeltas.rows, priority);
const columns = aDeltas.columns.transform(bDeltas.columns, priority);
const cells = reindexCellIdentities(b.cells || {}, {
rows: bDeltas.rows.transform(aDeltas.rows, !priority),
columns: bDeltas.columns.transform(aDeltas.columns, !priority)
});
Object.keys(a.cells || {}).forEach(identity => {
let [row, column] = parseCellIdentity(identity);
// @ts-expect-error Fix me later
row = composePosition(rows, row);
// @ts-expect-error Fix me later
column = composePosition(columns, column);
if (row !== null && column !== null) {
const newIdentity = stringifyCellIdentity(row, column);
// @ts-expect-error Fix me later
const aCell = a.cells[identity];
const bCell = cells[newIdentity];
if (bCell) {
const content = new Delta(aCell.content || []).transform(new Delta(bCell.content || []), priority);
const attributes = Delta.AttributeMap.transform(aCell.attributes, bCell.attributes, priority);
const cell = compactCellData({
content,
attributes
});
if (cell) {
cells[newIdentity] = cell;
} else {
delete cells[newIdentity];
}
}
}
});
return compactTableData({
rows,
columns,
cells
});
},
invert(change, base) {
const rows = new Delta(change.rows || []).invert(new Delta(base.rows || []));
const columns = new Delta(change.columns || []).invert(new Delta(base.columns || []));
const cells = reindexCellIdentities(change.cells || {}, {
rows,
columns
});
Object.keys(cells).forEach(identity => {
const changeCell = cells[identity] || {};
const baseCell = (base.cells || {})[identity] || {};
const content = new Delta(changeCell.content || []).invert(new Delta(baseCell.content || []));
const attributes = Delta.AttributeMap.invert(changeCell.attributes, baseCell.attributes);
const cell = compactCellData({
content,
attributes
});
if (cell) {
cells[identity] = cell;
} else {
delete cells[identity];
}
});
// Cells may be removed when their row or column is removed
// by row/column deltas. We should add them back.
Object.keys(base.cells || {}).forEach(identity => {
const [row, column] = parseCellIdentity(identity);
if (composePosition(new Delta(change.rows || []), row) === null || composePosition(new Delta(change.columns || []), column) === null) {
// @ts-expect-error Fix me later
cells[identity] = base.cells[identity];
}
});
return compactTableData({
rows,
columns,
cells
});
}
};
class TableEmbed extends Module {
static register() {
Delta.registerEmbed('table-embed', tableHandler);
}
}
export default TableEmbed;
//# sourceMappingURL=tableEmbed.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,24 @@
import Quill from '../core/quill.js';
import Module from '../core/module.js';
import type { Range } from '../core/selection.js';
type Handler = (this: Toolbar, value: any) => void;
export type ToolbarConfig = Array<string[] | Array<string | Record<string, unknown>>>;
export interface ToolbarProps {
container?: HTMLElement | ToolbarConfig | null;
handlers?: Record<string, Handler>;
option?: number;
module?: boolean;
theme?: boolean;
}
declare class Toolbar extends Module<ToolbarProps> {
static DEFAULTS: ToolbarProps;
container?: HTMLElement | null;
controls: [string, HTMLElement][];
handlers: Record<string, Handler>;
constructor(quill: Quill, options: Partial<ToolbarProps>);
addHandler(format: string, handler: Handler): void;
attach(input: HTMLElement): void;
update(range: Range | null): void;
}
declare function addControls(container: HTMLElement, groups: (string | Record<string, unknown>)[][] | (string | Record<string, unknown>)[]): void;
export { Toolbar as default, addControls };

View File

@@ -0,0 +1,265 @@
import Delta from 'quill-delta';
import { EmbedBlot, Scope } from 'parchment';
import Quill from '../core/quill.js';
import logger from '../core/logger.js';
import Module from '../core/module.js';
const debug = logger('quill:toolbar');
class Toolbar extends Module {
constructor(quill, options) {
super(quill, options);
if (Array.isArray(this.options.container)) {
const container = document.createElement('div');
container.setAttribute('role', 'toolbar');
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)) {
debug.error('Container required for toolbar', this.options);
return;
}
this.container.classList.add('ql-toolbar');
this.controls = [];
this.handlers = {};
if (this.options.handlers) {
Object.keys(this.options.handlers).forEach(format => {
const handler = this.options.handlers?.[format];
if (handler) {
this.addHandler(format, handler);
}
});
}
Array.from(this.container.querySelectorAll('button, select')).forEach(input => {
// @ts-expect-error
this.attach(input);
});
this.quill.on(Quill.events.EDITOR_CHANGE, () => {
const [range] = this.quill.selection.getRange(); // quill.getSelection triggers update
this.update(range);
});
}
addHandler(format, handler) {
this.handlers[format] = handler;
}
attach(input) {
let format = Array.from(input.classList).find(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 && this.quill.scroll.query(format) == null) {
debug.warn('ignoring attaching to nonexistent format', format, input);
return;
}
const eventName = input.tagName === 'SELECT' ? 'change' : 'click';
input.addEventListener(eventName, e => {
let value;
if (input.tagName === 'SELECT') {
// @ts-expect-error
if (input.selectedIndex < 0) return;
// @ts-expect-error
const 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 {
// @ts-expect-error
value = input.value || !input.hasAttribute('value');
}
e.preventDefault();
}
this.quill.focus();
const [range] = this.quill.selection.getRange();
if (this.handlers[format] != null) {
this.handlers[format].call(this, value);
} else if (
// @ts-expect-error
this.quill.scroll.query(format).prototype instanceof EmbedBlot) {
value = prompt(`Enter ${format}`); // eslint-disable-line no-alert
if (!value) return;
this.quill.updateContents(new Delta()
// @ts-expect-error Fix me later
.retain(range.index)
// @ts-expect-error Fix me later
.delete(range.length).insert({
[format]: value
}), Quill.sources.USER);
} else {
this.quill.format(format, value, Quill.sources.USER);
}
this.update(range);
});
this.controls.push([format, input]);
}
update(range) {
const formats = range == null ? {} : this.quill.getFormat(range);
this.controls.forEach(pair => {
const [format, input] = pair;
if (input.tagName === 'SELECT') {
let option = null;
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) {
// @ts-expect-error TODO fix me later
input.value = ''; // TODO make configurable?
// @ts-expect-error TODO fix me later
input.selectedIndex = -1;
} else {
option.selected = true;
}
} else if (range == null) {
input.classList.remove('ql-active');
input.setAttribute('aria-pressed', 'false');
} else if (input.hasAttribute('value')) {
// both being null should match (default values)
// '1' should match with 1 (headers)
const value = formats[format];
const isActive = value === input.getAttribute('value') || value != null && value.toString() === input.getAttribute('value') || value == null && !input.getAttribute('value');
input.classList.toggle('ql-active', isActive);
input.setAttribute('aria-pressed', isActive.toString());
} else {
const isActive = formats[format] != null;
input.classList.toggle('ql-active', isActive);
input.setAttribute('aria-pressed', isActive.toString());
}
});
}
}
Toolbar.DEFAULTS = {};
function addButton(container, format, value) {
const input = document.createElement('button');
input.setAttribute('type', 'button');
input.classList.add(`ql-${format}`);
input.setAttribute('aria-pressed', 'false');
if (value != null) {
input.value = value;
input.setAttribute('aria-label', `${format}: ${value}`);
} else {
input.setAttribute('aria-label', format);
}
container.appendChild(input);
}
function addControls(container, groups) {
if (!Array.isArray(groups[0])) {
// @ts-expect-error
groups = [groups];
}
groups.forEach(controls => {
const group = document.createElement('span');
group.classList.add('ql-formats');
controls.forEach(control => {
if (typeof control === 'string') {
addButton(group, control);
} else {
const format = Object.keys(control)[0];
const value = control[format];
if (Array.isArray(value)) {
addSelect(group, format, value);
} else {
addButton(group, format, value);
}
}
});
container.appendChild(group);
});
}
function addSelect(container, format, values) {
const input = document.createElement('select');
input.classList.add(`ql-${format}`);
values.forEach(value => {
const option = document.createElement('option');
if (value !== false) {
option.setAttribute('value', String(value));
} else {
option.setAttribute('selected', 'selected');
}
input.appendChild(option);
});
container.appendChild(input);
}
Toolbar.DEFAULTS = {
container: null,
handlers: {
clean() {
const range = this.quill.getSelection();
if (range == null) return;
if (range.length === 0) {
const formats = this.quill.getFormat();
Object.keys(formats).forEach(name => {
// Clean functionality in existing apps only clean inline formats
if (this.quill.scroll.query(name, Scope.INLINE) != null) {
this.quill.format(name, false, Quill.sources.USER);
}
});
} else {
this.quill.removeFormat(range.index, range.length, Quill.sources.USER);
}
},
direction(value) {
const {
align
} = this.quill.getFormat();
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(value) {
const range = this.quill.getSelection();
// @ts-expect-error
const formats = this.quill.getFormat(range);
// @ts-expect-error
const indent = parseInt(formats.indent || 0, 10);
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(value) {
if (value === true) {
value = prompt('Enter link URL:'); // eslint-disable-line no-alert
}
this.quill.format('link', value, Quill.sources.USER);
},
list(value) {
const range = this.quill.getSelection();
// @ts-expect-error
const 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 };
//# sourceMappingURL=toolbar.js.map

File diff suppressed because one or more lines are too long

19
public/assets/quill/modules/uiNode.d.ts vendored Normal file
View File

@@ -0,0 +1,19 @@
import Module from '../core/module.js';
import Quill from '../core/quill.js';
export declare const TTL_FOR_VALID_SELECTION_CHANGE = 100;
declare class UINode extends Module {
isListening: boolean;
selectionChangeDeadline: number;
constructor(quill: Quill, options: Record<string, never>);
private handleArrowKeys;
private handleNavigationShortcuts;
/**
* We only listen to the `selectionchange` event when
* there is an intention of moving the caret to the beginning using shortcuts.
* This is primarily implemented to prevent infinite loops, as we are changing
* the selection within the handler of a `selectionchange` event.
*/
private ensureListeningToSelectionChange;
private handleSelectionChange;
}
export default UINode;

View File

@@ -0,0 +1,95 @@
import { ParentBlot } from 'parchment';
import Module from '../core/module.js';
import Quill from '../core/quill.js';
const isMac = /Mac/i.test(navigator.platform);
// Export for testing
export const TTL_FOR_VALID_SELECTION_CHANGE = 100;
// A loose check to determine if the shortcut can move the caret before a UI node:
// <ANY_PARENT>[CARET]<div class="ql-ui"></div>[CONTENT]</ANY_PARENT>
const canMoveCaretBeforeUINode = event => {
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight' ||
// RTL scripts or moving from the end of the previous line
event.key === 'ArrowUp' || event.key === 'ArrowDown' || event.key === 'Home') {
return true;
}
if (isMac && event.key === 'a' && event.ctrlKey === true) {
return true;
}
return false;
};
class UINode extends Module {
isListening = false;
selectionChangeDeadline = 0;
constructor(quill, options) {
super(quill, options);
this.handleArrowKeys();
this.handleNavigationShortcuts();
}
handleArrowKeys() {
this.quill.keyboard.addBinding({
key: ['ArrowLeft', 'ArrowRight'],
offset: 0,
shiftKey: null,
handler(range, _ref) {
let {
line,
event
} = _ref;
if (!(line instanceof ParentBlot) || !line.uiNode) {
return true;
}
const isRTL = getComputedStyle(line.domNode)['direction'] === 'rtl';
if (isRTL && event.key !== 'ArrowRight' || !isRTL && event.key !== 'ArrowLeft') {
return true;
}
this.quill.setSelection(range.index - 1, range.length + (event.shiftKey ? 1 : 0), Quill.sources.USER);
return false;
}
});
}
handleNavigationShortcuts() {
this.quill.root.addEventListener('keydown', event => {
if (!event.defaultPrevented && canMoveCaretBeforeUINode(event)) {
this.ensureListeningToSelectionChange();
}
});
}
/**
* We only listen to the `selectionchange` event when
* there is an intention of moving the caret to the beginning using shortcuts.
* This is primarily implemented to prevent infinite loops, as we are changing
* the selection within the handler of a `selectionchange` event.
*/
ensureListeningToSelectionChange() {
this.selectionChangeDeadline = Date.now() + TTL_FOR_VALID_SELECTION_CHANGE;
if (this.isListening) return;
this.isListening = true;
const listener = () => {
this.isListening = false;
if (Date.now() <= this.selectionChangeDeadline) {
this.handleSelectionChange();
}
};
document.addEventListener('selectionchange', listener, {
once: true
});
}
handleSelectionChange() {
const selection = document.getSelection();
if (!selection) return;
const range = selection.getRangeAt(0);
if (range.collapsed !== true || range.startOffset !== 0) return;
const line = this.quill.scroll.find(range.startContainer);
if (!(line instanceof ParentBlot) || !line.uiNode) return;
const newRange = document.createRange();
newRange.setStartAfter(line.uiNode);
newRange.setEndAfter(line.uiNode);
selection.removeAllRanges();
selection.addRange(newRange);
}
}
export default UINode;
//# sourceMappingURL=uiNode.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,15 @@
import type Quill from '../core/quill.js';
import Module from '../core/module.js';
import type { Range } from '../core/selection.js';
interface UploaderOptions {
mimetypes: string[];
handler: (this: {
quill: Quill;
}, range: Range, files: File[]) => void;
}
declare class Uploader extends Module<UploaderOptions> {
static DEFAULTS: UploaderOptions;
constructor(quill: Quill, options: Partial<UploaderOptions>);
upload(range: Range, files: FileList | File[]): void;
}
export default Uploader;

View File

@@ -0,0 +1,69 @@
import Delta from 'quill-delta';
import Emitter from '../core/emitter.js';
import Module from '../core/module.js';
class Uploader extends Module {
constructor(quill, options) {
super(quill, options);
quill.root.addEventListener('drop', e => {
e.preventDefault();
let native = null;
if (document.caretRangeFromPoint) {
native = document.caretRangeFromPoint(e.clientX, e.clientY);
// @ts-expect-error
} else if (document.caretPositionFromPoint) {
// @ts-expect-error
const position = document.caretPositionFromPoint(e.clientX, e.clientY);
native = document.createRange();
native.setStart(position.offsetNode, position.offset);
native.setEnd(position.offsetNode, position.offset);
}
const normalized = native && quill.selection.normalizeNative(native);
if (normalized) {
const range = quill.selection.normalizedToRange(normalized);
if (e.dataTransfer?.files) {
this.upload(range, e.dataTransfer.files);
}
}
});
}
upload(range, files) {
const uploads = [];
Array.from(files).forEach(file => {
if (file && this.options.mimetypes?.includes(file.type)) {
uploads.push(file);
}
});
if (uploads.length > 0) {
// @ts-expect-error Fix me later
this.options.handler.call(this, range, uploads);
}
}
}
Uploader.DEFAULTS = {
mimetypes: ['image/png', 'image/jpeg'],
handler(range, files) {
if (!this.quill.scroll.query('image')) {
return;
}
const promises = files.map(file => {
return new Promise(resolve => {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.readAsDataURL(file);
});
});
Promise.all(promises).then(images => {
const update = images.reduce((delta, image) => {
return delta.insert({
image
});
}, new Delta().retain(range.index).delete(range.length));
this.quill.updateContents(update, Emitter.sources.USER);
this.quill.setSelection(range.index + images.length, Emitter.sources.SILENT);
});
}
};
export default Uploader;
//# sourceMappingURL=uploader.js.map

File diff suppressed because one or more lines are too long