268 lines
8.9 KiB
JavaScript
268 lines
8.9 KiB
JavaScript
|
import Delta from 'quill-delta';
|
||
|
import DeltaOp from 'quill-delta/lib/op';
|
||
|
import Parchment from 'parchment';
|
||
|
import CodeBlock from '../formats/code';
|
||
|
import CursorBlot from '../blots/cursor';
|
||
|
import Block, { bubbleFormats } from '../blots/block';
|
||
|
import Break from '../blots/break';
|
||
|
import clone from 'clone';
|
||
|
import equal from 'deep-equal';
|
||
|
import extend from 'extend';
|
||
|
|
||
|
|
||
|
const ASCII = /^[ -~]*$/;
|
||
|
|
||
|
|
||
|
class Editor {
|
||
|
constructor(scroll) {
|
||
|
this.scroll = scroll;
|
||
|
this.delta = this.getDelta();
|
||
|
}
|
||
|
|
||
|
applyDelta(delta) {
|
||
|
let consumeNextNewline = false;
|
||
|
this.scroll.update();
|
||
|
let scrollLength = this.scroll.length();
|
||
|
this.scroll.batchStart();
|
||
|
delta = normalizeDelta(delta);
|
||
|
delta.reduce((index, op) => {
|
||
|
let length = op.retain || op.delete || op.insert.length || 1;
|
||
|
let attributes = op.attributes || {};
|
||
|
if (op.insert != null) {
|
||
|
if (typeof op.insert === 'string') {
|
||
|
let text = op.insert;
|
||
|
if (text.endsWith('\n') && consumeNextNewline) {
|
||
|
consumeNextNewline = false;
|
||
|
text = text.slice(0, -1);
|
||
|
}
|
||
|
if (index >= scrollLength && !text.endsWith('\n')) {
|
||
|
consumeNextNewline = true;
|
||
|
}
|
||
|
this.scroll.insertAt(index, text);
|
||
|
let [line, offset] = this.scroll.line(index);
|
||
|
let formats = extend({}, bubbleFormats(line));
|
||
|
if (line instanceof Block) {
|
||
|
let [leaf, ] = line.descendant(Parchment.Leaf, offset);
|
||
|
formats = extend(formats, bubbleFormats(leaf));
|
||
|
}
|
||
|
attributes = DeltaOp.attributes.diff(formats, attributes) || {};
|
||
|
} else if (typeof op.insert === 'object') {
|
||
|
let key = Object.keys(op.insert)[0]; // There should only be one key
|
||
|
if (key == null) return index;
|
||
|
this.scroll.insertAt(index, key, op.insert[key]);
|
||
|
}
|
||
|
scrollLength += length;
|
||
|
}
|
||
|
Object.keys(attributes).forEach((name) => {
|
||
|
this.scroll.formatAt(index, length, name, attributes[name]);
|
||
|
});
|
||
|
return index + length;
|
||
|
}, 0);
|
||
|
delta.reduce((index, op) => {
|
||
|
if (typeof op.delete === 'number') {
|
||
|
this.scroll.deleteAt(index, op.delete);
|
||
|
return index;
|
||
|
}
|
||
|
return index + (op.retain || op.insert.length || 1);
|
||
|
}, 0);
|
||
|
this.scroll.batchEnd();
|
||
|
return this.update(delta);
|
||
|
}
|
||
|
|
||
|
deleteText(index, length) {
|
||
|
this.scroll.deleteAt(index, length);
|
||
|
return this.update(new Delta().retain(index).delete(length));
|
||
|
}
|
||
|
|
||
|
formatLine(index, length, formats = {}) {
|
||
|
this.scroll.update();
|
||
|
Object.keys(formats).forEach((format) => {
|
||
|
if (this.scroll.whitelist != null && !this.scroll.whitelist[format]) return;
|
||
|
let lines = this.scroll.lines(index, Math.max(length, 1));
|
||
|
let lengthRemaining = length;
|
||
|
lines.forEach((line) => {
|
||
|
let lineLength = line.length();
|
||
|
if (!(line instanceof CodeBlock)) {
|
||
|
line.format(format, formats[format]);
|
||
|
} else {
|
||
|
let codeIndex = index - line.offset(this.scroll);
|
||
|
let codeLength = line.newlineIndex(codeIndex + lengthRemaining) - codeIndex + 1;
|
||
|
line.formatAt(codeIndex, codeLength, format, formats[format]);
|
||
|
}
|
||
|
lengthRemaining -= lineLength;
|
||
|
});
|
||
|
});
|
||
|
this.scroll.optimize();
|
||
|
return this.update(new Delta().retain(index).retain(length, clone(formats)));
|
||
|
}
|
||
|
|
||
|
formatText(index, length, formats = {}) {
|
||
|
Object.keys(formats).forEach((format) => {
|
||
|
this.scroll.formatAt(index, length, format, formats[format]);
|
||
|
});
|
||
|
return this.update(new Delta().retain(index).retain(length, clone(formats)));
|
||
|
}
|
||
|
|
||
|
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, length = 0) {
|
||
|
let lines = [], leaves = [];
|
||
|
if (length === 0) {
|
||
|
this.scroll.path(index).forEach(function(path) {
|
||
|
let [blot, ] = path;
|
||
|
if (blot instanceof Block) {
|
||
|
lines.push(blot);
|
||
|
} else if (blot instanceof Parchment.Leaf) {
|
||
|
leaves.push(blot);
|
||
|
}
|
||
|
});
|
||
|
} else {
|
||
|
lines = this.scroll.lines(index, length);
|
||
|
leaves = this.scroll.descendants(Parchment.Leaf, index, length);
|
||
|
}
|
||
|
let formatsArr = [lines, leaves].map(function(blots) {
|
||
|
if (blots.length === 0) return {};
|
||
|
let formats = bubbleFormats(blots.shift());
|
||
|
while (Object.keys(formats).length > 0) {
|
||
|
let blot = blots.shift();
|
||
|
if (blot == null) return formats;
|
||
|
formats = combineFormats(bubbleFormats(blot), formats);
|
||
|
}
|
||
|
return formats;
|
||
|
});
|
||
|
return extend.apply(extend, formatsArr);
|
||
|
}
|
||
|
|
||
|
getText(index, length) {
|
||
|
return this.getContents(index, length).filter(function(op) {
|
||
|
return typeof op.insert === 'string';
|
||
|
}).map(function(op) {
|
||
|
return op.insert;
|
||
|
}).join('');
|
||
|
}
|
||
|
|
||
|
insertEmbed(index, embed, value) {
|
||
|
this.scroll.insertAt(index, embed, value);
|
||
|
return this.update(new Delta().retain(index).insert({ [embed]: value }));
|
||
|
}
|
||
|
|
||
|
insertText(index, text, formats = {}) {
|
||
|
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, clone(formats)));
|
||
|
}
|
||
|
|
||
|
isBlank() {
|
||
|
if (this.scroll.children.length == 0) return true;
|
||
|
if (this.scroll.children.length > 1) return false;
|
||
|
let block = this.scroll.children.head;
|
||
|
if (block.statics.blotName !== Block.blotName) return false;
|
||
|
if (block.children.length > 1) return false;
|
||
|
return block.children.head instanceof Break;
|
||
|
}
|
||
|
|
||
|
removeFormat(index, length) {
|
||
|
let text = this.getText(index, length);
|
||
|
let [line, offset] = this.scroll.line(index + length);
|
||
|
let suffixLength = 0, suffix = new Delta();
|
||
|
if (line != null) {
|
||
|
if (!(line instanceof CodeBlock)) {
|
||
|
suffixLength = line.length() - offset;
|
||
|
} else {
|
||
|
suffixLength = line.newlineIndex(offset) - offset + 1;
|
||
|
}
|
||
|
suffix = line.delta().slice(offset, offset + suffixLength - 1).insert('\n');
|
||
|
}
|
||
|
let contents = this.getContents(index, length + suffixLength);
|
||
|
let diff = contents.diff(new Delta().insert(text).concat(suffix));
|
||
|
let delta = new Delta().retain(index).concat(diff);
|
||
|
return this.applyDelta(delta);
|
||
|
}
|
||
|
|
||
|
update(change, mutations = [], cursorIndex = undefined) {
|
||
|
let oldDelta = this.delta;
|
||
|
if (mutations.length === 1 &&
|
||
|
mutations[0].type === 'characterData' &&
|
||
|
mutations[0].target.data.match(ASCII) &&
|
||
|
Parchment.find(mutations[0].target)) {
|
||
|
// Optimization for character changes
|
||
|
let textBlot = Parchment.find(mutations[0].target);
|
||
|
let formats = bubbleFormats(textBlot);
|
||
|
let index = textBlot.offset(this.scroll);
|
||
|
let oldValue = mutations[0].oldValue.replace(CursorBlot.CONTENTS, '');
|
||
|
let oldText = new Delta().insert(oldValue);
|
||
|
let newText = new Delta().insert(textBlot.value());
|
||
|
let diffDelta = new Delta().retain(index).concat(oldText.diff(newText, cursorIndex));
|
||
|
change = diffDelta.reduce(function(delta, op) {
|
||
|
if (op.insert) {
|
||
|
return delta.insert(op.insert, formats);
|
||
|
} else {
|
||
|
return delta.push(op);
|
||
|
}
|
||
|
}, new Delta());
|
||
|
this.delta = oldDelta.compose(change);
|
||
|
} else {
|
||
|
this.delta = this.getDelta();
|
||
|
if (!change || !equal(oldDelta.compose(change), this.delta)) {
|
||
|
change = oldDelta.diff(this.delta, cursorIndex);
|
||
|
}
|
||
|
}
|
||
|
return change;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
function combineFormats(formats, combined) {
|
||
|
return Object.keys(combined).reduce(function(merged, name) {
|
||
|
if (formats[name] == null) return merged;
|
||
|
if (combined[name] === formats[name]) {
|
||
|
merged[name] = combined[name];
|
||
|
} else if (Array.isArray(combined[name])) {
|
||
|
if (combined[name].indexOf(formats[name]) < 0) {
|
||
|
merged[name] = combined[name].concat([formats[name]]);
|
||
|
}
|
||
|
} else {
|
||
|
merged[name] = [combined[name], formats[name]];
|
||
|
}
|
||
|
return merged;
|
||
|
}, {});
|
||
|
}
|
||
|
|
||
|
function normalizeDelta(delta) {
|
||
|
return delta.reduce(function(delta, op) {
|
||
|
if (op.insert === 1) {
|
||
|
let attributes = clone(op.attributes);
|
||
|
delete attributes['image'];
|
||
|
return delta.insert({ image: op.attributes.image }, attributes);
|
||
|
}
|
||
|
if (op.attributes != null && (op.attributes.list === true || op.attributes.bullet === true)) {
|
||
|
op = clone(op);
|
||
|
if (op.attributes.list) {
|
||
|
op.attributes.list = 'ordered';
|
||
|
} else {
|
||
|
op.attributes.list = 'bullet';
|
||
|
delete op.attributes.bullet;
|
||
|
}
|
||
|
}
|
||
|
if (typeof op.insert === 'string') {
|
||
|
let text = op.insert.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||
|
return delta.insert(text, op.attributes);
|
||
|
}
|
||
|
return delta.push(op);
|
||
|
}, new Delta());
|
||
|
}
|
||
|
|
||
|
|
||
|
export default Editor;
|