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;