New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@blocksuite/virgo

Package Overview
Dependencies
Maintainers
5
Versions
509
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@blocksuite/virgo - npm Package Compare versions

Comparing version 0.5.0-20230326033652-70ca43c to 0.5.0-20230404060355-e26ee252

dist/services/attribute.d.ts

3

dist/components/virgo-element.d.ts
import { LitElement, type TemplateResult } from 'lit';
import type { DeltaInsert } from '../types.js';
import type { BaseTextAttributes } from '../utils/base-attributes.js';
import { VText } from './virgo-text.js';
export declare class VirgoElement<T extends BaseTextAttributes = BaseTextAttributes> extends LitElement {
delta: DeltaInsert<T>;
attributesRenderer: (vText: VText, attributes?: T) => TemplateResult<1>;
attributeRenderer: (delta: DeltaInsert<T>) => TemplateResult<1>;
render(): TemplateResult<1>;

@@ -9,0 +8,0 @@ createRenderRoot(): this;

@@ -10,4 +10,3 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {

import { ZERO_WIDTH_SPACE } from '../constant.js';
import { getDefaultAttributeRenderer } from '../utils/attributes-renderer.js';
import { VText } from './virgo-text.js';
import { getDefaultAttributeRenderer } from '../utils/attribute-renderer.js';
let VirgoElement = class VirgoElement extends LitElement {

@@ -19,11 +18,9 @@ constructor() {

};
this.attributesRenderer = getDefaultAttributeRenderer();
this.attributeRenderer = getDefaultAttributeRenderer();
}
render() {
const vText = new VText();
vText.str = this.delta.insert;
// we need to avoid \n appearing before and after the span element, which will
// cause the unexpected space
return html `<span data-virgo-element="true"
>${this.attributesRenderer(vText, this.delta.attributes)}</span
>${this.attributeRenderer(this.delta)}</span
>`;

@@ -40,3 +37,3 @@ }

property({ type: Function, attribute: false })
], VirgoElement.prototype, "attributesRenderer", void 0);
], VirgoElement.prototype, "attributeRenderer", void 0);
VirgoElement = __decorate([

@@ -43,0 +40,0 @@ customElement('v-element')

@@ -1,6 +0,12 @@

import { LitElement } from 'lit';
import type { BaseTextAttributes } from '../utils/index.js';
import type { VirgoElement } from './virgo-element.js';
export declare class VirgoLine<TextAttributes extends BaseTextAttributes = BaseTextAttributes> extends LitElement {
elements: VirgoElement<TextAttributes>[];
import { LitElement, type TemplateResult } from 'lit';
export declare class VirgoLine extends LitElement {
elements: TemplateResult<1>[];
get vElements(): import("./virgo-element.js").VirgoElement<{
bold?: true | undefined;
italic?: true | undefined;
underline?: true | undefined;
strike?: true | undefined;
code?: true | undefined;
link?: string | undefined;
}>[];
get textLength(): number;

@@ -10,3 +16,3 @@ get textContent(): string;

protected firstUpdated(): void;
render(): import("lit").TemplateResult<1>;
render(): TemplateResult<1>;
createRenderRoot(): this;

@@ -13,0 +19,0 @@ }

@@ -14,11 +14,14 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {

}
get vElements() {
return Array.from(this.querySelectorAll('v-element'));
}
get textLength() {
return this.elements.reduce((acc, el) => acc + el.delta.insert.length, 0);
return this.vElements.reduce((acc, el) => acc + el.delta.insert.length, 0);
}
get textContent() {
return this.elements.reduce((acc, el) => acc + el.delta.insert, '');
return this.vElements.reduce((acc, el) => acc + el.delta.insert, '');
}
async getUpdateComplete() {
const result = await super.getUpdateComplete();
await Promise.all(this.elements.map(el => el.updateComplete));
await Promise.all(this.vElements.map(el => el.updateComplete));
return result;

@@ -25,0 +28,0 @@ }

@@ -5,2 +5,3 @@ import { LitElement } from 'lit';

export declare class VText extends LitElement {
static styles: import("lit").CSSResult;
str: string;

@@ -7,0 +8,0 @@ styles: DirectiveResult<typeof StyleMapDirective>;

@@ -7,3 +7,3 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {

};
import { html, LitElement } from 'lit';
import { css, html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@@ -29,2 +29,8 @@ import { styleMap } from 'lit/directives/style-map.js';

};
VText.styles = css `
v-text {
word-wrap: break-word;
white-space: break-spaces;
}
`;
__decorate([

@@ -31,0 +37,0 @@ property()

export declare const ZERO_WIDTH_SPACE = "\u200B";
export declare const ZERO_WIDTH_NON_JOINER = "\u200C";
//# sourceMappingURL=constant.d.ts.map
export const ZERO_WIDTH_SPACE = '\u200B';
// see https://en.wikipedia.org/wiki/Zero-width_non-joiner
export const ZERO_WIDTH_NON_JOINER = '\u200C';
//# sourceMappingURL=constant.js.map

@@ -12,3 +12,3 @@ import { type BaseTextAttributes } from '../utils/index.js';

constructor(editor: VEditor<TextAttributes>);
private _defaultHandlers;
defaultHandlers: VirgoEventService<TextAttributes>['_handlers'];
mount: () => void;

@@ -15,0 +15,0 @@ unmount: () => void;

import { findDocumentOrShadowRoot, } from '../utils/index.js';
import { transformInput } from '../utils/transform-input.js';
export class VirgoEventService {

@@ -10,3 +11,3 @@ constructor(editor) {

this._previousFocus = null;
this._defaultHandlers = {
this.defaultHandlers = {
paste: (event) => {

@@ -60,6 +61,6 @@ const data = event.clipboardData?.getData('text/plain');

}
this._handlers = this._defaultHandlers;
this._handlers = this.defaultHandlers;
};
this.bindHandlers = (handlers = this
._defaultHandlers) => {
.defaultHandlers) => {
this._handlers = handlers;

@@ -164,128 +165,3 @@ if (this._handlerAbortController) {

const { inputType, data } = event;
const currentVRange = vRange;
// You can find explanation of inputType here:
// [Input Events Level 2](https://w3c.github.io/input-events/#interface-InputEvent-Attributes)
if (inputType === 'insertText' && currentVRange.index >= 0 && data) {
this._editor.slots.vRangeUpdated.emit([
{
index: currentVRange.index + data.length,
length: 0,
},
'input',
]);
this._editor.insertText(currentVRange, data);
}
else if (inputType === 'insertParagraph' && currentVRange.index >= 0) {
this._editor.slots.vRangeUpdated.emit([
{
index: currentVRange.index + 1,
length: 0,
},
'input',
]);
this._editor.insertLineBreak(currentVRange);
}
else if (
// Chrome and Safari on Mac: Backspace or Ctrl + H
(inputType === 'deleteContentBackward' || inputType === 'deleteByCut') &&
currentVRange.index >= 0) {
if (currentVRange.length > 0) {
this._editor.slots.vRangeUpdated.emit([
{
index: currentVRange.index,
length: 0,
},
'input',
]);
this._editor.deleteText(currentVRange);
}
else if (currentVRange.index > 0) {
// https://dev.to/acanimal/how-to-slice-or-get-symbols-from-a-unicode-string-with-emojis-in-javascript-lets-learn-how-javascript-represent-strings-h3a
const tmpString = this._editor.yText
.toString()
.slice(0, currentVRange.index);
const deletedCharacter = [...tmpString].slice(-1).join('');
this._editor.slots.vRangeUpdated.emit([
{
index: currentVRange.index - deletedCharacter.length,
length: 0,
},
'input',
]);
this._editor.deleteText({
index: currentVRange.index - deletedCharacter.length,
length: deletedCharacter.length,
});
}
}
else if (
// On Mac: Option + Backspace
// On iOS: Hold the backspace for a while and the whole words will start to disappear
inputType === 'deleteWordBackward') {
const matchs = /\S+\s*$/.exec(this._editor.yText.toString().slice(0, currentVRange.index));
if (!matchs)
return;
const deleteLength = matchs[0].length;
this._editor.slots.vRangeUpdated.emit([
{
index: currentVRange.index - deleteLength,
length: 0,
},
'input',
]);
this._editor.deleteText({
index: currentVRange.index - deleteLength,
length: deleteLength,
});
}
else if (
// Safari on Mac: Cmd + Backspace
inputType === 'deleteHardLineBackward' ||
// Chrome on Mac: Cmd + Backspace
inputType === 'deleteSoftLineBackward') {
if (currentVRange.length > 0) {
this._editor.slots.vRangeUpdated.emit([
{
index: currentVRange.index,
length: 0,
},
'input',
]);
this._editor.deleteText(currentVRange);
}
else if (currentVRange.index > 0) {
const str = this._editor.yText.toString();
const deleteLength = currentVRange.index -
Math.max(0, str.slice(0, currentVRange.index).lastIndexOf('\n'));
this._editor.slots.vRangeUpdated.emit([
{
index: currentVRange.index - deleteLength,
length: 0,
},
'input',
]);
this._editor.deleteText({
index: currentVRange.index - deleteLength,
length: deleteLength,
});
}
}
else if (
// Chrome on Mac: Fn + Backspace or Ctrl + D
// Safari on Mac: Ctrl + K or Ctrl + D
inputType === 'deleteContentForward') {
if (currentVRange.index < this._editor.yText.length) {
this._editor.slots.vRangeUpdated.emit([
{
index: currentVRange.index,
length: 0,
},
'input',
]);
this._editor.deleteText({
index: currentVRange.index,
length: 1,
});
}
}
transformInput(inputType, data, vRange, this._editor);
};

@@ -292,0 +168,0 @@ this._editor = editor;

@@ -0,2 +1,5 @@

export * from './attribute.js';
export * from './delta.js';
export * from './event.js';
export * from './range.js';
//# sourceMappingURL=index.d.ts.map

@@ -0,2 +1,5 @@

export * from './attribute.js';
export * from './delta.js';
export * from './event.js';
export * from './range.js';
//# sourceMappingURL=index.js.map

@@ -1,2 +0,2 @@

import { getDefaultPlaygroundURL } from '@blocksuite/global/utils';
const defaultPlaygroundURL = new URL(`http://localhost:5173/`);
export async function type(page, content) {

@@ -9,3 +9,3 @@ await page.keyboard.type(content, { delay: 50 });

export async function enterVirgoPlayground(page) {
const url = new URL('examples/virgo/index.html', getDefaultPlaygroundURL(!!process.env.CI));
const url = new URL('examples/virgo/index.html', defaultPlaygroundURL);
await page.goto(url.toString());

@@ -26,3 +26,3 @@ }

export async function getDeltaFromVirgoRichText(page, index = 0) {
await page.waitForTimeout(50);
await page.waitForTimeout(100);
return await page.evaluate(index => {

@@ -29,0 +29,0 @@ const richTexts = document

@@ -13,3 +13,3 @@ import { expect, test } from '@playwright/test';

expect(await editorB.innerText()).toBe(ZERO_WIDTH_SPACE);
await page.waitForTimeout(50);
await page.waitForTimeout(100);
await type(page, 'abcdefg');

@@ -38,3 +38,3 @@ expect(await editorA.innerText()).toBe('abcdefg');

await focusVirgoRichText(page);
await page.waitForTimeout(50);
await page.waitForTimeout(100);
await press(page, 'Backspace');

@@ -53,3 +53,3 @@ await press(page, 'Backspace');

await focusVirgoRichText(page);
await page.waitForTimeout(50);
await page.waitForTimeout(100);
await press(page, 'Enter');

@@ -68,3 +68,3 @@ await press(page, 'Enter');

await focusVirgoRichText(page);
await page.waitForTimeout(50);
await page.waitForTimeout(100);
await press(page, 'Backspace');

@@ -83,3 +83,3 @@ await press(page, 'Backspace');

await focusVirgoRichText(page);
await page.waitForTimeout(50);
await page.waitForTimeout(100);
await press(page, 'ArrowLeft');

@@ -99,3 +99,3 @@ await press(page, 'ArrowLeft');

await focusVirgoRichText(page);
await page.waitForTimeout(50);
await page.waitForTimeout(100);
await press(page, 'ArrowLeft');

@@ -121,3 +121,3 @@ await press(page, 'ArrowLeft');

expect(await editorB.innerText()).toBe(ZERO_WIDTH_SPACE);
await page.waitForTimeout(50);
await page.waitForTimeout(100);
await type(page, 'abcdefg');

@@ -154,3 +154,3 @@ expect(await editorA.innerText()).toBe('abcdefg');

expect(await editorB.innerText()).toBe(ZERO_WIDTH_SPACE);
await page.waitForTimeout(50);
await page.waitForTimeout(100);
await type(page, 'abcdefg');

@@ -167,3 +167,3 @@ expect(await editorA.innerText()).toBe('abcdefg');

editorABold.click();
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -185,3 +185,3 @@ expect(delta).toEqual([

editorAItalic.click();
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -204,3 +204,3 @@ expect(delta).toEqual([

editorAUnderline.click();
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -224,3 +224,3 @@ expect(delta).toEqual([

editorAStrike.click();
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -245,3 +245,3 @@ expect(delta).toEqual([

editorACode.click();
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -269,3 +269,3 @@ expect(delta).toEqual([

});
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -280,3 +280,3 @@ expect(delta).toEqual([

});
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -302,3 +302,3 @@ expect(delta).toEqual([

editorABold.click();
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -323,3 +323,3 @@ expect(delta).toEqual([

editorAItalic.click();
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -343,3 +343,3 @@ expect(delta).toEqual([

editorAUnderline.click();
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -362,3 +362,3 @@ expect(delta).toEqual([

editorAStrike.click();
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -380,3 +380,3 @@ expect(delta).toEqual([

editorACode.click();
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -400,3 +400,3 @@ expect(delta).toEqual([

expect(await editorB.innerText()).toBe(ZERO_WIDTH_SPACE);
await page.waitForTimeout(50);
await page.waitForTimeout(100);
await type(page, 'abcdefghijk');

@@ -558,3 +558,3 @@ expect(await editorA.innerText()).toBe('abcdefghijk');

expect(await editorB.innerText()).toBe(ZERO_WIDTH_SPACE);
await page.waitForTimeout(50);
await page.waitForTimeout(100);
await type(page, 'abc def');

@@ -564,3 +564,3 @@ expect(await editorA.innerText()).toBe('abc def');

await focusVirgoRichText(page);
await page.waitForTimeout(50);
await page.waitForTimeout(100);
await press(page, 'ArrowLeft');

@@ -581,3 +581,3 @@ await press(page, 'ArrowLeft');

expect(await editorB.innerText()).toBe(ZERO_WIDTH_SPACE);
await page.waitForTimeout(50);
await page.waitForTimeout(100);
await type(page, 'abc');

@@ -623,3 +623,3 @@ await press(page, 'Enter');

expect(await editorB.innerText()).toBe(ZERO_WIDTH_SPACE);
await page.waitForTimeout(50);
await page.waitForTimeout(100);
await type(page, 'abc');

@@ -654,3 +654,3 @@ await press(page, 'Enter');

await focusVirgoRichText(page);
await page.waitForTimeout(50);
await page.waitForTimeout(100);
const message = await page.evaluate(() => {

@@ -657,0 +657,0 @@ const richText = document

import type { TemplateResult } from 'lit';
import type { VText } from './components/index.js';
import type { BaseTextAttributes } from './utils/index.js';

@@ -8,3 +7,3 @@ export type DeltaInsert<TextAttributes extends BaseTextAttributes = BaseTextAttributes> = {

};
export type AttributesRenderer<TextAttributes extends BaseTextAttributes = BaseTextAttributes> = (vText: VText, attributes?: TextAttributes) => TemplateResult<1>;
export type AttributeRenderer<TextAttributes extends BaseTextAttributes = BaseTextAttributes> = (delta: DeltaInsert<TextAttributes>) => TemplateResult<1>;
export interface VRange {

@@ -18,3 +17,3 @@ index: number;

];
export type DeltaEntry = [delta: DeltaInsert, range: VRange];
export type DeltaEntry<TextAttributes extends BaseTextAttributes = BaseTextAttributes> = [delta: DeltaInsert<TextAttributes>, range: VRange];
export type NativePoint = readonly [node: Node, offset: number];

@@ -21,0 +20,0 @@ export type TextPoint = readonly [text: Text, offset: number];

@@ -1,2 +0,2 @@

export * from './attributes-renderer.js';
export * from './attribute-renderer.js';
export * from './base-attributes.js';

@@ -9,2 +9,5 @@ export * from './convert.js';

export * from './text.js';
export * from './text-point.js';
export * from './to-virgo-range.js';
export * from './transform-input.js';
//# sourceMappingURL=index.d.ts.map

@@ -1,2 +0,2 @@

export * from './attributes-renderer.js';
export * from './attribute-renderer.js';
export * from './base-attributes.js';

@@ -9,2 +9,5 @@ export * from './convert.js';

export * from './text.js';
export * from './text-point.js';
export * from './to-virgo-range.js';
export * from './transform-input.js';
//# sourceMappingURL=index.js.map

@@ -1,5 +0,5 @@

import { VirgoElement } from '../components/virgo-element.js';
import type { AttributesRenderer, DeltaInsert } from '../types.js';
import { type TemplateResult } from 'lit';
import type { AttributeRenderer, DeltaInsert } from '../types.js';
import type { BaseTextAttributes } from './base-attributes.js';
export declare function renderElement<TextAttributes extends BaseTextAttributes>(delta: DeltaInsert<TextAttributes>, parseAttributes: (textAttributes?: TextAttributes) => TextAttributes | undefined, attributesRenderer: AttributesRenderer<TextAttributes>): VirgoElement<TextAttributes>;
export declare function renderElement<TextAttributes extends BaseTextAttributes>(delta: DeltaInsert<TextAttributes>, parseAttributes: (textAttributes?: TextAttributes) => TextAttributes | undefined, attributeRenderer: AttributeRenderer<TextAttributes>): TemplateResult<1>;
//# sourceMappingURL=renderer.d.ts.map

@@ -1,11 +0,11 @@

import { VirgoElement } from '../components/virgo-element.js';
export function renderElement(delta, parseAttributes, attributesRenderer) {
const vElement = new VirgoElement();
vElement.delta = {
import { html } from 'lit';
export function renderElement(delta, parseAttributes, attributeRenderer) {
return html `<v-element
.delta=${{
insert: delta.insert,
attributes: parseAttributes(delta.attributes),
};
vElement.attributesRenderer = attributesRenderer;
return vElement;
}}
.attributeRenderer=${attributeRenderer}
></v-element>`;
}
//# sourceMappingURL=renderer.js.map
export declare function calculateTextLength(text: Text): number;
export declare function getTextNodesFromElement(element: Element): Text[];
//# sourceMappingURL=text.d.ts.map

@@ -10,2 +10,13 @@ import { ZERO_WIDTH_SPACE } from '../constant.js';

}
export function getTextNodesFromElement(element) {
const textSpanElements = Array.from(element.querySelectorAll('[data-virgo-text="true"]'));
const textNodes = textSpanElements.map(textSpanElement => {
const textNode = Array.from(textSpanElement.childNodes).find((node) => node instanceof Text);
if (!textNode) {
throw new Error('text node not found');
}
return textNode;
});
return textNodes;
}
//# sourceMappingURL=text.js.map
import type { NullablePartial } from '@blocksuite/global/types';
import { Slot } from '@blocksuite/global/utils';
import type * as Y from 'yjs';
import type { z, ZodTypeDef } from 'zod';
import { VirgoLine } from './components/index.js';
import type { AttributesRenderer, DeltaEntry, DeltaInsert, DomPoint, VRange, VRangeUpdatedProp } from './types.js';
import type { TextPoint } from './types.js';
import { type BaseTextAttributes } from './utils/index.js';
import * as Y from 'yjs';
import type { VirgoLine } from './components/index.js';
import { VirgoAttributeService, VirgoDeltaService, VirgoEventService, VirgoRangeService } from './services/index.js';
import type { DeltaInsert, TextPoint, VRange, VRangeUpdatedProp } from './types.js';
import { type BaseTextAttributes, getTextNodesFromElement, nativePointToTextPoint, textPointToDomPoint } from './utils/index.js';
export interface VEditorOptions {
defaultMode: 'rich' | 'pure';
}
export declare class VEditor<TextAttributes extends BaseTextAttributes = BaseTextAttributes> {
static nativePointToTextPoint(node: unknown, offset: number): TextPoint | null;
static textPointToDomPoint(text: Text, offset: number, rootElement: HTMLElement): DomPoint | null;
static getTextNodesFromElement(element: Element): Text[];
static nativePointToTextPoint: typeof nativePointToTextPoint;
static textPointToDomPoint: typeof textPointToDomPoint;
static getTextNodesFromElement: typeof getTextNodesFromElement;
private readonly _yText;
private _rootElement;
private _vRange;
private _isReadonly;
private _marks;
private _attributesRenderer;
private _attributesSchema;
private _eventService;
private _parseSchema;
private _renderDeltas;
private _rangeService;
private _attributeService;
private _deltaService;
shouldScrollIntoView: boolean;

@@ -33,6 +32,12 @@ slots: {

get rootElement(): HTMLElement;
get eventService(): VirgoEventService<TextAttributes>;
get rangeService(): VirgoRangeService<TextAttributes>;
get attributeService(): VirgoAttributeService<TextAttributes>;
get deltaService(): VirgoDeltaService<TextAttributes>;
get marks(): TextAttributes | null;
constructor(yText: VEditor['yText']);
setAttributesSchema: (schema: z.ZodSchema<TextAttributes, ZodTypeDef, unknown>) => void;
setAttributesRenderer: (renderer: AttributesRenderer<TextAttributes>) => void;
setAttributeSchema: (schema: import("zod").ZodType<TextAttributes, import("zod").ZodTypeDef, unknown>) => void;
setAttributeRenderer: (renderer: import("./types.js").AttributeRenderer<TextAttributes>) => void;
setMarks: (marks: TextAttributes) => void;
resetMarks: () => void;
getFormat: (vRange: VRange, loose?: boolean) => TextAttributes;
bindHandlers: (handlers?: {

@@ -44,2 +49,11 @@ keydown?: ((event: KeyboardEvent) => void) | undefined;

}) => void;
toDomRange: (vRange: VRange) => Range | null;
toVRange: (selection: Selection) => VRange | null;
getVRange: () => VRange | null;
setVRange: (vRange: VRange) => void;
syncVRange: () => void;
getDeltasByVRange: (vRange: VRange) => import("./types.js").DeltaEntry<TextAttributes>[];
getDeltaByRangeIndex: (rangeIndex: number) => DeltaInsert<TextAttributes> | null;
mapDeltasInVRange: <Result>(vRange: VRange, callback: (delta: DeltaInsert<TextAttributes>, index: number) => Result) => Result[];
constructor(text: VEditor['yText'] | string, options?: VEditorOptions);
mount(rootElement: HTMLElement): void;

@@ -49,91 +63,4 @@ unmount(): void;

getNativeSelection(): Selection | null;
/**
* Here are examples of how this function computes and gets the delta.
*
* We have such a text:
* ```
* [
* {
* insert: 'aaa',
* attributes: { bold: true },
* },
* {
* insert: 'bbb',
* attributes: { italic: true },
* },
* ]
* ```
*
* `getDeltaByRangeIndex(0)` returns `{ insert: 'aaa', attributes: { bold: true } }`.
*
* `getDeltaByRangeIndex(1)` returns `{ insert: 'aaa', attributes: { bold: true } }`.
*
* `getDeltaByRangeIndex(3)` returns `{ insert: 'aaa', attributes: { bold: true } }`.
*
* `getDeltaByRangeIndex(4)` returns `{ insert: 'bbb', attributes: { italic: true } }`.
*/
getDeltaByRangeIndex(rangeIndex: VRange['index']): DeltaInsert | null;
getTextPoint(rangeIndex: VRange['index']): TextPoint;
getLine(rangeIndex: VRange['index']): readonly [VirgoLine, number];
/**
* Here are examples of how this function computes and gets the deltas.
*
* We have such a text:
* ```
* [
* {
* insert: 'aaa',
* attributes: { bold: true },
* },
* {
* insert: 'bbb',
* attributes: { italic: true },
* },
* {
* insert: 'ccc',
* attributes: { underline: true },
* },
* ]
* ```
*
* `getDeltasByVRange({ index: 0, length: 0 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }]]
* ```
*
* `getDeltasByVRange({ index: 0, length: 1 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }]]
* ```
*
* `getDeltasByVRange({ index: 0, length: 4 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }],
* [{ insert: 'bbb', attributes: { italic: true }, }, { index: 3, length: 3, }]]
* ```
*
* `getDeltasByVRange({ index: 3, length: 1 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }],
* [{ insert: 'bbb', attributes: { italic: true }, }, { index: 3, length: 3, }]]
* ```
*
* `getDeltasByVRange({ index: 3, length: 3 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }],
* [{ insert: 'bbb', attributes: { italic: true }, }, { index: 3, length: 3, }]]
* ```
*
* `getDeltasByVRange({ index: 3, length: 4 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }],
* [{ insert: 'bbb', attributes: { italic: true }, }, { index: 3, length: 3, }],
* [{ insert: 'ccc', attributes: { underline: true }, }, { index: 6, length: 3, }]]
* ```
*/
getDeltasByVRange(vRange: VRange): DeltaEntry[];
getVRange(): VRange | null;
getFormat(vRange: VRange): TextAttributes;
setMarks(marks: TextAttributes): void;
resetMarks(): void;
setReadonly(isReadonly: boolean): void;

@@ -144,6 +71,2 @@ get isReadonly(): boolean;

*/
setVRange(vRange: VRange): void;
/**
* the vRange is synced to the native selection asynchronically
*/
focusEnd(): void;

@@ -158,42 +81,5 @@ deleteText(vRange: VRange): void;

resetText(vRange: VRange): void;
/**
* sync the dom selection from vRange for **this Editor**
*/
syncVRange(): void;
private _applyVRange;
/**
* calculate the dom selection from vRange for **this Editor**
*/
toDomRange(vRange: VRange): Range | null;
/**
* calculate the vRange from dom selection for **this Editor**
* there are three cases when the vRange of this Editor is not null:
* (In the following, "|" mean anchor and focus, each line is a separate Editor)
* 1. anchor and focus are in this Editor
* aaaaaa
* b|bbbb|b
* cccccc
* the vRange of second Editor is {index: 1, length: 4}, the others are null
* 2. anchor and focus one in this Editor, one in another Editor
* aaa|aaa aaaaaa
* bbbbb|b or bbbbb|b
* cccccc cc|cccc
* 2.1
* the vRange of first Editor is {index: 3, length: 3}, the second is {index: 0, length: 5},
* the third is null
* 2.2
* the vRange of first Editor is null, the second is {index: 5, length: 1},
* the third is {index: 0, length: 2}
* 3. anchor and focus are in another Editor
* aa|aaaa
* bbbbbb
* cccc|cc
* the vRange of first Editor is {index: 2, length: 4},
* the second is {index: 0, length: 6}, the third is {index: 0, length: 4}
*/
toVRange(selection: Selection): VRange | null;
private _onYTextChange;
private _onVRangeUpdated;
private _transact;
}
//# sourceMappingURL=virgo.d.ts.map
import { assertExists, Slot } from '@blocksuite/global/utils';
import { VirgoElement, VirgoLine } from './components/index.js';
import { ZERO_WIDTH_SPACE } from './constant.js';
import { VirgoEventService } from './services/index.js';
import { baseTextAttributes, calculateTextLength, deltaInsertsToChunks, findDocumentOrShadowRoot, getDefaultAttributeRenderer, isSelectionBackwards, isVElement, isVLine, isVRoot, isVText, renderElement, } from './utils/index.js';
export class VEditor {
static nativePointToTextPoint(node, offset) {
let text = null;
let textOffset = offset;
if (isVText(node)) {
text = node;
textOffset = offset;
}
else if (isVElement(node)) {
const texts = VEditor.getTextNodesFromElement(node);
for (let i = 0; i < texts.length; i++) {
if (offset <= texts[i].length) {
text = texts[i];
textOffset = offset;
break;
}
offset -= texts[i].length;
}
}
else if (isVLine(node) || isVRoot(node)) {
const texts = VEditor.getTextNodesFromElement(node);
if (texts.length > 0) {
text = texts[0];
textOffset = offset === 0 ? offset : text.length;
}
}
else {
if (node instanceof Node) {
const vLine = node.parentElement?.closest('v-line');
if (vLine) {
const vElements = Array.from(vLine.querySelectorAll('v-element'));
for (let i = 0; i < vElements.length; i++) {
if (node.compareDocumentPosition(vElements[i]) ===
Node.DOCUMENT_POSITION_CONTAINED_BY ||
node.compareDocumentPosition(vElements[i]) === 20) {
const texts = VEditor.getTextNodesFromElement(vElements[0]);
if (texts.length === 0)
return null;
text = texts[texts.length - 1];
textOffset = offset === 0 ? offset : text.length;
break;
}
if (i === 0 &&
node.compareDocumentPosition(vElements[i]) ===
Node.DOCUMENT_POSITION_FOLLOWING) {
const texts = VEditor.getTextNodesFromElement(vElements[i]);
if (texts.length === 0)
return null;
text = texts[0];
textOffset = offset === 0 ? offset : text.length;
break;
}
else if (i === vElements.length - 1 &&
node.compareDocumentPosition(vElements[i]) ===
Node.DOCUMENT_POSITION_PRECEDING) {
const texts = VEditor.getTextNodesFromElement(vElements[i]);
if (texts.length === 0)
return null;
text = texts[texts.length - 1];
textOffset = calculateTextLength(text);
break;
}
if (i < vElements.length - 1 &&
node.compareDocumentPosition(vElements[i]) ===
Node.DOCUMENT_POSITION_PRECEDING &&
node.compareDocumentPosition(vElements[i + 1]) ===
Node.DOCUMENT_POSITION_FOLLOWING) {
const texts = VEditor.getTextNodesFromElement(vElements[i]);
if (texts.length === 0)
return null;
text = texts[texts.length - 1];
textOffset = calculateTextLength(text);
break;
}
}
}
}
}
if (!text) {
return null;
}
return [text, textOffset];
}
static textPointToDomPoint(text, offset, rootElement) {
if (rootElement.dataset.virgoRoot !== 'true') {
throw new Error('textRangeToDomPoint should be called with editor root element');
}
if (!rootElement.contains(text)) {
return null;
}
const texts = VEditor.getTextNodesFromElement(rootElement);
const goalIndex = texts.indexOf(text);
let index = 0;
for (const text of texts.slice(0, goalIndex)) {
index += calculateTextLength(text);
}
if (text.wholeText !== ZERO_WIDTH_SPACE) {
index += offset;
}
const textParentElement = text.parentElement;
if (!textParentElement) {
throw new Error('text element parent not found');
}
const lineElement = textParentElement.closest('v-line');
if (!lineElement) {
throw new Error('line element not found');
}
const lineIndex = Array.from(rootElement.querySelectorAll('v-line')).indexOf(lineElement);
return { text, index: index + lineIndex };
}
static getTextNodesFromElement(element) {
const textSpanElements = Array.from(element.querySelectorAll('[data-virgo-text="true"]'));
const textNodes = textSpanElements.map(textSpanElement => {
const textNode = Array.from(textSpanElement.childNodes).find((node) => node instanceof Text);
if (!textNode) {
throw new Error('text node not found');
}
return textNode;
});
return textNodes;
}
import { html } from 'lit';
import * as Y from 'yjs';
import { VirgoAttributeService, VirgoDeltaService, VirgoEventService, VirgoRangeService, } from './services/index.js';
import { findDocumentOrShadowRoot, getTextNodesFromElement, nativePointToTextPoint, textPointToDomPoint, } from './utils/index.js';
class VEditor {
get yText() {

@@ -134,86 +14,45 @@ return this._yText;

}
get eventService() {
return this._eventService;
}
get rangeService() {
return this._rangeService;
}
get attributeService() {
return this._attributeService;
}
get deltaService() {
return this._deltaService;
}
// Expose attribute service API
get marks() {
return this._marks;
return this._attributeService.marks;
}
constructor(yText) {
constructor(text, options = {
defaultMode: 'rich',
}) {
this._rootElement = null;
this._vRange = null;
this._isReadonly = false;
this._marks = null;
this._attributesRenderer = getDefaultAttributeRenderer();
this._attributesSchema = baseTextAttributes;
this._eventService = new VirgoEventService(this);
this._parseSchema = (textAttributes) => {
if (!textAttributes) {
return undefined;
}
const attributesResult = this._attributesSchema.safeParse(textAttributes);
if (!attributesResult.success) {
console.error(attributesResult.error);
return undefined;
}
const attributes = Object.fromEntries(
// filter out undefined values
Object.entries(attributesResult.data).filter(([k, v]) => v));
return attributes;
};
this._renderDeltas = async () => {
assertExists(this._rootElement);
const deltas = this.yText.toDelta();
const chunks = deltaInsertsToChunks(deltas);
// every chunk is a line
const lines = chunks.map(chunk => {
const virgoLine = new VirgoLine();
if (chunk.length === 0) {
virgoLine.elements.push(new VirgoElement());
}
else {
chunk.forEach(delta => {
const element = renderElement(delta, this._parseSchema, this._attributesRenderer);
virgoLine.elements.push(element);
});
}
return virgoLine;
});
this._rootElement.replaceChildren(...lines);
await Promise.all(lines.map(async (line) => {
await line.updateComplete;
}));
//FIXME: wait for render refactor
this.rootElement.focus({ preventScroll: true });
this.slots.updated.emit();
};
this._rangeService = new VirgoRangeService(this);
this._attributeService = new VirgoAttributeService(this);
this._deltaService = new VirgoDeltaService(this);
this.shouldScrollIntoView = true;
this.setAttributesSchema = (schema) => {
this._attributesSchema = schema;
};
this.setAttributesRenderer = (renderer) => {
this._attributesRenderer = renderer;
};
this.setAttributeSchema = this._attributeService.setAttributeSchema;
this.setAttributeRenderer = this._attributeService.setAttributeRenderer;
this.setMarks = this._attributeService.setMarks;
this.resetMarks = this._attributeService.resetMarks;
this.getFormat = this._attributeService.getFormat;
// Expose event service API
this.bindHandlers = this._eventService.bindHandlers;
this._applyVRange = (vRange) => {
const newRange = this.toDomRange(vRange);
if (!newRange) {
return;
}
const selectionRoot = findDocumentOrShadowRoot(this);
const selection = selectionRoot.getSelection();
if (!selection) {
return;
}
selection.removeAllRanges();
selection.addRange(newRange);
//TODO: wait for render refactor
// this.rootElement.focus({ preventScroll: true});
if (this.shouldScrollIntoView) {
let lineElement = newRange.endContainer.parentElement;
while (!(lineElement instanceof VirgoLine)) {
lineElement = lineElement?.parentElement ?? null;
}
lineElement?.scrollIntoView({
block: 'nearest',
});
}
this.slots.rangeUpdated.emit(newRange);
};
// Expose range service API
this.toDomRange = this.rangeService.toDomRange;
this.toVRange = this.rangeService.toVRange;
this.getVRange = this.rangeService.getVRange;
this.setVRange = this.rangeService.setVRange;
this.syncVRange = this.rangeService.syncVRange;
// Expose delta service API
this.getDeltasByVRange = this.deltaService.getDeltasByVRange;
this.getDeltaByRangeIndex = this.deltaService.getDeltaByRangeIndex;
this.mapDeltasInVRange = this.deltaService.mapDeltasInVRange;
this._onYTextChange = () => {

@@ -225,22 +64,14 @@ if (this.yText.toString().includes('\r')) {

assertExists(this._rootElement);
this._renderDeltas();
this.deltaService.render();
});
};
this._onVRangeUpdated = ([newVRange, origin]) => {
this._vRange = newVRange;
if (origin === 'native') {
return;
}
// avoid cursor jumping to beginning in a moment
this._rootElement?.blur();
const fn = () => {
if (newVRange) {
// when using input method _vRange will return to the starting point,
// so we need to re-sync
this._applyVRange(newVRange);
}
};
// updates in lit are performed asynchronously
requestAnimationFrame(fn);
};
let yText;
if (typeof text === 'string') {
const temporaryYDoc = new Y.Doc();
yText = temporaryYDoc.getText('text');
yText.insert(0, text);
}
else {
yText = text;
}
if (!yText.doc) {

@@ -252,2 +83,9 @@ throw new Error('yText must be attached to a Y.Doc');

}
// we can change default render to pure for making `VEditor` to be a pure string render,
// you can change schema and renderer again after construction
if (options.defaultMode === 'pure') {
this._attributeService.setAttributeRenderer(delta => {
return html `<span><v-text .str="${delta.insert}"></v-text></span>`;
});
}
this._yText = yText;

@@ -261,3 +99,3 @@ this.slots = {

};
this.slots.vRangeUpdated.on(this._onVRangeUpdated);
this.slots.vRangeUpdated.on(this.rangeService.onVRangeUpdated);
}

@@ -270,3 +108,3 @@ mount(rootElement) {

this.yText.observe(this._onYTextChange);
this._renderDeltas();
this._deltaService.render();
this._eventService.mount();

@@ -277,2 +115,3 @@ this.slots.mounted.emit();

this._eventService.unmount();
this.yText.unobserve(this._onYTextChange);
this._rootElement?.replaceChildren();

@@ -285,3 +124,3 @@ this._rootElement = null;

assertExists(this._rootElement);
this._renderDeltas();
this._deltaService.render();
});

@@ -298,39 +137,2 @@ }

}
/**
* Here are examples of how this function computes and gets the delta.
*
* We have such a text:
* ```
* [
* {
* insert: 'aaa',
* attributes: { bold: true },
* },
* {
* insert: 'bbb',
* attributes: { italic: true },
* },
* ]
* ```
*
* `getDeltaByRangeIndex(0)` returns `{ insert: 'aaa', attributes: { bold: true } }`.
*
* `getDeltaByRangeIndex(1)` returns `{ insert: 'aaa', attributes: { bold: true } }`.
*
* `getDeltaByRangeIndex(3)` returns `{ insert: 'aaa', attributes: { bold: true } }`.
*
* `getDeltaByRangeIndex(4)` returns `{ insert: 'bbb', attributes: { italic: true } }`.
*/
getDeltaByRangeIndex(rangeIndex) {
const deltas = this.yText.toDelta();
let index = 0;
for (let i = 0; i < deltas.length; i++) {
const delta = deltas[i];
if (index + delta.insert.length >= rangeIndex) {
return delta;
}
index += delta.insert.length;
}
return null;
}
getTextPoint(rangeIndex) {

@@ -372,108 +174,2 @@ assertExists(this._rootElement);

}
/**
* Here are examples of how this function computes and gets the deltas.
*
* We have such a text:
* ```
* [
* {
* insert: 'aaa',
* attributes: { bold: true },
* },
* {
* insert: 'bbb',
* attributes: { italic: true },
* },
* {
* insert: 'ccc',
* attributes: { underline: true },
* },
* ]
* ```
*
* `getDeltasByVRange({ index: 0, length: 0 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }]]
* ```
*
* `getDeltasByVRange({ index: 0, length: 1 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }]]
* ```
*
* `getDeltasByVRange({ index: 0, length: 4 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }],
* [{ insert: 'bbb', attributes: { italic: true }, }, { index: 3, length: 3, }]]
* ```
*
* `getDeltasByVRange({ index: 3, length: 1 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }],
* [{ insert: 'bbb', attributes: { italic: true }, }, { index: 3, length: 3, }]]
* ```
*
* `getDeltasByVRange({ index: 3, length: 3 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }],
* [{ insert: 'bbb', attributes: { italic: true }, }, { index: 3, length: 3, }]]
* ```
*
* `getDeltasByVRange({ index: 3, length: 4 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }],
* [{ insert: 'bbb', attributes: { italic: true }, }, { index: 3, length: 3, }],
* [{ insert: 'ccc', attributes: { underline: true }, }, { index: 6, length: 3, }]]
* ```
*/
getDeltasByVRange(vRange) {
const deltas = this.yText.toDelta();
const result = [];
let index = 0;
for (let i = 0; i < deltas.length; i++) {
const delta = deltas[i];
if (index + delta.insert.length >= vRange.index &&
(index < vRange.index + vRange.length ||
(vRange.length === 0 && index === vRange.index))) {
result.push([delta, { index, length: delta.insert.length }]);
}
index += delta.insert.length;
}
return result;
}
getVRange() {
return this._vRange;
}
getFormat(vRange) {
const deltas = this.getDeltasByVRange(vRange).filter(([delta, position]) => position.index + position.length > vRange.index &&
position.index <= vRange.index + vRange.length);
const maybeAttributesArray = deltas.map(([delta]) => delta.attributes);
if (!maybeAttributesArray.length ||
// some text does not have any attributes
maybeAttributesArray.some(attributes => !attributes)) {
return {};
}
const attributesArray = maybeAttributesArray;
return attributesArray.reduce((acc, cur) => {
const newFormat = {};
for (const key in acc) {
const typedKey = key;
// If the given range contains multiple different formats
// such as links with different values,
// we will treat it as having no format
if (acc[typedKey] === cur[typedKey]) {
// This cast is secure because we have checked that the value of the key is the same.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
newFormat[typedKey] = acc[typedKey];
}
}
return newFormat;
});
}
setMarks(marks) {
this._marks = marks;
}
resetMarks() {
this._marks = null;
}
setReadonly(isReadonly) {

@@ -489,10 +185,4 @@ this.rootElement.contentEditable = isReadonly ? 'false' : 'true';

*/
setVRange(vRange) {
this.slots.vRangeUpdated.emit([vRange, 'other']);
}
/**
* the vRange is synced to the native selection asynchronically
*/
focusEnd() {
this.setVRange({
this.rangeService.setVRange({
index: this.yText.length,

@@ -508,6 +198,6 @@ length: 0,

insertText(vRange, text, attributes = {}) {
if (this._marks) {
attributes = { ...attributes, ...this._marks };
if (this._attributeService.marks) {
attributes = { ...attributes, ...this._attributeService.marks };
}
const normalizedAttributes = this._parseSchema(attributes);
const normalizedAttributes = this._attributeService.normalizeAttributes(attributes);
if (!text || !text.length) {

@@ -529,10 +219,7 @@ throw new Error('text must not be empty');

const { match = () => true, mode = 'merge' } = options;
const deltas = this.getDeltasByVRange(vRange);
const deltas = this._deltaService.getDeltasByVRange(vRange);
deltas
.filter(([delta, deltaVRange]) => match(delta, deltaVRange))
.forEach(([delta, deltaVRange]) => {
const targetVRange = {
index: Math.max(vRange.index, deltaVRange.index),
length: Math.min(vRange.index + vRange.length, deltaVRange.index + deltaVRange.length) - Math.max(vRange.index, deltaVRange.index),
};
const targetVRange = this._rangeService.mergeRanges(vRange, deltaVRange);
if (mode === 'replace') {

@@ -563,160 +250,2 @@ this.resetText(targetVRange);

}
/**
* sync the dom selection from vRange for **this Editor**
*/
syncVRange() {
if (this._vRange) {
this._applyVRange(this._vRange);
}
}
/**
* calculate the dom selection from vRange for **this Editor**
*/
toDomRange(vRange) {
assertExists(this._rootElement);
const lineElements = Array.from(this._rootElement.querySelectorAll('v-line'));
// calculate anchorNode and focusNode
let anchorText = null;
let focusText = null;
let anchorOffset = 0;
let focusOffset = 0;
let index = 0;
for (let i = 0; i < lineElements.length; i++) {
if (anchorText && focusText) {
break;
}
const texts = VEditor.getTextNodesFromElement(lineElements[i]);
for (const text of texts) {
const textLength = calculateTextLength(text);
if (!anchorText && index + textLength >= vRange.index) {
anchorText = text;
anchorOffset = vRange.index - index;
}
if (!focusText && index + textLength >= vRange.index + vRange.length) {
focusText = text;
focusOffset = vRange.index + vRange.length - index;
}
if (anchorText && focusText) {
break;
}
index += textLength;
}
// the one because of the line break
index += 1;
}
if (!anchorText || !focusText) {
return null;
}
const range = document.createRange();
range.setStart(anchorText, anchorOffset);
range.setEnd(focusText, focusOffset);
return range;
}
/**
* calculate the vRange from dom selection for **this Editor**
* there are three cases when the vRange of this Editor is not null:
* (In the following, "|" mean anchor and focus, each line is a separate Editor)
* 1. anchor and focus are in this Editor
* aaaaaa
* b|bbbb|b
* cccccc
* the vRange of second Editor is {index: 1, length: 4}, the others are null
* 2. anchor and focus one in this Editor, one in another Editor
* aaa|aaa aaaaaa
* bbbbb|b or bbbbb|b
* cccccc cc|cccc
* 2.1
* the vRange of first Editor is {index: 3, length: 3}, the second is {index: 0, length: 5},
* the third is null
* 2.2
* the vRange of first Editor is null, the second is {index: 5, length: 1},
* the third is {index: 0, length: 2}
* 3. anchor and focus are in another Editor
* aa|aaaa
* bbbbbb
* cccc|cc
* the vRange of first Editor is {index: 2, length: 4},
* the second is {index: 0, length: 6}, the third is {index: 0, length: 4}
*/
toVRange(selection) {
assertExists(this._rootElement);
const root = this._rootElement;
const { anchorNode, anchorOffset, focusNode, focusOffset } = selection;
if (!anchorNode || !focusNode) {
return null;
}
const anchorTextPoint = VEditor.nativePointToTextPoint(anchorNode, anchorOffset);
const focusTextPoint = VEditor.nativePointToTextPoint(focusNode, focusOffset);
if (!anchorTextPoint || !focusTextPoint) {
return null;
}
const [anchorText, anchorTextOffset] = anchorTextPoint;
const [focusText, focusTextOffset] = focusTextPoint;
// case 1
if (root.contains(anchorText) && root.contains(focusText)) {
const anchorDomPoint = VEditor.textPointToDomPoint(anchorText, anchorTextOffset, this._rootElement);
const focusDomPoint = VEditor.textPointToDomPoint(focusText, focusTextOffset, this._rootElement);
if (!anchorDomPoint || !focusDomPoint) {
return null;
}
return {
index: Math.min(anchorDomPoint.index, focusDomPoint.index),
length: Math.abs(anchorDomPoint.index - focusDomPoint.index),
};
}
// case 2.1
if (!root.contains(anchorText) && root.contains(focusText)) {
if (isSelectionBackwards(selection)) {
const anchorDomPoint = VEditor.textPointToDomPoint(anchorText, anchorTextOffset, this._rootElement);
if (!anchorDomPoint) {
return null;
}
return {
index: anchorDomPoint.index,
length: this.yText.length - anchorDomPoint.index,
};
}
else {
const focusDomPoint = VEditor.textPointToDomPoint(focusText, focusTextOffset, this._rootElement);
if (!focusDomPoint) {
return null;
}
return {
index: 0,
length: focusDomPoint.index,
};
}
}
// case 2.2
if (root.contains(anchorText) && !root.contains(focusText)) {
if (isSelectionBackwards(selection)) {
const focusDomPoint = VEditor.textPointToDomPoint(focusText, focusTextOffset, this._rootElement);
if (!focusDomPoint) {
return null;
}
return {
index: 0,
length: focusDomPoint.index,
};
}
else {
const anchorDomPoint = VEditor.textPointToDomPoint(anchorText, anchorTextOffset, this._rootElement);
if (!anchorDomPoint) {
return null;
}
return {
index: anchorDomPoint.index,
length: this.yText.length - anchorDomPoint.index,
};
}
}
// case 3
if (!root.contains(anchorText) && !root.contains(focusText)) {
return {
index: 0,
length: this.yText.length,
};
}
return null;
}
_transact(fn) {

@@ -730,2 +259,6 @@ const doc = this.yText.doc;

}
VEditor.nativePointToTextPoint = nativePointToTextPoint;
VEditor.textPointToDomPoint = textPointToDomPoint;
VEditor.getTextNodesFromElement = getTextNodesFromElement;
export { VEditor };
//# sourceMappingURL=virgo.js.map
{
"name": "@blocksuite/virgo",
"version": "0.5.0-20230326033652-70ca43c",
"version": "0.5.0-20230404060355-e26ee252",
"description": "A micro editor.",

@@ -26,4 +26,4 @@ "main": "dist/index.js",

"dependencies": {
"@blocksuite/global": "0.5.0-20230326033652-70ca43c",
"zod": "^3.21.4"
"zod": "^3.21.4",
"@blocksuite/global": "0.5.0-20230404060355-e26ee252"
},

@@ -30,0 +30,0 @@ "scripts": {

@@ -5,7 +5,18 @@ import type {

} from '@playwright/test';
import * as process from 'process';
const config: PlaywrightTestConfig = {
fullyParallel: true,
testDir: 'src/',
testIgnore: ['**.unit.spec.ts'],
workers: 4,
webServer: {
command: 'pnpm -w dev',
port: 5173,
// command: process.env.CI ? 'pnpm preview' : 'pnpm dev',
// port: process.env.CI ? 4173 : 5173,
reuseExistingServer: !process.env.CI,
env: {
COVERAGE: process.env.COVERAGE ?? '',
},
},
use: {

@@ -18,11 +29,16 @@ browserName:

},
forbidOnly: !!process.env.CI,
workers: 4,
retries: 1,
// 'github' for GitHub Actions CI to generate annotations, plus a concise 'dot'
// default 'list' when running locally
// See https://playwright.dev/docs/test-reporters#github-actions-annotations
reporter: process.env.CI ? 'github' : 'list',
};
if (process.env.CI) {
config.webServer = {
command: 'pnpm dev',
port: 5173,
};
config.retries = 3;
config.workers = '50%';
}
export default config;

@@ -6,5 +6,4 @@ import { html, LitElement, type TemplateResult } from 'lit';

import type { DeltaInsert } from '../types.js';
import { getDefaultAttributeRenderer } from '../utils/attributes-renderer.js';
import { getDefaultAttributeRenderer } from '../utils/attribute-renderer.js';
import type { BaseTextAttributes } from '../utils/base-attributes.js';
import { VText } from './virgo-text.js';

@@ -21,13 +20,10 @@ @customElement('v-element')

@property({ type: Function, attribute: false })
attributesRenderer: (vText: VText, attributes?: T) => TemplateResult<1> =
attributeRenderer: (delta: DeltaInsert<T>) => TemplateResult<1> =
getDefaultAttributeRenderer<T>();
render() {
const vText = new VText();
vText.str = this.delta.insert;
// we need to avoid \n appearing before and after the span element, which will
// cause the unexpected space
return html`<span data-virgo-element="true"
>${this.attributesRenderer(vText, this.delta.attributes)}</span
>${this.attributeRenderer(this.delta)}</span
>`;

@@ -34,0 +30,0 @@ }

@@ -1,20 +0,19 @@

import { html, LitElement } from 'lit';
import { html, LitElement, type TemplateResult } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import type { BaseTextAttributes } from '../utils/index.js';
import type { VirgoElement } from './virgo-element.js';
@customElement('v-line')
export class VirgoLine<
TextAttributes extends BaseTextAttributes = BaseTextAttributes
> extends LitElement {
export class VirgoLine extends LitElement {
@property({ attribute: false })
elements: VirgoElement<TextAttributes>[] = [];
elements: TemplateResult<1>[] = [];
get vElements() {
return Array.from(this.querySelectorAll('v-element'));
}
get textLength() {
return this.elements.reduce((acc, el) => acc + el.delta.insert.length, 0);
return this.vElements.reduce((acc, el) => acc + el.delta.insert.length, 0);
}
get textContent() {
return this.elements.reduce((acc, el) => acc + el.delta.insert, '');
return this.vElements.reduce((acc, el) => acc + el.delta.insert, '');
}

@@ -24,3 +23,3 @@

const result = await super.getUpdateComplete();
await Promise.all(this.elements.map(el => el.updateComplete));
await Promise.all(this.vElements.map(el => el.updateComplete));
return result;

@@ -27,0 +26,0 @@ }

@@ -1,2 +0,2 @@

import { html, LitElement } from 'lit';
import { css, html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@@ -10,2 +10,9 @@ import type { DirectiveResult } from 'lit/directive.js';

export class VText extends LitElement {
static styles = css`
v-text {
word-wrap: break-word;
white-space: break-spaces;
}
`;
@property()

@@ -12,0 +19,0 @@ str: string = ZERO_WIDTH_SPACE;

export const ZERO_WIDTH_SPACE = '\u200B';
// see https://en.wikipedia.org/wiki/Zero-width_non-joiner
export const ZERO_WIDTH_NON_JOINER = '\u200C';

@@ -6,2 +6,3 @@ import type { NativePoint } from '../types.js';

} from '../utils/index.js';
import { transformInput } from '../utils/transform-input.js';
import type { VEditor } from '../virgo.js';

@@ -31,3 +32,3 @@

private _defaultHandlers: VirgoEventService<TextAttributes>['_handlers'] = {
defaultHandlers: VirgoEventService<TextAttributes>['_handlers'] = {
paste: (event: ClipboardEvent) => {

@@ -90,3 +91,3 @@ const data = event.clipboardData?.getData('text/plain');

this._handlers = this._defaultHandlers;
this._handlers = this.defaultHandlers;
};

@@ -96,3 +97,3 @@

handlers: VirgoEventService<TextAttributes>['_handlers'] = this
._defaultHandlers
.defaultHandlers
) => {

@@ -218,139 +219,5 @@ this._handlers = handlers;

const { inputType, data } = event;
const currentVRange = vRange;
// You can find explanation of inputType here:
// [Input Events Level 2](https://w3c.github.io/input-events/#interface-InputEvent-Attributes)
if (inputType === 'insertText' && currentVRange.index >= 0 && data) {
this._editor.slots.vRangeUpdated.emit([
{
index: currentVRange.index + data.length,
length: 0,
},
'input',
]);
this._editor.insertText(currentVRange, data);
} else if (inputType === 'insertParagraph' && currentVRange.index >= 0) {
this._editor.slots.vRangeUpdated.emit([
{
index: currentVRange.index + 1,
length: 0,
},
'input',
]);
this._editor.insertLineBreak(currentVRange);
} else if (
// Chrome and Safari on Mac: Backspace or Ctrl + H
(inputType === 'deleteContentBackward' || inputType === 'deleteByCut') &&
currentVRange.index >= 0
) {
if (currentVRange.length > 0) {
this._editor.slots.vRangeUpdated.emit([
{
index: currentVRange.index,
length: 0,
},
'input',
]);
this._editor.deleteText(currentVRange);
} else if (currentVRange.index > 0) {
// https://dev.to/acanimal/how-to-slice-or-get-symbols-from-a-unicode-string-with-emojis-in-javascript-lets-learn-how-javascript-represent-strings-h3a
const tmpString = this._editor.yText
.toString()
.slice(0, currentVRange.index);
const deletedCharacter = [...tmpString].slice(-1).join('');
this._editor.slots.vRangeUpdated.emit([
{
index: currentVRange.index - deletedCharacter.length,
length: 0,
},
'input',
]);
this._editor.deleteText({
index: currentVRange.index - deletedCharacter.length,
length: deletedCharacter.length,
});
}
} else if (
// On Mac: Option + Backspace
// On iOS: Hold the backspace for a while and the whole words will start to disappear
inputType === 'deleteWordBackward'
) {
const matchs = /\S+\s*$/.exec(
this._editor.yText.toString().slice(0, currentVRange.index)
);
if (!matchs) return;
const deleteLength = matchs[0].length;
this._editor.slots.vRangeUpdated.emit([
{
index: currentVRange.index - deleteLength,
length: 0,
},
'input',
]);
this._editor.deleteText({
index: currentVRange.index - deleteLength,
length: deleteLength,
});
} else if (
// Safari on Mac: Cmd + Backspace
inputType === 'deleteHardLineBackward' ||
// Chrome on Mac: Cmd + Backspace
inputType === 'deleteSoftLineBackward'
) {
if (currentVRange.length > 0) {
this._editor.slots.vRangeUpdated.emit([
{
index: currentVRange.index,
length: 0,
},
'input',
]);
this._editor.deleteText(currentVRange);
} else if (currentVRange.index > 0) {
const str = this._editor.yText.toString();
const deleteLength =
currentVRange.index -
Math.max(0, str.slice(0, currentVRange.index).lastIndexOf('\n'));
this._editor.slots.vRangeUpdated.emit([
{
index: currentVRange.index - deleteLength,
length: 0,
},
'input',
]);
this._editor.deleteText({
index: currentVRange.index - deleteLength,
length: deleteLength,
});
}
} else if (
// Chrome on Mac: Fn + Backspace or Ctrl + D
// Safari on Mac: Ctrl + K or Ctrl + D
inputType === 'deleteContentForward'
) {
if (currentVRange.index < this._editor.yText.length) {
this._editor.slots.vRangeUpdated.emit([
{
index: currentVRange.index,
length: 0,
},
'input',
]);
this._editor.deleteText({
index: currentVRange.index,
length: 1,
});
}
}
transformInput(inputType, data, vRange, this._editor as VEditor);
};
}

@@ -0,1 +1,4 @@

export * from './attribute.js';
export * from './delta.js';
export * from './event.js';
export * from './range.js';

@@ -1,5 +0,6 @@

import { getDefaultPlaygroundURL } from '@blocksuite/global/utils';
import type { DeltaInsert, VEditor, VRange } from '@blocksuite/virgo';
import type { Page } from '@playwright/test';
const defaultPlaygroundURL = new URL(`http://localhost:5173/`);
export async function type(page: Page, content: string) {

@@ -14,6 +15,3 @@ await page.keyboard.type(content, { delay: 50 });

export async function enterVirgoPlayground(page: Page) {
const url = new URL(
'examples/virgo/index.html',
getDefaultPlaygroundURL(!!process.env.CI)
);
const url = new URL('examples/virgo/index.html', defaultPlaygroundURL);
await page.goto(url.toString());

@@ -41,3 +39,3 @@ }

): Promise<DeltaInsert> {
await page.waitForTimeout(50);
await page.waitForTimeout(100);
return await page.evaluate(index => {

@@ -44,0 +42,0 @@ const richTexts = document

@@ -28,3 +28,3 @@ import { expect, test } from '@playwright/test';

await page.waitForTimeout(50);
await page.waitForTimeout(100);

@@ -62,3 +62,3 @@ await type(page, 'abcdefg');

await focusVirgoRichText(page);
await page.waitForTimeout(50);
await page.waitForTimeout(100);
await press(page, 'Backspace');

@@ -83,3 +83,3 @@ await press(page, 'Backspace');

await focusVirgoRichText(page);
await page.waitForTimeout(50);
await page.waitForTimeout(100);
await press(page, 'Enter');

@@ -105,3 +105,3 @@ await press(page, 'Enter');

await focusVirgoRichText(page);
await page.waitForTimeout(50);
await page.waitForTimeout(100);
await press(page, 'Backspace');

@@ -126,3 +126,3 @@ await press(page, 'Backspace');

await focusVirgoRichText(page);
await page.waitForTimeout(50);
await page.waitForTimeout(100);
await press(page, 'ArrowLeft');

@@ -148,3 +148,3 @@ await press(page, 'ArrowLeft');

await focusVirgoRichText(page);
await page.waitForTimeout(50);
await page.waitForTimeout(100);
await press(page, 'ArrowLeft');

@@ -179,3 +179,3 @@ await press(page, 'ArrowLeft');

await page.waitForTimeout(50);
await page.waitForTimeout(100);

@@ -225,3 +225,3 @@ await type(page, 'abcdefg');

await page.waitForTimeout(50);
await page.waitForTimeout(100);

@@ -243,3 +243,3 @@ await type(page, 'abcdefg');

editorABold.click();
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -262,3 +262,3 @@ expect(delta).toEqual([

editorAItalic.click();
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -282,3 +282,3 @@ expect(delta).toEqual([

editorAUnderline.click();
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -303,3 +303,3 @@ expect(delta).toEqual([

editorAStrike.click();
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -325,3 +325,3 @@ expect(delta).toEqual([

editorACode.click();
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -350,3 +350,3 @@ expect(delta).toEqual([

});
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -362,3 +362,3 @@ expect(delta).toEqual([

});
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -385,3 +385,3 @@ expect(delta).toEqual([

editorABold.click();
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -407,3 +407,3 @@ expect(delta).toEqual([

editorAItalic.click();
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -428,3 +428,3 @@ expect(delta).toEqual([

editorAUnderline.click();
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -448,3 +448,3 @@ expect(delta).toEqual([

editorAStrike.click();
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -467,3 +467,3 @@ expect(delta).toEqual([

editorACode.click();
page.waitForTimeout(50);
page.waitForTimeout(100);
delta = await getDeltaFromVirgoRichText(page);

@@ -493,3 +493,3 @@ expect(delta).toEqual([

await page.waitForTimeout(50);
await page.waitForTimeout(100);

@@ -666,3 +666,3 @@ await type(page, 'abcdefghijk');

await page.waitForTimeout(50);
await page.waitForTimeout(100);

@@ -675,3 +675,3 @@ await type(page, 'abc def');

await focusVirgoRichText(page);
await page.waitForTimeout(50);
await page.waitForTimeout(100);
await press(page, 'ArrowLeft');

@@ -698,3 +698,3 @@ await press(page, 'ArrowLeft');

await page.waitForTimeout(50);
await page.waitForTimeout(100);

@@ -750,3 +750,3 @@ await type(page, 'abc');

await page.waitForTimeout(50);
await page.waitForTimeout(100);

@@ -787,3 +787,3 @@ await type(page, 'abc');

await page.waitForTimeout(50);
await page.waitForTimeout(100);
const message = await page.evaluate(() => {

@@ -790,0 +790,0 @@ const richText = document

import type { TemplateResult } from 'lit';
import type { VText } from './components/index.js';
import type { BaseTextAttributes } from './utils/index.js';

@@ -13,5 +12,5 @@

export type AttributesRenderer<
export type AttributeRenderer<
TextAttributes extends BaseTextAttributes = BaseTextAttributes
> = (vText: VText, attributes?: TextAttributes) => TemplateResult<1>;
> = (delta: DeltaInsert<TextAttributes>) => TemplateResult<1>;

@@ -28,3 +27,5 @@ export interface VRange {

export type DeltaEntry = [delta: DeltaInsert, range: VRange];
export type DeltaEntry<
TextAttributes extends BaseTextAttributes = BaseTextAttributes
> = [delta: DeltaInsert<TextAttributes>, range: VRange];

@@ -31,0 +32,0 @@ // corresponding to [anchorNode/focusNode, anchorOffset/focusOffset]

@@ -1,2 +0,2 @@

export * from './attributes-renderer.js';
export * from './attribute-renderer.js';
export * from './base-attributes.js';

@@ -9,1 +9,4 @@ export * from './convert.js';

export * from './text.js';
export * from './text-point.js';
export * from './to-virgo-range.js';
export * from './transform-input.js';

@@ -1,3 +0,4 @@

import { VirgoElement } from '../components/virgo-element.js';
import type { AttributesRenderer, DeltaInsert } from '../types.js';
import { html, type TemplateResult } from 'lit';
import type { AttributeRenderer, DeltaInsert } from '../types.js';
import type { BaseTextAttributes } from './base-attributes.js';

@@ -10,12 +11,11 @@

) => TextAttributes | undefined,
attributesRenderer: AttributesRenderer<TextAttributes>
): VirgoElement<TextAttributes> {
const vElement = new VirgoElement<TextAttributes>();
vElement.delta = {
insert: delta.insert,
attributes: parseAttributes(delta.attributes),
};
vElement.attributesRenderer = attributesRenderer;
return vElement;
attributeRenderer: AttributeRenderer<TextAttributes>
): TemplateResult<1> {
return html`<v-element
.delta=${{
insert: delta.insert,
attributes: parseAttributes(delta.attributes),
}}
.attributeRenderer=${attributeRenderer}
></v-element>`;
}

@@ -10,1 +10,20 @@ import { ZERO_WIDTH_SPACE } from '../constant.js';

}
export function getTextNodesFromElement(element: Element): Text[] {
const textSpanElements = Array.from(
element.querySelectorAll('[data-virgo-text="true"]')
);
const textNodes = textSpanElements.map(textSpanElement => {
const textNode = Array.from(textSpanElement.childNodes).find(
(node): node is Text => node instanceof Text
);
if (!textNode) {
throw new Error('text node not found');
}
return textNode;
});
return textNodes;
}
import type { NullablePartial } from '@blocksuite/global/types';
import { assertExists, Slot } from '@blocksuite/global/utils';
import type * as Y from 'yjs';
import type { z, ZodTypeDef } from 'zod';
import { html } from 'lit';
import * as Y from 'yjs';
import { VirgoElement, VirgoLine } from './components/index.js';
import { ZERO_WIDTH_SPACE } from './constant.js';
import { VirgoEventService } from './services/index.js';
import type { VirgoLine } from './components/index.js';
import {
VirgoAttributeService,
VirgoDeltaService,
VirgoEventService,
VirgoRangeService,
} from './services/index.js';
import type {
AttributesRenderer,
DeltaEntry,
DeltaInsert,
DomPoint,
TextPoint,
VRange,
VRangeUpdatedProp,
} from './types.js';
import type { TextPoint } from './types.js';
import {
type BaseTextAttributes,
baseTextAttributes,
calculateTextLength,
deltaInsertsToChunks,
findDocumentOrShadowRoot,
getDefaultAttributeRenderer,
isSelectionBackwards,
isVElement,
isVLine,
isVRoot,
isVText,
renderElement,
getTextNodesFromElement,
nativePointToTextPoint,
textPointToDomPoint,
} from './utils/index.js';
export interface VEditorOptions {
// it is a option to determine defult `_attributeRenderer`
defaultMode: 'rich' | 'pure';
}
export class VEditor<
TextAttributes extends BaseTextAttributes = BaseTextAttributes
> {
static nativePointToTextPoint(
node: unknown,
offset: number
): TextPoint | null {
let text: Text | null = null;
let textOffset = offset;
static nativePointToTextPoint = nativePointToTextPoint;
static textPointToDomPoint = textPointToDomPoint;
static getTextNodesFromElement = getTextNodesFromElement;
if (isVText(node)) {
text = node;
textOffset = offset;
} else if (isVElement(node)) {
const texts = VEditor.getTextNodesFromElement(node);
for (let i = 0; i < texts.length; i++) {
if (offset <= texts[i].length) {
text = texts[i];
textOffset = offset;
break;
}
offset -= texts[i].length;
}
} else if (isVLine(node) || isVRoot(node)) {
const texts = VEditor.getTextNodesFromElement(node);
if (texts.length > 0) {
text = texts[0];
textOffset = offset === 0 ? offset : text.length;
}
} else {
if (node instanceof Node) {
const vLine = node.parentElement?.closest('v-line');
if (vLine) {
const vElements = Array.from(vLine.querySelectorAll('v-element'));
for (let i = 0; i < vElements.length; i++) {
if (
node.compareDocumentPosition(vElements[i]) ===
Node.DOCUMENT_POSITION_CONTAINED_BY ||
node.compareDocumentPosition(vElements[i]) === 20
) {
const texts = VEditor.getTextNodesFromElement(vElements[0]);
if (texts.length === 0) return null;
text = texts[texts.length - 1];
textOffset = offset === 0 ? offset : text.length;
break;
}
if (
i === 0 &&
node.compareDocumentPosition(vElements[i]) ===
Node.DOCUMENT_POSITION_FOLLOWING
) {
const texts = VEditor.getTextNodesFromElement(vElements[i]);
if (texts.length === 0) return null;
text = texts[0];
textOffset = offset === 0 ? offset : text.length;
break;
} else if (
i === vElements.length - 1 &&
node.compareDocumentPosition(vElements[i]) ===
Node.DOCUMENT_POSITION_PRECEDING
) {
const texts = VEditor.getTextNodesFromElement(vElements[i]);
if (texts.length === 0) return null;
text = texts[texts.length - 1];
textOffset = calculateTextLength(text);
break;
}
if (
i < vElements.length - 1 &&
node.compareDocumentPosition(vElements[i]) ===
Node.DOCUMENT_POSITION_PRECEDING &&
node.compareDocumentPosition(vElements[i + 1]) ===
Node.DOCUMENT_POSITION_FOLLOWING
) {
const texts = VEditor.getTextNodesFromElement(vElements[i]);
if (texts.length === 0) return null;
text = texts[texts.length - 1];
textOffset = calculateTextLength(text);
break;
}
}
}
}
}
if (!text) {
return null;
}
return [text, textOffset] as const;
}
static textPointToDomPoint(
text: Text,
offset: number,
rootElement: HTMLElement
): DomPoint | null {
if (rootElement.dataset.virgoRoot !== 'true') {
throw new Error(
'textRangeToDomPoint should be called with editor root element'
);
}
if (!rootElement.contains(text)) {
return null;
}
const texts = VEditor.getTextNodesFromElement(rootElement);
const goalIndex = texts.indexOf(text);
let index = 0;
for (const text of texts.slice(0, goalIndex)) {
index += calculateTextLength(text);
}
if (text.wholeText !== ZERO_WIDTH_SPACE) {
index += offset;
}
const textParentElement = text.parentElement;
if (!textParentElement) {
throw new Error('text element parent not found');
}
const lineElement = textParentElement.closest('v-line');
if (!lineElement) {
throw new Error('line element not found');
}
const lineIndex = Array.from(
rootElement.querySelectorAll('v-line')
).indexOf(lineElement);
return { text, index: index + lineIndex };
}
static getTextNodesFromElement(element: Element): Text[] {
const textSpanElements = Array.from(
element.querySelectorAll('[data-virgo-text="true"]')
);
const textNodes = textSpanElements.map(textSpanElement => {
const textNode = Array.from(textSpanElement.childNodes).find(
(node): node is Text => node instanceof Text
);
if (!textNode) {
throw new Error('text node not found');
}
return textNode;
});
return textNodes;
}
private readonly _yText: Y.Text;
private _rootElement: HTMLElement | null = null;
private _vRange: VRange | null = null;
private _isReadonly = false;
private _marks: TextAttributes | null = null;
private _attributesRenderer: AttributesRenderer<TextAttributes> =
getDefaultAttributeRenderer<TextAttributes>();
private _attributesSchema: z.ZodSchema<TextAttributes, ZodTypeDef, unknown> =
baseTextAttributes as z.ZodSchema<TextAttributes, ZodTypeDef, unknown>;
private _eventService: VirgoEventService<TextAttributes> =
new VirgoEventService<TextAttributes>(this);
private _parseSchema = (textAttributes?: TextAttributes) => {
if (!textAttributes) {
return undefined;
}
const attributesResult = this._attributesSchema.safeParse(textAttributes);
if (!attributesResult.success) {
console.error(attributesResult.error);
return undefined;
}
const attributes = Object.fromEntries(
// filter out undefined values
Object.entries(attributesResult.data).filter(([k, v]) => v)
) as TextAttributes;
return attributes;
};
private _rangeService: VirgoRangeService<TextAttributes> =
new VirgoRangeService<TextAttributes>(this);
private _renderDeltas = async () => {
assertExists(this._rootElement);
private _attributeService: VirgoAttributeService<TextAttributes> =
new VirgoAttributeService<TextAttributes>(this);
const deltas = this.yText.toDelta() as DeltaInsert<TextAttributes>[];
const chunks = deltaInsertsToChunks(deltas);
private _deltaService: VirgoDeltaService<TextAttributes> =
new VirgoDeltaService<TextAttributes>(this);
// every chunk is a line
const lines = chunks.map(chunk => {
const virgoLine = new VirgoLine<TextAttributes>();
if (chunk.length === 0) {
virgoLine.elements.push(new VirgoElement());
} else {
chunk.forEach(delta => {
const element = renderElement(
delta,
this._parseSchema,
this._attributesRenderer
);
virgoLine.elements.push(element);
});
}
return virgoLine;
});
this._rootElement.replaceChildren(...lines);
await Promise.all(
lines.map(async line => {
await line.updateComplete;
})
);
//FIXME: wait for render refactor
this.rootElement.focus({ preventScroll: true });
this.slots.updated.emit();
};
shouldScrollIntoView = true;

@@ -281,7 +74,59 @@

get eventService() {
return this._eventService;
}
get rangeService() {
return this._rangeService;
}
get attributeService() {
return this._attributeService;
}
get deltaService() {
return this._deltaService;
}
// Expose attribute service API
get marks() {
return this._marks;
return this._attributeService.marks;
}
constructor(yText: VEditor['yText']) {
setAttributeSchema = this._attributeService.setAttributeSchema;
setAttributeRenderer = this._attributeService.setAttributeRenderer;
setMarks = this._attributeService.setMarks;
resetMarks = this._attributeService.resetMarks;
getFormat = this._attributeService.getFormat;
// Expose event service API
bindHandlers = this._eventService.bindHandlers;
// Expose range service API
toDomRange = this.rangeService.toDomRange;
toVRange = this.rangeService.toVRange;
getVRange = this.rangeService.getVRange;
setVRange = this.rangeService.setVRange;
syncVRange = this.rangeService.syncVRange;
// Expose delta service API
getDeltasByVRange = this.deltaService.getDeltasByVRange;
getDeltaByRangeIndex = this.deltaService.getDeltaByRangeIndex;
mapDeltasInVRange = this.deltaService.mapDeltasInVRange;
constructor(
text: VEditor['yText'] | string,
options: VEditorOptions = {
defaultMode: 'rich',
}
) {
let yText: Y.Text;
if (typeof text === 'string') {
const temporaryYDoc = new Y.Doc();
yText = temporaryYDoc.getText('text');
yText.insert(0, text);
} else {
yText = text;
}
if (!yText.doc) {

@@ -297,2 +142,10 @@ throw new Error('yText must be attached to a Y.Doc');

// we can change default render to pure for making `VEditor` to be a pure string render,
// you can change schema and renderer again after construction
if (options.defaultMode === 'pure') {
this._attributeService.setAttributeRenderer(delta => {
return html`<span><v-text .str="${delta.insert}"></v-text></span>`;
});
}
this._yText = yText;

@@ -308,17 +161,5 @@

this.slots.vRangeUpdated.on(this._onVRangeUpdated);
this.slots.vRangeUpdated.on(this.rangeService.onVRangeUpdated);
}
setAttributesSchema = (
schema: z.ZodSchema<TextAttributes, ZodTypeDef, unknown>
) => {
this._attributesSchema = schema;
};
setAttributesRenderer = (renderer: AttributesRenderer<TextAttributes>) => {
this._attributesRenderer = renderer;
};
bindHandlers = this._eventService.bindHandlers;
mount(rootElement: HTMLElement) {

@@ -331,3 +172,3 @@ this._rootElement = rootElement;

this._renderDeltas();
this._deltaService.render();

@@ -341,2 +182,3 @@ this._eventService.mount();

this._eventService.unmount();
this.yText.unobserve(this._onYTextChange);

@@ -353,3 +195,3 @@ this._rootElement?.replaceChildren();

this._renderDeltas();
this._deltaService.render();
});

@@ -367,42 +209,2 @@ }

/**
* Here are examples of how this function computes and gets the delta.
*
* We have such a text:
* ```
* [
* {
* insert: 'aaa',
* attributes: { bold: true },
* },
* {
* insert: 'bbb',
* attributes: { italic: true },
* },
* ]
* ```
*
* `getDeltaByRangeIndex(0)` returns `{ insert: 'aaa', attributes: { bold: true } }`.
*
* `getDeltaByRangeIndex(1)` returns `{ insert: 'aaa', attributes: { bold: true } }`.
*
* `getDeltaByRangeIndex(3)` returns `{ insert: 'aaa', attributes: { bold: true } }`.
*
* `getDeltaByRangeIndex(4)` returns `{ insert: 'bbb', attributes: { italic: true } }`.
*/
getDeltaByRangeIndex(rangeIndex: VRange['index']): DeltaInsert | null {
const deltas = this.yText.toDelta() as DeltaInsert[];
let index = 0;
for (let i = 0; i < deltas.length; i++) {
const delta = deltas[i];
if (index + delta.insert.length >= rangeIndex) {
return delta;
}
index += delta.insert.length;
}
return null;
}
getTextPoint(rangeIndex: VRange['index']): TextPoint {

@@ -457,122 +259,2 @@ assertExists(this._rootElement);

/**
* Here are examples of how this function computes and gets the deltas.
*
* We have such a text:
* ```
* [
* {
* insert: 'aaa',
* attributes: { bold: true },
* },
* {
* insert: 'bbb',
* attributes: { italic: true },
* },
* {
* insert: 'ccc',
* attributes: { underline: true },
* },
* ]
* ```
*
* `getDeltasByVRange({ index: 0, length: 0 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }]]
* ```
*
* `getDeltasByVRange({ index: 0, length: 1 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }]]
* ```
*
* `getDeltasByVRange({ index: 0, length: 4 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }],
* [{ insert: 'bbb', attributes: { italic: true }, }, { index: 3, length: 3, }]]
* ```
*
* `getDeltasByVRange({ index: 3, length: 1 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }],
* [{ insert: 'bbb', attributes: { italic: true }, }, { index: 3, length: 3, }]]
* ```
*
* `getDeltasByVRange({ index: 3, length: 3 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }],
* [{ insert: 'bbb', attributes: { italic: true }, }, { index: 3, length: 3, }]]
* ```
*
* `getDeltasByVRange({ index: 3, length: 4 })` returns
* ```
* [{ insert: 'aaa', attributes: { bold: true }, }, { index: 0, length: 3, }],
* [{ insert: 'bbb', attributes: { italic: true }, }, { index: 3, length: 3, }],
* [{ insert: 'ccc', attributes: { underline: true }, }, { index: 6, length: 3, }]]
* ```
*/
getDeltasByVRange(vRange: VRange): DeltaEntry[] {
const deltas = this.yText.toDelta() as DeltaInsert[];
const result: DeltaEntry[] = [];
let index = 0;
for (let i = 0; i < deltas.length; i++) {
const delta = deltas[i];
if (
index + delta.insert.length >= vRange.index &&
(index < vRange.index + vRange.length ||
(vRange.length === 0 && index === vRange.index))
) {
result.push([delta, { index, length: delta.insert.length }]);
}
index += delta.insert.length;
}
return result;
}
getVRange(): VRange | null {
return this._vRange;
}
getFormat(vRange: VRange): TextAttributes {
const deltas = this.getDeltasByVRange(vRange).filter(
([delta, position]) =>
position.index + position.length > vRange.index &&
position.index <= vRange.index + vRange.length
);
const maybeAttributesArray = deltas.map(([delta]) => delta.attributes);
if (
!maybeAttributesArray.length ||
// some text does not have any attributes
maybeAttributesArray.some(attributes => !attributes)
) {
return {} as TextAttributes;
}
const attributesArray = maybeAttributesArray as TextAttributes[];
return attributesArray.reduce((acc, cur) => {
const newFormat = {} as TextAttributes;
for (const key in acc) {
const typedKey = key as keyof TextAttributes;
// If the given range contains multiple different formats
// such as links with different values,
// we will treat it as having no format
if (acc[typedKey] === cur[typedKey]) {
// This cast is secure because we have checked that the value of the key is the same.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
newFormat[typedKey] = acc[typedKey] as any;
}
}
return newFormat;
});
}
setMarks(marks: TextAttributes): void {
this._marks = marks;
}
resetMarks(): void {
this._marks = null;
}
setReadonly(isReadonly: boolean): void {

@@ -590,11 +272,4 @@ this.rootElement.contentEditable = isReadonly ? 'false' : 'true';

*/
setVRange(vRange: VRange): void {
this.slots.vRangeUpdated.emit([vRange, 'other']);
}
/**
* the vRange is synced to the native selection asynchronically
*/
focusEnd(): void {
this.setVRange({
this.rangeService.setVRange({
index: this.yText.length,

@@ -616,6 +291,7 @@ length: 0,

): void {
if (this._marks) {
attributes = { ...attributes, ...this._marks };
if (this._attributeService.marks) {
attributes = { ...attributes, ...this._attributeService.marks };
}
const normalizedAttributes = this._parseSchema(attributes);
const normalizedAttributes =
this._attributeService.normalizeAttributes(attributes);

@@ -648,3 +324,3 @@ if (!text || !text.length) {

const { match = () => true, mode = 'merge' } = options;
const deltas = this.getDeltasByVRange(vRange);
const deltas = this._deltaService.getDeltasByVRange(vRange);

@@ -654,10 +330,6 @@ deltas

.forEach(([delta, deltaVRange]) => {
const targetVRange = {
index: Math.max(vRange.index, deltaVRange.index),
length:
Math.min(
vRange.index + vRange.length,
deltaVRange.index + deltaVRange.length
) - Math.max(vRange.index, deltaVRange.index),
};
const targetVRange = this._rangeService.mergeRanges(
vRange,
deltaVRange
);

@@ -702,253 +374,2 @@ if (mode === 'replace') {

/**
* sync the dom selection from vRange for **this Editor**
*/
syncVRange(): void {
if (this._vRange) {
this._applyVRange(this._vRange);
}
}
private _applyVRange = (vRange: VRange): void => {
const newRange = this.toDomRange(vRange);
if (!newRange) {
return;
}
const selectionRoot = findDocumentOrShadowRoot(this);
const selection = selectionRoot.getSelection();
if (!selection) {
return;
}
selection.removeAllRanges();
selection.addRange(newRange);
//TODO: wait for render refactor
// this.rootElement.focus({ preventScroll: true});
if (this.shouldScrollIntoView) {
let lineElement: HTMLElement | null = newRange.endContainer.parentElement;
while (!(lineElement instanceof VirgoLine)) {
lineElement = lineElement?.parentElement ?? null;
}
lineElement?.scrollIntoView({
block: 'nearest',
});
}
this.slots.rangeUpdated.emit(newRange);
};
/**
* calculate the dom selection from vRange for **this Editor**
*/
toDomRange(vRange: VRange): Range | null {
assertExists(this._rootElement);
const lineElements = Array.from(
this._rootElement.querySelectorAll('v-line')
);
// calculate anchorNode and focusNode
let anchorText: Text | null = null;
let focusText: Text | null = null;
let anchorOffset = 0;
let focusOffset = 0;
let index = 0;
for (let i = 0; i < lineElements.length; i++) {
if (anchorText && focusText) {
break;
}
const texts = VEditor.getTextNodesFromElement(lineElements[i]);
for (const text of texts) {
const textLength = calculateTextLength(text);
if (!anchorText && index + textLength >= vRange.index) {
anchorText = text;
anchorOffset = vRange.index - index;
}
if (!focusText && index + textLength >= vRange.index + vRange.length) {
focusText = text;
focusOffset = vRange.index + vRange.length - index;
}
if (anchorText && focusText) {
break;
}
index += textLength;
}
// the one because of the line break
index += 1;
}
if (!anchorText || !focusText) {
return null;
}
const range = document.createRange();
range.setStart(anchorText, anchorOffset);
range.setEnd(focusText, focusOffset);
return range;
}
/**
* calculate the vRange from dom selection for **this Editor**
* there are three cases when the vRange of this Editor is not null:
* (In the following, "|" mean anchor and focus, each line is a separate Editor)
* 1. anchor and focus are in this Editor
* aaaaaa
* b|bbbb|b
* cccccc
* the vRange of second Editor is {index: 1, length: 4}, the others are null
* 2. anchor and focus one in this Editor, one in another Editor
* aaa|aaa aaaaaa
* bbbbb|b or bbbbb|b
* cccccc cc|cccc
* 2.1
* the vRange of first Editor is {index: 3, length: 3}, the second is {index: 0, length: 5},
* the third is null
* 2.2
* the vRange of first Editor is null, the second is {index: 5, length: 1},
* the third is {index: 0, length: 2}
* 3. anchor and focus are in another Editor
* aa|aaaa
* bbbbbb
* cccc|cc
* the vRange of first Editor is {index: 2, length: 4},
* the second is {index: 0, length: 6}, the third is {index: 0, length: 4}
*/
toVRange(selection: Selection): VRange | null {
assertExists(this._rootElement);
const root = this._rootElement;
const { anchorNode, anchorOffset, focusNode, focusOffset } = selection;
if (!anchorNode || !focusNode) {
return null;
}
const anchorTextPoint = VEditor.nativePointToTextPoint(
anchorNode,
anchorOffset
);
const focusTextPoint = VEditor.nativePointToTextPoint(
focusNode,
focusOffset
);
if (!anchorTextPoint || !focusTextPoint) {
return null;
}
const [anchorText, anchorTextOffset] = anchorTextPoint;
const [focusText, focusTextOffset] = focusTextPoint;
// case 1
if (root.contains(anchorText) && root.contains(focusText)) {
const anchorDomPoint = VEditor.textPointToDomPoint(
anchorText,
anchorTextOffset,
this._rootElement
);
const focusDomPoint = VEditor.textPointToDomPoint(
focusText,
focusTextOffset,
this._rootElement
);
if (!anchorDomPoint || !focusDomPoint) {
return null;
}
return {
index: Math.min(anchorDomPoint.index, focusDomPoint.index),
length: Math.abs(anchorDomPoint.index - focusDomPoint.index),
};
}
// case 2.1
if (!root.contains(anchorText) && root.contains(focusText)) {
if (isSelectionBackwards(selection)) {
const anchorDomPoint = VEditor.textPointToDomPoint(
anchorText,
anchorTextOffset,
this._rootElement
);
if (!anchorDomPoint) {
return null;
}
return {
index: anchorDomPoint.index,
length: this.yText.length - anchorDomPoint.index,
};
} else {
const focusDomPoint = VEditor.textPointToDomPoint(
focusText,
focusTextOffset,
this._rootElement
);
if (!focusDomPoint) {
return null;
}
return {
index: 0,
length: focusDomPoint.index,
};
}
}
// case 2.2
if (root.contains(anchorText) && !root.contains(focusText)) {
if (isSelectionBackwards(selection)) {
const focusDomPoint = VEditor.textPointToDomPoint(
focusText,
focusTextOffset,
this._rootElement
);
if (!focusDomPoint) {
return null;
}
return {
index: 0,
length: focusDomPoint.index,
};
} else {
const anchorDomPoint = VEditor.textPointToDomPoint(
anchorText,
anchorTextOffset,
this._rootElement
);
if (!anchorDomPoint) {
return null;
}
return {
index: anchorDomPoint.index,
length: this.yText.length - anchorDomPoint.index,
};
}
}
// case 3
if (!root.contains(anchorText) && !root.contains(focusText)) {
return {
index: 0,
length: this.yText.length,
};
}
return null;
}
private _onYTextChange = () => {

@@ -964,28 +385,6 @@ if (this.yText.toString().includes('\r')) {

this._renderDeltas();
this.deltaService.render();
});
};
private _onVRangeUpdated = ([newVRange, origin]: VRangeUpdatedProp) => {
this._vRange = newVRange;
if (origin === 'native') {
return;
}
// avoid cursor jumping to beginning in a moment
this._rootElement?.blur();
const fn = () => {
if (newVRange) {
// when using input method _vRange will return to the starting point,
// so we need to re-sync
this._applyVRange(newVRange);
}
};
// updates in lit are performed asynchronously
requestAnimationFrame(fn);
};
private _transact(fn: () => void): void {

@@ -992,0 +391,0 @@ const doc = this.yText.doc;

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc