typewriter-editor
Version:
A rich text editor using the Delta format with decorations and rendered with a tiny virtual dom
119 lines (118 loc) • 5.05 kB
JavaScript
import { Delta, Line, isEqual, normalizeRange } from '@typewriter/document';
import { Editor } from '../Editor';
import { Source } from '../Source';
import { deltaFromHTML } from '../rendering/html';
const dontFixNewline = { dontFixNewline: true };
const ignoreId = { excludeProps: new Set(['id']) };
export class PasteEvent extends Event {
delta;
html;
text;
constructor(type, init) {
super(type, init);
this.delta = init.delta;
this.html = init.html;
this.text = init.text;
}
}
export function paste(editor, options) {
const allowHTMLPaste = options?.allowHTMLPaste ?? true;
function paste({ selection, text, html }) {
const { doc } = editor;
selection = selection || doc.selection;
selection = selection && normalizeRange(selection);
if (!selection)
return;
const [at, to] = selection;
let delta;
if (!html) {
if (!text)
return;
delta = new Delta().insert(text.replace(/\xA0/g, ' ').replace(/\r\n/g, '\n'));
}
else if (options?.htmlParser) {
delta = options.htmlParser(editor, html);
}
else {
delta = deltaFromHTML(editor, html, { possiblePartial: true });
}
const hasLines = delta.filter(op => typeof op.insert === 'string' && op.insert.includes('\n')).length > 0;
let length = delta.length();
if (hasLines) {
// check the boundaries to see if they can be merged with the current line or need to make a new line
let lines = Line.fromDelta(delta, doc.byId);
delta = Line.toDelta(lines);
length = delta.length();
const startLine = doc.getLineAt(at);
const endLine = doc.getLineAt(to);
const startAttrs = getAttributes(startLine);
const endAttrs = startLine === endLine ? startAttrs : getAttributes(endLine);
// plain text should merge better with the existing content
if (!html) {
lines = lines.map(line => ({ ...line, attributes: startLine.attributes }));
if (startAttrs !== endAttrs)
lines[lines.length - 1].attributes = endLine.attributes;
}
const pastedStartLine = lines[0];
const pastedStartAttrs = getAttributes(pastedStartLine);
const pastedEndLine = lines[lines.length - 1];
const pastedEndAttrs = pastedStartLine === pastedEndLine ? pastedStartAttrs : getAttributes(pastedEndLine);
if (at !== doc.getLineRange(startLine)[0] && !isEqual(startAttrs, pastedStartAttrs, ignoreId)) {
delta = new Delta().insert('\n', startAttrs).concat(delta);
length++;
}
const lastInsert = delta.ops[delta.ops.length - 1].insert;
const endsInNewline = typeof lastInsert === 'string' && lastInsert.endsWith('\n');
// Does the last line of a multi-line paste merge with the last line?
if (endsInNewline && to !== doc.getLineRange(endLine)[1] && isEqual(endAttrs, pastedEndAttrs, ignoreId)) {
// Remove the trailing newline to merge with the last line
delta = delta.slice(0, --length);
// If a multi-line paste is inserted at the end of a line, delete the line's newline and let the inserted one take
}
else if (endsInNewline && to === doc.getLineRange(endLine)[1] - 1) {
delta.delete(1);
length--;
}
}
const viewEvent = new PasteEvent('paste', { delta, html, text, cancelable: true });
editor.dispatchEvent(viewEvent);
delta = viewEvent.delta;
if (!viewEvent.defaultPrevented) {
if (delta && delta.ops.length) {
const change = editor.change.delete(selection, hasLines ? dontFixNewline : undefined);
change.insertContent(at, delta).select(at + length);
editor.update(change, Source.paste);
}
else if (at !== to) {
editor.delete([at, to]);
}
}
}
function onPaste(event) {
if (!editor.enabled || !editor.doc.selection || event.defaultPrevented)
return;
event.preventDefault();
const dataTransfer = event.clipboardData;
const { doc } = editor;
if (!dataTransfer || !doc.selection)
return;
const html = allowHTMLPaste ? dataTransfer.getData('text/html') : undefined;
const text = dataTransfer.getData('text/plain');
paste({ text, html });
}
return {
commands: {
paste,
},
init() {
editor.root.addEventListener('paste', onPaste);
},
destroy() {
editor.root.removeEventListener('paste', onPaste);
},
};
}
function getAttributes(line) {
const { id, ...attributes } = line.attributes;
return attributes;
}