You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
349 lines
12 KiB
349 lines
12 KiB
import { ContainerBlot, LeafBlot, Scope, ScrollBlot } from 'parchment'; |
|
import Delta, { AttributeMap, Op } from 'quill-delta'; |
|
import Emitter from '../core/emitter.js'; |
|
import Block, { BlockEmbed, bubbleFormats } from './block.js'; |
|
import Break from './break.js'; |
|
import Container from './container.js'; |
|
function isLine(blot) { |
|
return blot instanceof Block || blot instanceof BlockEmbed; |
|
} |
|
function isUpdatable(blot) { |
|
return typeof blot.updateContent === 'function'; |
|
} |
|
class Scroll extends ScrollBlot { |
|
static blotName = 'scroll'; |
|
static className = 'ql-editor'; |
|
static tagName = 'DIV'; |
|
static defaultChild = Block; |
|
static allowedChildren = [Block, BlockEmbed, Container]; |
|
constructor(registry, domNode, _ref) { |
|
let { |
|
emitter |
|
} = _ref; |
|
super(registry, domNode); |
|
this.emitter = emitter; |
|
this.batch = false; |
|
this.optimize(); |
|
this.enable(); |
|
this.domNode.addEventListener('dragstart', e => this.handleDragStart(e)); |
|
} |
|
batchStart() { |
|
if (!Array.isArray(this.batch)) { |
|
this.batch = []; |
|
} |
|
} |
|
batchEnd() { |
|
if (!this.batch) return; |
|
const mutations = this.batch; |
|
this.batch = false; |
|
this.update(mutations); |
|
} |
|
emitMount(blot) { |
|
this.emitter.emit(Emitter.events.SCROLL_BLOT_MOUNT, blot); |
|
} |
|
emitUnmount(blot) { |
|
this.emitter.emit(Emitter.events.SCROLL_BLOT_UNMOUNT, blot); |
|
} |
|
emitEmbedUpdate(blot, change) { |
|
this.emitter.emit(Emitter.events.SCROLL_EMBED_UPDATE, blot, change); |
|
} |
|
deleteAt(index, length) { |
|
const [first, offset] = this.line(index); |
|
const [last] = this.line(index + length); |
|
super.deleteAt(index, length); |
|
if (last != null && first !== last && offset > 0) { |
|
if (first instanceof BlockEmbed || last instanceof BlockEmbed) { |
|
this.optimize(); |
|
return; |
|
} |
|
const ref = last.children.head instanceof Break ? null : last.children.head; |
|
// @ts-expect-error |
|
first.moveChildren(last, ref); |
|
// @ts-expect-error |
|
first.remove(); |
|
} |
|
this.optimize(); |
|
} |
|
enable() { |
|
let enabled = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; |
|
this.domNode.setAttribute('contenteditable', enabled ? 'true' : 'false'); |
|
} |
|
formatAt(index, length, format, value) { |
|
super.formatAt(index, length, format, value); |
|
this.optimize(); |
|
} |
|
insertAt(index, value, def) { |
|
if (index >= this.length()) { |
|
if (def == null || this.scroll.query(value, Scope.BLOCK) == null) { |
|
const blot = this.scroll.create(this.statics.defaultChild.blotName); |
|
this.appendChild(blot); |
|
if (def == null && value.endsWith('\n')) { |
|
blot.insertAt(0, value.slice(0, -1), def); |
|
} else { |
|
blot.insertAt(0, value, def); |
|
} |
|
} else { |
|
const embed = this.scroll.create(value, def); |
|
this.appendChild(embed); |
|
} |
|
} else { |
|
super.insertAt(index, value, def); |
|
} |
|
this.optimize(); |
|
} |
|
insertBefore(blot, ref) { |
|
if (blot.statics.scope === Scope.INLINE_BLOT) { |
|
const wrapper = this.scroll.create(this.statics.defaultChild.blotName); |
|
wrapper.appendChild(blot); |
|
super.insertBefore(wrapper, ref); |
|
} else { |
|
super.insertBefore(blot, ref); |
|
} |
|
} |
|
insertContents(index, delta) { |
|
const renderBlocks = this.deltaToRenderBlocks(delta.concat(new Delta().insert('\n'))); |
|
const last = renderBlocks.pop(); |
|
if (last == null) return; |
|
this.batchStart(); |
|
const first = renderBlocks.shift(); |
|
if (first) { |
|
const shouldInsertNewlineChar = first.type === 'block' && (first.delta.length() === 0 || !this.descendant(BlockEmbed, index)[0] && index < this.length()); |
|
const delta = first.type === 'block' ? first.delta : new Delta().insert({ |
|
[first.key]: first.value |
|
}); |
|
insertInlineContents(this, index, delta); |
|
const newlineCharLength = first.type === 'block' ? 1 : 0; |
|
const lineEndIndex = index + delta.length() + newlineCharLength; |
|
if (shouldInsertNewlineChar) { |
|
this.insertAt(lineEndIndex - 1, '\n'); |
|
} |
|
const formats = bubbleFormats(this.line(index)[0]); |
|
const attributes = AttributeMap.diff(formats, first.attributes) || {}; |
|
Object.keys(attributes).forEach(name => { |
|
this.formatAt(lineEndIndex - 1, 1, name, attributes[name]); |
|
}); |
|
index = lineEndIndex; |
|
} |
|
let [refBlot, refBlotOffset] = this.children.find(index); |
|
if (renderBlocks.length) { |
|
if (refBlot) { |
|
refBlot = refBlot.split(refBlotOffset); |
|
refBlotOffset = 0; |
|
} |
|
renderBlocks.forEach(renderBlock => { |
|
if (renderBlock.type === 'block') { |
|
const block = this.createBlock(renderBlock.attributes, refBlot || undefined); |
|
insertInlineContents(block, 0, renderBlock.delta); |
|
} else { |
|
const blockEmbed = this.create(renderBlock.key, renderBlock.value); |
|
this.insertBefore(blockEmbed, refBlot || undefined); |
|
Object.keys(renderBlock.attributes).forEach(name => { |
|
blockEmbed.format(name, renderBlock.attributes[name]); |
|
}); |
|
} |
|
}); |
|
} |
|
if (last.type === 'block' && last.delta.length()) { |
|
const offset = refBlot ? refBlot.offset(refBlot.scroll) + refBlotOffset : this.length(); |
|
insertInlineContents(this, offset, last.delta); |
|
} |
|
this.batchEnd(); |
|
this.optimize(); |
|
} |
|
isEnabled() { |
|
return this.domNode.getAttribute('contenteditable') === 'true'; |
|
} |
|
leaf(index) { |
|
const last = this.path(index).pop(); |
|
if (!last) { |
|
return [null, -1]; |
|
} |
|
const [blot, offset] = last; |
|
return blot instanceof LeafBlot ? [blot, offset] : [null, -1]; |
|
} |
|
line(index) { |
|
if (index === this.length()) { |
|
return this.line(index - 1); |
|
} |
|
// @ts-expect-error TODO: make descendant() generic |
|
return this.descendant(isLine, index); |
|
} |
|
lines() { |
|
let index = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; |
|
let length = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Number.MAX_VALUE; |
|
const getLines = (blot, blotIndex, blotLength) => { |
|
let lines = []; |
|
let lengthLeft = blotLength; |
|
blot.children.forEachAt(blotIndex, blotLength, (child, childIndex, childLength) => { |
|
if (isLine(child)) { |
|
lines.push(child); |
|
} else if (child instanceof ContainerBlot) { |
|
lines = lines.concat(getLines(child, childIndex, lengthLeft)); |
|
} |
|
lengthLeft -= childLength; |
|
}); |
|
return lines; |
|
}; |
|
return getLines(this, index, length); |
|
} |
|
optimize() { |
|
let mutations = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; |
|
let context = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; |
|
if (this.batch) return; |
|
super.optimize(mutations, context); |
|
if (mutations.length > 0) { |
|
this.emitter.emit(Emitter.events.SCROLL_OPTIMIZE, mutations, context); |
|
} |
|
} |
|
path(index) { |
|
return super.path(index).slice(1); // Exclude self |
|
} |
|
remove() { |
|
// Never remove self |
|
} |
|
update(mutations) { |
|
if (this.batch) { |
|
if (Array.isArray(mutations)) { |
|
this.batch = this.batch.concat(mutations); |
|
} |
|
return; |
|
} |
|
let source = Emitter.sources.USER; |
|
if (typeof mutations === 'string') { |
|
source = mutations; |
|
} |
|
if (!Array.isArray(mutations)) { |
|
mutations = this.observer.takeRecords(); |
|
} |
|
mutations = mutations.filter(_ref2 => { |
|
let { |
|
target |
|
} = _ref2; |
|
const blot = this.find(target, true); |
|
return blot && !isUpdatable(blot); |
|
}); |
|
if (mutations.length > 0) { |
|
this.emitter.emit(Emitter.events.SCROLL_BEFORE_UPDATE, source, mutations); |
|
} |
|
super.update(mutations.concat([])); // pass copy |
|
if (mutations.length > 0) { |
|
this.emitter.emit(Emitter.events.SCROLL_UPDATE, source, mutations); |
|
} |
|
} |
|
updateEmbedAt(index, key, change) { |
|
// Currently it only supports top-level embeds (BlockEmbed). |
|
// We can update `ParentBlot` in parchment to support inline embeds. |
|
const [blot] = this.descendant(b => b instanceof BlockEmbed, index); |
|
if (blot && blot.statics.blotName === key && isUpdatable(blot)) { |
|
blot.updateContent(change); |
|
} |
|
} |
|
handleDragStart(event) { |
|
event.preventDefault(); |
|
} |
|
deltaToRenderBlocks(delta) { |
|
const renderBlocks = []; |
|
let currentBlockDelta = new Delta(); |
|
delta.forEach(op => { |
|
const insert = op?.insert; |
|
if (!insert) return; |
|
if (typeof insert === 'string') { |
|
const splitted = insert.split('\n'); |
|
splitted.slice(0, -1).forEach(text => { |
|
currentBlockDelta.insert(text, op.attributes); |
|
renderBlocks.push({ |
|
type: 'block', |
|
delta: currentBlockDelta, |
|
attributes: op.attributes ?? {} |
|
}); |
|
currentBlockDelta = new Delta(); |
|
}); |
|
const last = splitted[splitted.length - 1]; |
|
if (last) { |
|
currentBlockDelta.insert(last, op.attributes); |
|
} |
|
} else { |
|
const key = Object.keys(insert)[0]; |
|
if (!key) return; |
|
if (this.query(key, Scope.INLINE)) { |
|
currentBlockDelta.push(op); |
|
} else { |
|
if (currentBlockDelta.length()) { |
|
renderBlocks.push({ |
|
type: 'block', |
|
delta: currentBlockDelta, |
|
attributes: {} |
|
}); |
|
} |
|
currentBlockDelta = new Delta(); |
|
renderBlocks.push({ |
|
type: 'blockEmbed', |
|
key, |
|
value: insert[key], |
|
attributes: op.attributes ?? {} |
|
}); |
|
} |
|
} |
|
}); |
|
if (currentBlockDelta.length()) { |
|
renderBlocks.push({ |
|
type: 'block', |
|
delta: currentBlockDelta, |
|
attributes: {} |
|
}); |
|
} |
|
return renderBlocks; |
|
} |
|
createBlock(attributes, refBlot) { |
|
let blotName; |
|
const formats = {}; |
|
Object.entries(attributes).forEach(_ref3 => { |
|
let [key, value] = _ref3; |
|
const isBlockBlot = this.query(key, Scope.BLOCK & Scope.BLOT) != null; |
|
if (isBlockBlot) { |
|
blotName = key; |
|
} else { |
|
formats[key] = value; |
|
} |
|
}); |
|
const block = this.create(blotName || this.statics.defaultChild.blotName, blotName ? attributes[blotName] : undefined); |
|
this.insertBefore(block, refBlot || undefined); |
|
const length = block.length(); |
|
Object.entries(formats).forEach(_ref4 => { |
|
let [key, value] = _ref4; |
|
block.formatAt(0, length, key, value); |
|
}); |
|
return block; |
|
} |
|
} |
|
function insertInlineContents(parent, index, inlineContents) { |
|
inlineContents.reduce((index, op) => { |
|
const length = Op.length(op); |
|
let attributes = op.attributes || {}; |
|
if (op.insert != null) { |
|
if (typeof op.insert === 'string') { |
|
const text = op.insert; |
|
parent.insertAt(index, text); |
|
const [leaf] = parent.descendant(LeafBlot, index); |
|
const 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; |
|
parent.insertAt(index, key, op.insert[key]); |
|
const isInlineEmbed = parent.scroll.query(key, Scope.INLINE) != null; |
|
if (isInlineEmbed) { |
|
const [leaf] = parent.descendant(LeafBlot, index); |
|
const formats = bubbleFormats(leaf); |
|
attributes = AttributeMap.diff(formats, attributes) || {}; |
|
} |
|
} |
|
} |
|
Object.keys(attributes).forEach(key => { |
|
parent.formatAt(index, length, key, attributes[key]); |
|
}); |
|
return index + length; |
|
}, index); |
|
} |
|
export default Scroll; |
|
//# sourceMappingURL=scroll.js.map
|