
package.blots.scroll.js Maven / Gradle / Ivy
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
© 2015 - 2025 Weber Informatics LLC | Privacy Policy