UNPKG

quill

Version:

Your powerful, rich text editor

404 lines 14.9 kB
import { cloneDeep, isEqual, merge } from 'lodash-es'; import { LeafBlot, EmbedBlot, Scope, ParentBlot } from 'parchment'; import Delta, { AttributeMap, Op } from 'quill-delta'; import Block, { BlockEmbed, bubbleFormats } from '../blots/block.js'; import Break from '../blots/break.js'; import CursorBlot from '../blots/cursor.js'; import TextBlot, { escapeText } from '../blots/text.js'; import { Range } from './selection.js'; const ASCII = /^[ -~]*$/; class Editor { constructor(scroll) { this.scroll = scroll; this.delta = this.getDelta(); } applyDelta(delta) { this.scroll.update(); let scrollLength = this.scroll.length(); this.scroll.batchStart(); const normalizedDelta = normalizeDelta(delta); const deleteDelta = new Delta(); const normalizedOps = splitOpLines(normalizedDelta.ops.slice()); normalizedOps.reduce((index, op) => { const length = Op.length(op); let attributes = op.attributes || {}; let isImplicitNewlinePrepended = false; let isImplicitNewlineAppended = false; if (op.insert != null) { deleteDelta.retain(length); if (typeof op.insert === 'string') { const text = op.insert; isImplicitNewlineAppended = !text.endsWith('\n') && (scrollLength <= index || !!this.scroll.descendant(BlockEmbed, index)[0]); this.scroll.insertAt(index, text); const [line, offset] = this.scroll.line(index); let formats = merge({}, bubbleFormats(line)); if (line instanceof Block) { const [leaf] = line.descendant(LeafBlot, offset); if (leaf) { formats = merge(formats, bubbleFormats(leaf)); } } attributes = AttributeMap.diff(formats, attributes) || {}; } else if (typeof op.insert === 'object') { const key = Object.keys(op.insert)[0]; // There should only be one key if (key == null) return index; const isInlineEmbed = this.scroll.query(key, Scope.INLINE) != null; if (isInlineEmbed) { if (scrollLength <= index || !!this.scroll.descendant(BlockEmbed, index)[0]) { isImplicitNewlineAppended = true; } } else if (index > 0) { const [leaf, offset] = this.scroll.descendant(LeafBlot, index - 1); if (leaf instanceof TextBlot) { const text = leaf.value(); if (text[offset] !== '\n') { isImplicitNewlinePrepended = true; } } else if (leaf instanceof EmbedBlot && leaf.statics.scope === Scope.INLINE_BLOT) { isImplicitNewlinePrepended = true; } } this.scroll.insertAt(index, key, op.insert[key]); if (isInlineEmbed) { const [leaf] = this.scroll.descendant(LeafBlot, index); if (leaf) { const formats = merge({}, bubbleFormats(leaf)); attributes = AttributeMap.diff(formats, attributes) || {}; } } } scrollLength += length; } else { deleteDelta.push(op); if (op.retain !== null && typeof op.retain === 'object') { const key = Object.keys(op.retain)[0]; if (key == null) return index; this.scroll.updateEmbedAt(index, key, op.retain[key]); } } Object.keys(attributes).forEach(name => { this.scroll.formatAt(index, length, name, attributes[name]); }); const prependedLength = isImplicitNewlinePrepended ? 1 : 0; const addedLength = isImplicitNewlineAppended ? 1 : 0; scrollLength += prependedLength + addedLength; deleteDelta.retain(prependedLength); deleteDelta.delete(addedLength); return index + length + prependedLength + addedLength; }, 0); deleteDelta.reduce((index, op) => { if (typeof op.delete === 'number') { this.scroll.deleteAt(index, op.delete); return index; } return index + Op.length(op); }, 0); this.scroll.batchEnd(); this.scroll.optimize(); return this.update(normalizedDelta); } deleteText(index, length) { this.scroll.deleteAt(index, length); return this.update(new Delta().retain(index).delete(length)); } formatLine(index, length) { let formats = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; this.scroll.update(); Object.keys(formats).forEach(format => { this.scroll.lines(index, Math.max(length, 1)).forEach(line => { line.format(format, formats[format]); }); }); this.scroll.optimize(); const delta = new Delta().retain(index).retain(length, cloneDeep(formats)); return this.update(delta); } formatText(index, length) { let formats = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; Object.keys(formats).forEach(format => { this.scroll.formatAt(index, length, format, formats[format]); }); const delta = new Delta().retain(index).retain(length, cloneDeep(formats)); return this.update(delta); } getContents(index, length) { return this.delta.slice(index, index + length); } getDelta() { return this.scroll.lines().reduce((delta, line) => { return delta.concat(line.delta()); }, new Delta()); } getFormat(index) { let length = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; let lines = []; let leaves = []; if (length === 0) { this.scroll.path(index).forEach(path => { const [blot] = path; if (blot instanceof Block) { lines.push(blot); } else if (blot instanceof LeafBlot) { leaves.push(blot); } }); } else { lines = this.scroll.lines(index, length); leaves = this.scroll.descendants(LeafBlot, index, length); } const [lineFormats, leafFormats] = [lines, leaves].map(blots => { const blot = blots.shift(); if (blot == null) return {}; let formats = bubbleFormats(blot); while (Object.keys(formats).length > 0) { const blot = blots.shift(); if (blot == null) return formats; formats = combineFormats(bubbleFormats(blot), formats); } return formats; }); return { ...lineFormats, ...leafFormats }; } getHTML(index, length) { const [line, lineOffset] = this.scroll.line(index); if (line) { const lineLength = line.length(); const isWithinLine = line.length() >= lineOffset + length; if (isWithinLine && !(lineOffset === 0 && length === lineLength)) { return convertHTML(line, lineOffset, length, true); } return convertHTML(this.scroll, index, length, true); } return ''; } getText(index, length) { return this.getContents(index, length).filter(op => typeof op.insert === 'string').map(op => op.insert).join(''); } insertContents(index, contents) { const normalizedDelta = normalizeDelta(contents); const change = new Delta().retain(index).concat(normalizedDelta); this.scroll.insertContents(index, normalizedDelta); return this.update(change); } insertEmbed(index, embed, value) { this.scroll.insertAt(index, embed, value); return this.update(new Delta().retain(index).insert({ [embed]: value })); } insertText(index, text) { let formats = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); this.scroll.insertAt(index, text); Object.keys(formats).forEach(format => { this.scroll.formatAt(index, text.length, format, formats[format]); }); return this.update(new Delta().retain(index).insert(text, cloneDeep(formats))); } isBlank() { if (this.scroll.children.length === 0) return true; if (this.scroll.children.length > 1) return false; const blot = this.scroll.children.head; if (blot?.statics.blotName !== Block.blotName) return false; const block = blot; if (block.children.length > 1) return false; return block.children.head instanceof Break; } removeFormat(index, length) { const text = this.getText(index, length); const [line, offset] = this.scroll.line(index + length); let suffixLength = 0; let suffix = new Delta(); if (line != null) { suffixLength = line.length() - offset; suffix = line.delta().slice(offset, offset + suffixLength - 1).insert('\n'); } const contents = this.getContents(index, length + suffixLength); const diff = contents.diff(new Delta().insert(text).concat(suffix)); const delta = new Delta().retain(index).concat(diff); return this.applyDelta(delta); } update(change) { let mutations = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; let selectionInfo = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : undefined; const oldDelta = this.delta; if (mutations.length === 1 && mutations[0].type === 'characterData' && // @ts-expect-error Fix me later mutations[0].target.data.match(ASCII) && this.scroll.find(mutations[0].target)) { // Optimization for character changes const textBlot = this.scroll.find(mutations[0].target); const formats = bubbleFormats(textBlot); const index = textBlot.offset(this.scroll); // @ts-expect-error Fix me later const oldValue = mutations[0].oldValue.replace(CursorBlot.CONTENTS, ''); const oldText = new Delta().insert(oldValue); // @ts-expect-error const newText = new Delta().insert(textBlot.value()); const relativeSelectionInfo = selectionInfo && { oldRange: shiftRange(selectionInfo.oldRange, -index), newRange: shiftRange(selectionInfo.newRange, -index) }; const diffDelta = new Delta().retain(index).concat(oldText.diff(newText, relativeSelectionInfo)); change = diffDelta.reduce((delta, op) => { if (op.insert) { return delta.insert(op.insert, formats); } return delta.push(op); }, new Delta()); this.delta = oldDelta.compose(change); } else { this.delta = this.getDelta(); if (!change || !isEqual(oldDelta.compose(change), this.delta)) { change = oldDelta.diff(this.delta, selectionInfo); } } return change; } } function convertListHTML(items, lastIndent, types) { if (items.length === 0) { const [endTag] = getListType(types.pop()); if (lastIndent <= 0) { return `</li></${endTag}>`; } return `</li></${endTag}>${convertListHTML([], lastIndent - 1, types)}`; } const [{ child, offset, length, indent, type }, ...rest] = items; const [tag, attribute] = getListType(type); if (indent > lastIndent) { types.push(type); if (indent === lastIndent + 1) { return `<${tag}><li${attribute}>${convertHTML(child, offset, length)}${convertListHTML(rest, indent, types)}`; } return `<${tag}><li>${convertListHTML(items, lastIndent + 1, types)}`; } const previousType = types[types.length - 1]; if (indent === lastIndent && type === previousType) { return `</li><li${attribute}>${convertHTML(child, offset, length)}${convertListHTML(rest, indent, types)}`; } const [endTag] = getListType(types.pop()); return `</li></${endTag}>${convertListHTML(items, lastIndent - 1, types)}`; } function convertHTML(blot, index, length) { let isRoot = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; if ('html' in blot && typeof blot.html === 'function') { return blot.html(index, length); } if (blot instanceof TextBlot) { const escapedText = escapeText(blot.value().slice(index, index + length)); return escapedText.replaceAll(' ', '&nbsp;'); } if (blot instanceof ParentBlot) { // TODO fix API if (blot.statics.blotName === 'list-container') { const items = []; blot.children.forEachAt(index, length, (child, offset, childLength) => { const formats = 'formats' in child && typeof child.formats === 'function' ? child.formats() : {}; items.push({ child, offset, length: childLength, indent: formats.indent || 0, type: formats.list }); }); return convertListHTML(items, -1, []); } const parts = []; blot.children.forEachAt(index, length, (child, offset, childLength) => { parts.push(convertHTML(child, offset, childLength)); }); if (isRoot || blot.statics.blotName === 'list') { return parts.join(''); } const { outerHTML, innerHTML } = blot.domNode; const [start, end] = outerHTML.split(`>${innerHTML}<`); // TODO cleanup if (start === '<table') { return `<table style="border: 1px solid #000;">${parts.join('')}<${end}`; } return `${start}>${parts.join('')}<${end}`; } return blot.domNode instanceof Element ? blot.domNode.outerHTML : ''; } function combineFormats(formats, combined) { return Object.keys(combined).reduce((merged, name) => { if (formats[name] == null) return merged; const combinedValue = combined[name]; if (combinedValue === formats[name]) { merged[name] = combinedValue; } else if (Array.isArray(combinedValue)) { if (combinedValue.indexOf(formats[name]) < 0) { merged[name] = combinedValue.concat([formats[name]]); } else { // If style already exists, don't add to an array, but don't lose other styles merged[name] = combinedValue; } } else { merged[name] = [combinedValue, formats[name]]; } return merged; }, {}); } function getListType(type) { const tag = type === 'ordered' ? 'ol' : 'ul'; switch (type) { case 'checked': return [tag, ' data-list="checked"']; case 'unchecked': return [tag, ' data-list="unchecked"']; default: return [tag, '']; } } function normalizeDelta(delta) { return delta.reduce((normalizedDelta, op) => { if (typeof op.insert === 'string') { const text = op.insert.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); return normalizedDelta.insert(text, op.attributes); } return normalizedDelta.push(op); }, new Delta()); } function shiftRange(_ref, amount) { let { index, length } = _ref; return new Range(index + amount, length); } function splitOpLines(ops) { const split = []; ops.forEach(op => { if (typeof op.insert === 'string') { const lines = op.insert.split('\n'); lines.forEach((line, index) => { if (index) split.push({ insert: '\n', attributes: op.attributes }); if (line) split.push({ insert: line, attributes: op.attributes }); }); } else { split.push(op); } }); return split; } export default Editor; //# sourceMappingURL=editor.js.map