@blocksuite/virgo
Advanced tools
Comparing version 0.4.0-alpha.3 to 0.4.0-alpha.4
import { LitElement } from 'lit'; | ||
import type { BaseArrtiubtes, DeltaInsert } from '../types.js'; | ||
import { z } from 'zod'; | ||
import type { DeltaInsert } from '../types.js'; | ||
export declare const baseTextAttributes: z.ZodOptional<z.ZodObject<{ | ||
bold: z.ZodOptional<z.ZodBoolean>; | ||
italic: z.ZodOptional<z.ZodBoolean>; | ||
underline: z.ZodOptional<z.ZodBoolean>; | ||
strikethrough: z.ZodOptional<z.ZodBoolean>; | ||
inlineCode: z.ZodOptional<z.ZodBoolean>; | ||
color: z.ZodOptional<z.ZodString>; | ||
link: z.ZodOptional<z.ZodString>; | ||
}, "strip", z.ZodTypeAny, { | ||
bold?: boolean | undefined; | ||
italic?: boolean | undefined; | ||
underline?: boolean | undefined; | ||
strikethrough?: boolean | undefined; | ||
inlineCode?: boolean | undefined; | ||
color?: string | undefined; | ||
link?: string | undefined; | ||
}, { | ||
bold?: boolean | undefined; | ||
italic?: boolean | undefined; | ||
underline?: boolean | undefined; | ||
strikethrough?: boolean | undefined; | ||
inlineCode?: boolean | undefined; | ||
color?: string | undefined; | ||
link?: string | undefined; | ||
}>>; | ||
export type BaseTextAttributes = z.infer<typeof baseTextAttributes>; | ||
export declare class BaseText extends LitElement { | ||
delta: DeltaInsert<BaseArrtiubtes>; | ||
delta: DeltaInsert<BaseTextAttributes>; | ||
render(): import("lit-html").TemplateResult<1>; | ||
@@ -6,0 +33,0 @@ createRenderRoot(): this; |
@@ -10,5 +10,19 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { | ||
import { styleMap } from 'lit/directives/style-map.js'; | ||
import { z } from 'zod'; | ||
import { ZERO_WIDTH_SPACE } from '../constant.js'; | ||
import { VirgoUnitText } from './virgo-unit-text.js'; | ||
export const baseTextAttributes = z | ||
.object({ | ||
bold: z.boolean().optional(), | ||
italic: z.boolean().optional(), | ||
underline: z.boolean().optional(), | ||
strikethrough: z.boolean().optional(), | ||
inlineCode: z.boolean().optional(), | ||
color: z.string().optional(), | ||
link: z.string().optional(), | ||
}) | ||
.optional(); | ||
function virgoTextStyles(props) { | ||
if (!props) | ||
return styleMap({}); | ||
let textDecorations = ''; | ||
@@ -21,2 +35,14 @@ if (props.underline) { | ||
} | ||
let inlineCodeStyle = {}; | ||
if (props.inlineCode) { | ||
inlineCodeStyle = { | ||
'font-family': '"SFMono-Regular", Menlo, Consolas, "PT Mono", "Liberation Mono", Courier, monospace', | ||
'line-height': 'normal', | ||
background: 'rgba(135,131,120,0.15)', | ||
color: '#EB5757', | ||
'border-radius': '3px', | ||
'font-size': '85%', | ||
padding: '0.2em 0.4em', | ||
}; | ||
} | ||
return styleMap({ | ||
@@ -27,2 +53,3 @@ 'white-space': 'break-spaces', | ||
'text-decoration': textDecorations.length > 0 ? textDecorations : 'none', | ||
...inlineCodeStyle, | ||
}); | ||
@@ -35,5 +62,2 @@ } | ||
insert: ZERO_WIDTH_SPACE, | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}; | ||
@@ -43,3 +67,3 @@ } | ||
const unitText = new VirgoUnitText(); | ||
unitText.delta = this.delta; | ||
unitText.str = this.delta.insert; | ||
// we need to avoid \n appearing before and after the span element, which will | ||
@@ -46,0 +70,0 @@ // cause the unexpected space |
export * from './base-text.js'; | ||
export * from './virgo-line.js'; | ||
export * from './virgo-unit-text.js'; | ||
export * from './optional/inline-code.js'; | ||
//# sourceMappingURL=index.d.ts.map |
export * from './base-text.js'; | ||
export * from './virgo-line.js'; | ||
export * from './virgo-unit-text.js'; | ||
// optional elements | ||
export * from './optional/inline-code.js'; | ||
//# sourceMappingURL=index.js.map |
import { LitElement } from 'lit'; | ||
import type { BaseArrtiubtes, DeltaInsert } from '../types.js'; | ||
export declare class VirgoUnitText extends LitElement { | ||
delta: DeltaInsert<BaseArrtiubtes>; | ||
str: string; | ||
render(): import("lit-html").TemplateResult<1>; | ||
@@ -6,0 +5,0 @@ createRenderRoot(): this; |
@@ -9,12 +9,11 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { | ||
import { customElement, property } from 'lit/decorators.js'; | ||
import { styleMap } from 'lit/directives/style-map.js'; | ||
import { ZERO_WIDTH_SPACE } from '../constant.js'; | ||
const unitTextStyles = styleMap({ | ||
whiteSpace: 'pre', | ||
}); | ||
let VirgoUnitText = class VirgoUnitText extends LitElement { | ||
constructor() { | ||
super(...arguments); | ||
this.delta = { | ||
insert: ZERO_WIDTH_SPACE, | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}; | ||
this.str = ZERO_WIDTH_SPACE; | ||
} | ||
@@ -24,3 +23,5 @@ render() { | ||
// cause the sync problem about the cursor position | ||
return html `<span data-virgo-text="true">${this.delta.insert}</span>`; | ||
return html `<span style=${unitTextStyles} data-virgo-text="true" | ||
>${this.str}</span | ||
>`; | ||
} | ||
@@ -32,4 +33,4 @@ createRenderRoot() { | ||
__decorate([ | ||
property({ type: Object }) | ||
], VirgoUnitText.prototype, "delta", void 0); | ||
property() | ||
], VirgoUnitText.prototype, "str", void 0); | ||
VirgoUnitText = __decorate([ | ||
@@ -36,0 +37,0 @@ customElement('virgo-unit-text') |
@@ -0,1 +1,2 @@ | ||
import { getDefaultPlaygroundURL } from '@blocksuite/global/utils'; | ||
export async function type(page, content) { | ||
@@ -5,3 +6,3 @@ await page.keyboard.type(content, { delay: 50 }); | ||
export async function enterVirgoPlayground(page) { | ||
const url = new URL('examples/virgo/index.html', 'http://localhost:5173/'); | ||
const url = new URL('examples/virgo/index.html', getDefaultPlaygroundURL(!!process.env.CI)); | ||
await page.goto(url.toString()); | ||
@@ -8,0 +9,0 @@ } |
import { expect, test } from '@playwright/test'; | ||
import { ZERO_WIDTH_SPACE } from '../constant.js'; | ||
import { enterVirgoPlayground, focusVirgoRichText, getDeltaFromVirgoRichText, setVirgoRichTextRange, type, } from './utils/misc.js'; | ||
const ZERO_WIDTH_SPACE = '\u200B'; | ||
test('basic input', async ({ page }) => { | ||
@@ -39,2 +39,3 @@ await enterVirgoPlayground(page); | ||
await type(page, 'bbb'); | ||
page.waitForTimeout(100); | ||
expect(await editorA.innerText()).toBe('abc\n' + ZERO_WIDTH_SPACE + '\nbbb'); | ||
@@ -113,3 +114,3 @@ expect(await editorB.innerText()).toBe('abc\n' + ZERO_WIDTH_SPACE + '\nbbb'); | ||
}); | ||
test('basic text style', async ({ page }) => { | ||
test('basic styles', async ({ page }) => { | ||
await enterVirgoPlayground(page); | ||
@@ -124,3 +125,4 @@ await focusVirgoRichText(page); | ||
const editorAInlineCode = page.getByText('inline-code').nth(0); | ||
const editorAReset = page.getByText('reset').nth(0); | ||
const editorAUndo = page.getByText('undo').nth(0); | ||
const editorARedo = page.getByText('redo').nth(0); | ||
expect(await editorA.innerText()).toBe(ZERO_WIDTH_SPACE); | ||
@@ -135,5 +137,2 @@ expect(await editorB.innerText()).toBe(ZERO_WIDTH_SPACE); | ||
insert: 'abcdefg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -147,5 +146,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -155,3 +151,2 @@ { | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
@@ -162,5 +157,2 @@ }, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -173,5 +165,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -181,3 +170,2 @@ { | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
@@ -189,5 +177,2 @@ italic: true, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -200,5 +185,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -208,3 +190,2 @@ { | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
@@ -217,5 +198,2 @@ italic: true, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -228,10 +206,25 @@ ]); | ||
insert: 'ab', | ||
}, | ||
{ | ||
insert: 'cde', | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
italic: true, | ||
underline: true, | ||
strikethrough: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'fg', | ||
}, | ||
]); | ||
editorAInlineCode.click(); | ||
delta = await getDeltaFromVirgoRichText(page); | ||
expect(delta).toEqual([ | ||
{ | ||
insert: 'ab', | ||
}, | ||
{ | ||
insert: 'cde', | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
@@ -241,2 +234,3 @@ italic: true, | ||
strikethrough: true, | ||
inlineCode: true, | ||
}, | ||
@@ -246,6 +240,34 @@ }, | ||
insert: 'fg', | ||
}, | ||
]); | ||
editorAUndo.click({ | ||
clickCount: 5, | ||
}); | ||
delta = await getDeltaFromVirgoRichText(page); | ||
expect(delta).toEqual([ | ||
{ | ||
insert: 'abcdefg', | ||
}, | ||
]); | ||
editorARedo.click({ | ||
clickCount: 5, | ||
}); | ||
delta = await getDeltaFromVirgoRichText(page); | ||
expect(delta).toEqual([ | ||
{ | ||
insert: 'ab', | ||
}, | ||
{ | ||
insert: 'cde', | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
italic: true, | ||
underline: true, | ||
strikethrough: true, | ||
inlineCode: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'fg', | ||
}, | ||
]); | ||
@@ -257,5 +279,2 @@ editorABold.click(); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -265,6 +284,6 @@ { | ||
attributes: { | ||
type: 'base', | ||
italic: true, | ||
underline: true, | ||
strikethrough: true, | ||
inlineCode: true, | ||
}, | ||
@@ -274,5 +293,2 @@ }, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -285,5 +301,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -293,5 +306,5 @@ { | ||
attributes: { | ||
type: 'base', | ||
underline: true, | ||
strikethrough: true, | ||
inlineCode: true, | ||
}, | ||
@@ -301,5 +314,2 @@ }, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -312,5 +322,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -320,4 +327,4 @@ { | ||
attributes: { | ||
type: 'base', | ||
strikethrough: true, | ||
inlineCode: true, | ||
}, | ||
@@ -327,8 +334,21 @@ }, | ||
insert: 'fg', | ||
}, | ||
]); | ||
editorAStrikethrough.click(); | ||
delta = await getDeltaFromVirgoRichText(page); | ||
expect(delta).toEqual([ | ||
{ | ||
insert: 'ab', | ||
}, | ||
{ | ||
insert: 'cde', | ||
attributes: { | ||
type: 'base', | ||
inlineCode: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'fg', | ||
}, | ||
]); | ||
editorAStrikethrough.click(); | ||
editorAInlineCode.click(); | ||
delta = await getDeltaFromVirgoRichText(page); | ||
@@ -338,50 +358,184 @@ expect(delta).toEqual([ | ||
insert: 'abcdefg', | ||
}, | ||
]); | ||
}); | ||
test('overlapping styles', async ({ page }) => { | ||
await enterVirgoPlayground(page); | ||
await focusVirgoRichText(page); | ||
const editorA = page.locator('[data-virgo-root="true"]').nth(0); | ||
const editorB = page.locator('[data-virgo-root="true"]').nth(1); | ||
const editorABold = page.getByText('bold').nth(0); | ||
const editorAItalic = page.getByText('italic').nth(0); | ||
const editorAUndo = page.getByText('undo').nth(0); | ||
const editorARedo = page.getByText('redo').nth(0); | ||
expect(await editorA.innerText()).toBe(ZERO_WIDTH_SPACE); | ||
expect(await editorB.innerText()).toBe(ZERO_WIDTH_SPACE); | ||
await type(page, 'abcdefghijk'); | ||
expect(await editorA.innerText()).toBe('abcdefghijk'); | ||
expect(await editorB.innerText()).toBe('abcdefghijk'); | ||
let delta = await getDeltaFromVirgoRichText(page); | ||
expect(delta).toEqual([ | ||
{ | ||
insert: 'abcdefghijk', | ||
}, | ||
]); | ||
await setVirgoRichTextRange(page, { index: 1, length: 3 }); | ||
editorABold.click(); | ||
delta = await getDeltaFromVirgoRichText(page); | ||
expect(delta).toEqual([ | ||
{ | ||
insert: 'a', | ||
}, | ||
{ | ||
insert: 'bcd', | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'efghijk', | ||
}, | ||
]); | ||
editorAReset.click(); | ||
await setVirgoRichTextRange(page, { index: 7, length: 3 }); | ||
editorABold.click(); | ||
delta = await getDeltaFromVirgoRichText(page); | ||
expect(delta).toEqual([ | ||
{ | ||
insert: 'abcdefg', | ||
insert: 'a', | ||
}, | ||
{ | ||
insert: 'bcd', | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'efg', | ||
}, | ||
{ | ||
insert: 'hij', | ||
attributes: { | ||
bold: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'k', | ||
}, | ||
]); | ||
editorAInlineCode.click(); | ||
await setVirgoRichTextRange(page, { index: 3, length: 5 }); | ||
editorAItalic.click(); | ||
delta = await getDeltaFromVirgoRichText(page); | ||
expect(delta).toEqual([ | ||
{ | ||
insert: 'ab', | ||
insert: 'a', | ||
}, | ||
{ | ||
insert: 'bc', | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'cde', | ||
insert: 'd', | ||
attributes: { | ||
type: 'inline-code', | ||
bold: true, | ||
italic: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'fg', | ||
insert: 'efg', | ||
attributes: { | ||
type: 'base', | ||
italic: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'h', | ||
attributes: { | ||
bold: true, | ||
italic: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'ij', | ||
attributes: { | ||
bold: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'k', | ||
}, | ||
]); | ||
editorAInlineCode.click(); | ||
editorAUndo.click({ | ||
clickCount: 3, | ||
}); | ||
delta = await getDeltaFromVirgoRichText(page); | ||
expect(delta).toEqual([ | ||
{ | ||
insert: 'abcdefg', | ||
insert: 'abcdefghijk', | ||
}, | ||
]); | ||
editorARedo.click({ | ||
clickCount: 3, | ||
}); | ||
delta = await getDeltaFromVirgoRichText(page); | ||
expect(delta).toEqual([ | ||
{ | ||
insert: 'a', | ||
}, | ||
{ | ||
insert: 'bc', | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'd', | ||
attributes: { | ||
bold: true, | ||
italic: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'efg', | ||
attributes: { | ||
italic: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'h', | ||
attributes: { | ||
bold: true, | ||
italic: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'ij', | ||
attributes: { | ||
bold: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'k', | ||
}, | ||
]); | ||
}); | ||
test('input continuous spaces', async ({ page }) => { | ||
await enterVirgoPlayground(page); | ||
await focusVirgoRichText(page); | ||
const editorA = page.locator('[data-virgo-root="true"]').nth(0); | ||
const editorB = page.locator('[data-virgo-root="true"]').nth(1); | ||
expect(await editorA.innerText()).toBe(ZERO_WIDTH_SPACE); | ||
expect(await editorB.innerText()).toBe(ZERO_WIDTH_SPACE); | ||
await type(page, 'abc def'); | ||
expect(await editorA.innerText()).toBe('abc def'); | ||
expect(await editorB.innerText()).toBe('abc def'); | ||
await focusVirgoRichText(page); | ||
await page.keyboard.press('ArrowLeft'); | ||
await page.keyboard.press('ArrowLeft'); | ||
await page.keyboard.press('ArrowLeft'); | ||
await page.keyboard.press('ArrowLeft'); | ||
await page.keyboard.press('Enter'); | ||
expect(await editorA.innerText()).toBe('abc \n' + ' def'); | ||
expect(await editorB.innerText()).toBe('abc \n' + ' def'); | ||
}); | ||
//# sourceMappingURL=virgo.spec.js.map |
@@ -1,15 +0,2 @@ | ||
import type { BaseText } from './components/base-text.js'; | ||
import type { InlineCode, InlineCodeAttributes } from './components/optional/inline-code.js'; | ||
export interface BaseArrtiubtes { | ||
type: 'base'; | ||
bold?: true; | ||
italic?: true; | ||
underline?: true; | ||
strikethrough?: true; | ||
} | ||
export interface LineBreakAttributes { | ||
type: 'line-break'; | ||
} | ||
export type BaseTextElement = BaseText | InlineCode; | ||
export type BaseTextAttributes = BaseArrtiubtes | LineBreakAttributes | InlineCodeAttributes; | ||
import type { BaseText, BaseTextAttributes } from './components/base-text.js'; | ||
export interface CustomTypes { | ||
@@ -20,9 +7,9 @@ [key: string]: unknown; | ||
type ExtendedType<K extends ExtendableKeys, B> = unknown extends CustomTypes[K] ? B : CustomTypes[K]; | ||
export type TextElement = ExtendedType<'Element', BaseTextElement>; | ||
export type TextAttributes = ExtendedType<'Attributes', BaseTextAttributes>; | ||
export type TextElement = ExtendedType<'Element', BaseText>; | ||
export type DeltaInsert<A extends TextAttributes = TextAttributes> = { | ||
insert: string; | ||
attributes: A; | ||
attributes?: A; | ||
}; | ||
export {}; | ||
//# sourceMappingURL=types.d.ts.map |
import type { DeltaInsert } from '../types.js'; | ||
export declare function transformDelta(delta: DeltaInsert): (DeltaInsert | '\n')[]; | ||
/** | ||
* convert a delta insert array to chunks, each chunk is a line | ||
*/ | ||
export declare function deltaInsersToChunks(delta: DeltaInsert[]): DeltaInsert[][]; | ||
export declare function deltaInsertsToChunks(delta: DeltaInsert[]): DeltaInsert[][]; | ||
//# sourceMappingURL=convert.d.ts.map |
@@ -0,12 +1,36 @@ | ||
export function transformDelta(delta) { | ||
const result = []; | ||
let tmpString = delta.insert; | ||
while (tmpString.length > 0) { | ||
const index = tmpString.indexOf('\n'); | ||
if (index === -1) { | ||
result.push({ | ||
insert: tmpString, | ||
attributes: delta.attributes, | ||
}); | ||
break; | ||
} | ||
if (tmpString.slice(0, index).length > 0) { | ||
result.push({ | ||
insert: tmpString.slice(0, index), | ||
attributes: delta.attributes, | ||
}); | ||
} | ||
result.push('\n'); | ||
tmpString = tmpString.slice(index + 1); | ||
} | ||
return result; | ||
} | ||
/** | ||
* convert a delta insert array to chunks, each chunk is a line | ||
*/ | ||
export function deltaInsersToChunks(delta) { | ||
export function deltaInsertsToChunks(delta) { | ||
if (delta.length === 0) { | ||
return [[]]; | ||
} | ||
const transformedDelta = delta.flatMap(transformDelta); | ||
function* chunksGenerator(arr) { | ||
let start = 0; | ||
for (let i = 0; i < arr.length; i++) { | ||
if (arr[i].attributes.type === 'line-break') { | ||
if (arr[i] === '\n') { | ||
const chunk = arr.slice(start, i); | ||
@@ -20,8 +44,8 @@ start = i + 1; | ||
} | ||
if (arr[arr.length - 1].attributes.type === 'line-break') { | ||
if (arr.at(-1) === '\n') { | ||
yield []; | ||
} | ||
} | ||
return [...chunksGenerator(delta)]; | ||
return [...chunksGenerator(transformedDelta)]; | ||
} | ||
//# sourceMappingURL=convert.js.map |
@@ -1,2 +0,2 @@ | ||
import { BaseText } from '../components/base-text.js'; | ||
import { BaseText, baseTextAttributes } from '../components/base-text.js'; | ||
/** | ||
@@ -6,12 +6,10 @@ * a default render function for text element | ||
export function baseRenderElement(delta) { | ||
switch (delta.attributes.type) { | ||
case 'base': { | ||
const baseText = new BaseText(); | ||
baseText.delta = delta; | ||
return baseText; | ||
} | ||
default: | ||
throw new Error(`Unknown text type: ${delta.attributes.type}`); | ||
} | ||
const parseResult = baseTextAttributes.parse(delta.attributes); | ||
const baseText = new BaseText(); | ||
baseText.delta = { | ||
insert: delta.insert, | ||
attributes: parseResult, | ||
}; | ||
return baseText; | ||
} | ||
//# sourceMappingURL=render.js.map |
@@ -28,3 +28,2 @@ import { Signal } from '@blocksuite/global/utils'; | ||
unmount(): void; | ||
getBaseElement(node: Node): TextElement | null; | ||
getNativeSelection(): Selection | null; | ||
@@ -42,3 +41,3 @@ getDeltaByRangeIndex(rangeIndex: VRange['index']): DeltaInsert | null; | ||
insertLineBreak(vRange: VRange): void; | ||
formatText(vRange: VRange, attributes: TextAttributes, options?: { | ||
formatText(vRange: VRange, attributes: NonNullable<TextAttributes>, options?: { | ||
match?: (delta: DeltaInsert, deltaVRange: VRange) => boolean; | ||
@@ -45,0 +44,0 @@ mode?: 'replace' | 'merge'; |
@@ -5,3 +5,3 @@ import { assertExists, Signal } from '@blocksuite/global/utils'; | ||
import { ZERO_WIDTH_SPACE } from './constant.js'; | ||
import { deltaInsersToChunks } from './utils/convert.js'; | ||
import { deltaInsertsToChunks } from './utils/convert.js'; | ||
import { baseRenderElement } from './utils/render.js'; | ||
@@ -21,11 +21,3 @@ export class VEditor { | ||
assertExists(this._rootElement); | ||
const deltas = this.yText.toDelta().flatMap(d => { | ||
if (d.attributes.type === 'line-break') { | ||
return d.insert | ||
.split('') | ||
.map(c => ({ insert: c, attributes: d.attributes })); | ||
} | ||
return d; | ||
}); | ||
renderDeltas(deltas, this._rootElement, this._renderElement); | ||
renderDeltas(this.yText.toDelta(), this._rootElement, this._renderElement); | ||
}; | ||
@@ -78,3 +70,5 @@ this._onSelectionChange = () => { | ||
if (onKeyDown) { | ||
this._onKeyDown = onKeyDown; | ||
this._onKeyDown = e => { | ||
onKeyDown(e); | ||
}; | ||
} | ||
@@ -125,9 +119,2 @@ this.signals = { | ||
} | ||
getBaseElement(node) { | ||
const element = node.parentElement?.closest('[data-virgo-element="true"]'); | ||
if (element) { | ||
return element; | ||
} | ||
return null; | ||
} | ||
getNativeSelection() { | ||
@@ -198,15 +185,6 @@ const selectionRoot = findDocumentOrShadowRoot(this); | ||
} | ||
// TODO add support for formatting | ||
insertText(vRange, text) { | ||
const currentDelta = this.getDeltaByRangeIndex(vRange.index); | ||
this._transact(() => { | ||
this.yText.delete(vRange.index, vRange.length); | ||
if (vRange.index > 0 && | ||
currentDelta && | ||
currentDelta.attributes.type !== 'line-break') { | ||
this.yText.insert(vRange.index, text, currentDelta.attributes); | ||
} | ||
else { | ||
this.yText.insert(vRange.index, text, { type: 'base' }); | ||
} | ||
this.yText.insert(vRange.index, text); | ||
}); | ||
@@ -217,3 +195,3 @@ } | ||
this.yText.delete(vRange.index, vRange.length); | ||
this.yText.insert(vRange.index, '\n', { type: 'line-break' }); | ||
this.yText.insert(vRange.index, '\n'); | ||
}); | ||
@@ -225,5 +203,2 @@ } | ||
for (const [delta, deltaVRange] of deltas) { | ||
if (delta.attributes.type === 'line-break') { | ||
continue; | ||
} | ||
if (match(delta, deltaVRange)) { | ||
@@ -251,7 +226,8 @@ const targetVRange = { | ||
} | ||
const unset = Object.fromEntries(coverDeltas.flatMap(delta => Object.keys(delta.attributes).map(key => [key, null]))); | ||
const unset = Object.fromEntries(coverDeltas.flatMap(delta => delta.attributes | ||
? Object.keys(delta.attributes).map(key => [key, null]) | ||
: [])); | ||
this._transact(() => { | ||
this.yText.format(vRange.index, vRange.length, { | ||
...unset, | ||
type: 'base', | ||
}); | ||
@@ -264,3 +240,3 @@ }); | ||
syncVRange() { | ||
setTimeout(() => { | ||
requestAnimationFrame(() => { | ||
if (this._vRange) { | ||
@@ -612,3 +588,3 @@ const newRange = this.toDomRange(this._vRange); | ||
function renderDeltas(deltas, rootElement, render) { | ||
const chunks = deltaInsersToChunks(deltas); | ||
const chunks = deltaInsertsToChunks(deltas); | ||
// every chunk is a line | ||
@@ -615,0 +591,0 @@ const lines = []; |
{ | ||
"name": "@blocksuite/virgo", | ||
"version": "0.4.0-alpha.3", | ||
"version": "0.4.0-alpha.4", | ||
"description": "A micro editor.", | ||
@@ -11,8 +11,8 @@ "main": "dist/index.js", | ||
"devDependencies": { | ||
"yjs": "^13.5.45", | ||
"lit": "^2.6.1" | ||
"lit": "^2.6.1", | ||
"yjs": "^13.5.46" | ||
}, | ||
"peerDependencies": { | ||
"yjs": "^13", | ||
"lit": "^2" | ||
"lit": "^2", | ||
"yjs": "^13" | ||
}, | ||
@@ -27,8 +27,14 @@ "exports": { | ||
"dependencies": { | ||
"@blocksuite/global": "0.4.0-alpha.3" | ||
"@blocksuite/global": "0.4.0-alpha.4", | ||
"zod": "^3.20.6" | ||
}, | ||
"scripts": { | ||
"build": "tsc" | ||
"build": "tsc", | ||
"test:unit": "vitest --run", | ||
"test:unit:coverage": "vitest run --coverage", | ||
"test:unit:ui": "vitest --ui", | ||
"test:e2e": "playwright test", | ||
"test": "pnpm test:unit && pnpm test:e2e" | ||
}, | ||
"types": "dist/index.d.ts" | ||
} |
@@ -5,3 +5,3 @@ # `@blocksuite/virgo` | ||
Virgo is a minimized rich-text editing kernel that synchronizes the state between DOM and [Y.Text](https://docs.yjs.dev/api/shared-types/y.text), which differs from other rich-text editing frameworks in that its data model are _natively_ CRDT. For example, to support collaborative editing in Slate.js, you may need to use a plugin like slate-yjs, a wrapper around [Yjs](https://github.com/yjs/yjs). In these plugins, all text operations should be converted between Yjs and Slate.js operations. This may result in undo/redo properly and hard to maintain the code. However, with Virgo, we can directly synchronize the DOM state between Yjs and DOM, which means that the state in Yjs is the single source of truth. This means that to update, can just calling the `Y.Text` API to manipulate the DOM state, which could significantly reduces the complexity of the editor. | ||
Virgo is a minimized rich-text editing kernel that synchronizes the state between DOM and [Y.Text](https://docs.yjs.dev/api/shared-types/y.text), which differs from other rich-text editing frameworks in that its data model are _natively_ CRDT. For example, to support collaborative editing in Slate.js, you may need to use a plugin like slate-yjs, a wrapper around [Yjs](https://github.com/yjs/yjs). In these plugins, all text operations should be converted between Yjs and Slate.js operations. This may result in undo/redo properly and hard to maintain the code. However, with Virgo, we can directly synchronize the DOM state between Yjs and DOM, which means that the state in Yjs is the single source of truth. It signify that to update, can just calling the `Y.Text` API to manipulate the DOM state, which could significantly reduces the complexity of the editor. | ||
@@ -23,19 +23,4 @@ Initially in BlockSuite, we use [Quill](https://github.com/quilljs/quill) for in-block rich-text editing, which only utilizes a small subset of its APIs. Every paragraph in BlockSuite is managed in a standalone Quill instance, which is attached to a `Y.Text` instance for collaborative editing. Virgo makes this further simpler, since what it needs to do is the same as how we use the Quill subset. It just needs to provide a flat rich-text synchronization mechanism, since the block-tree-level state management is handled by the data store in BlockSuite. | ||
{ | ||
insert: 'aaa', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
insert: 'aaa\nbbb', | ||
}, | ||
{ | ||
insert: '\n', | ||
attributes: { | ||
type: 'line-break', | ||
}, | ||
}, | ||
{ | ||
insert: 'bbb', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
]; | ||
@@ -58,3 +43,2 @@ */ | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
@@ -64,19 +48,4 @@ }, | ||
{ | ||
insert: 'a', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
insert: 'a\nbbb', | ||
}, | ||
{ | ||
insert: '\n', | ||
attributes: { | ||
type: 'line-break', | ||
}, | ||
}, | ||
{ | ||
insert: 'bbb', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
]; | ||
@@ -83,0 +52,0 @@ */ |
import { html, LitElement } from 'lit'; | ||
import { customElement, property } from 'lit/decorators.js'; | ||
import { styleMap } from 'lit/directives/style-map.js'; | ||
import { z } from 'zod'; | ||
import { ZERO_WIDTH_SPACE } from '../constant.js'; | ||
import type { BaseArrtiubtes, DeltaInsert } from '../types.js'; | ||
import type { DeltaInsert } from '../types.js'; | ||
import { VirgoUnitText } from './virgo-unit-text.js'; | ||
function virgoTextStyles(props: BaseArrtiubtes): ReturnType<typeof styleMap> { | ||
export const baseTextAttributes = z | ||
.object({ | ||
bold: z.boolean().optional(), | ||
italic: z.boolean().optional(), | ||
underline: z.boolean().optional(), | ||
strikethrough: z.boolean().optional(), | ||
inlineCode: z.boolean().optional(), | ||
color: z.string().optional(), | ||
link: z.string().optional(), | ||
}) | ||
.optional(); | ||
export type BaseTextAttributes = z.infer<typeof baseTextAttributes>; | ||
function virgoTextStyles( | ||
props: BaseTextAttributes | ||
): ReturnType<typeof styleMap> { | ||
if (!props) return styleMap({}); | ||
let textDecorations = ''; | ||
@@ -18,2 +37,16 @@ if (props.underline) { | ||
let inlineCodeStyle = {}; | ||
if (props.inlineCode) { | ||
inlineCodeStyle = { | ||
'font-family': | ||
'"SFMono-Regular", Menlo, Consolas, "PT Mono", "Liberation Mono", Courier, monospace', | ||
'line-height': 'normal', | ||
background: 'rgba(135,131,120,0.15)', | ||
color: '#EB5757', | ||
'border-radius': '3px', | ||
'font-size': '85%', | ||
padding: '0.2em 0.4em', | ||
}; | ||
} | ||
return styleMap({ | ||
@@ -24,2 +57,3 @@ 'white-space': 'break-spaces', | ||
'text-decoration': textDecorations.length > 0 ? textDecorations : 'none', | ||
...inlineCodeStyle, | ||
}); | ||
@@ -31,7 +65,4 @@ } | ||
@property({ type: Object }) | ||
delta: DeltaInsert<BaseArrtiubtes> = { | ||
delta: DeltaInsert<BaseTextAttributes> = { | ||
insert: ZERO_WIDTH_SPACE, | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}; | ||
@@ -41,3 +72,3 @@ | ||
const unitText = new VirgoUnitText(); | ||
unitText.delta = this.delta; | ||
unitText.str = this.delta.insert; | ||
@@ -44,0 +75,0 @@ // we need to avoid \n appearing before and after the span element, which will |
export * from './base-text.js'; | ||
export * from './virgo-line.js'; | ||
export * from './virgo-unit-text.js'; | ||
// optional elements | ||
export * from './optional/inline-code.js'; |
import { html, LitElement } from 'lit'; | ||
import { customElement, property } from 'lit/decorators.js'; | ||
import { styleMap } from 'lit/directives/style-map.js'; | ||
import { ZERO_WIDTH_SPACE } from '../constant.js'; | ||
import type { BaseArrtiubtes, DeltaInsert } from '../types.js'; | ||
const unitTextStyles = styleMap({ | ||
whiteSpace: 'pre', | ||
}); | ||
@customElement('virgo-unit-text') | ||
export class VirgoUnitText extends LitElement { | ||
@property({ type: Object }) | ||
delta: DeltaInsert<BaseArrtiubtes> = { | ||
insert: ZERO_WIDTH_SPACE, | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}; | ||
@property() | ||
str: string = ZERO_WIDTH_SPACE; | ||
@@ -20,3 +19,5 @@ render() { | ||
// cause the sync problem about the cursor position | ||
return html`<span data-virgo-text="true">${this.delta.insert}</span>`; | ||
return html`<span style=${unitTextStyles} data-virgo-text="true" | ||
>${this.str}</span | ||
>`; | ||
} | ||
@@ -23,0 +24,0 @@ |
@@ -0,1 +1,2 @@ | ||
import { getDefaultPlaygroundURL } from '@blocksuite/global/utils'; | ||
import type { DeltaInsert, VEditor, VRange } from '@blocksuite/virgo'; | ||
@@ -9,3 +10,6 @@ import type { Page } from '@playwright/test'; | ||
export async function enterVirgoPlayground(page: Page) { | ||
const url = new URL('examples/virgo/index.html', 'http://localhost:5173/'); | ||
const url = new URL( | ||
'examples/virgo/index.html', | ||
getDefaultPlaygroundURL(!!process.env.CI) | ||
); | ||
await page.goto(url.toString()); | ||
@@ -12,0 +16,0 @@ } |
import { expect, test } from '@playwright/test'; | ||
import { ZERO_WIDTH_SPACE } from '../constant.js'; | ||
import { | ||
@@ -11,3 +12,2 @@ enterVirgoPlayground, | ||
const ZERO_WIDTH_SPACE = '\u200B'; | ||
test('basic input', async ({ page }) => { | ||
@@ -65,2 +65,4 @@ await enterVirgoPlayground(page); | ||
page.waitForTimeout(100); | ||
expect(await editorA.innerText()).toBe('abc\n' + ZERO_WIDTH_SPACE + '\nbbb'); | ||
@@ -172,3 +174,3 @@ expect(await editorB.innerText()).toBe('abc\n' + ZERO_WIDTH_SPACE + '\nbbb'); | ||
test('basic text style', async ({ page }) => { | ||
test('basic styles', async ({ page }) => { | ||
await enterVirgoPlayground(page); | ||
@@ -185,4 +187,6 @@ await focusVirgoRichText(page); | ||
const editorAInlineCode = page.getByText('inline-code').nth(0); | ||
const editorAReset = page.getByText('reset').nth(0); | ||
const editorAUndo = page.getByText('undo').nth(0); | ||
const editorARedo = page.getByText('redo').nth(0); | ||
expect(await editorA.innerText()).toBe(ZERO_WIDTH_SPACE); | ||
@@ -200,5 +204,2 @@ expect(await editorB.innerText()).toBe(ZERO_WIDTH_SPACE); | ||
insert: 'abcdefg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -214,5 +215,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -222,3 +220,2 @@ { | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
@@ -229,5 +226,2 @@ }, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -241,5 +235,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -249,3 +240,2 @@ { | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
@@ -257,5 +247,2 @@ italic: true, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -269,5 +256,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -277,3 +261,2 @@ { | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
@@ -286,5 +269,2 @@ italic: true, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -298,10 +278,26 @@ ]); | ||
insert: 'ab', | ||
}, | ||
{ | ||
insert: 'cde', | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
italic: true, | ||
underline: true, | ||
strikethrough: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'fg', | ||
}, | ||
]); | ||
editorAInlineCode.click(); | ||
delta = await getDeltaFromVirgoRichText(page); | ||
expect(delta).toEqual([ | ||
{ | ||
insert: 'ab', | ||
}, | ||
{ | ||
insert: 'cde', | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
@@ -311,2 +307,3 @@ italic: true, | ||
strikethrough: true, | ||
inlineCode: true, | ||
}, | ||
@@ -316,6 +313,36 @@ }, | ||
insert: 'fg', | ||
}, | ||
]); | ||
editorAUndo.click({ | ||
clickCount: 5, | ||
}); | ||
delta = await getDeltaFromVirgoRichText(page); | ||
expect(delta).toEqual([ | ||
{ | ||
insert: 'abcdefg', | ||
}, | ||
]); | ||
editorARedo.click({ | ||
clickCount: 5, | ||
}); | ||
delta = await getDeltaFromVirgoRichText(page); | ||
expect(delta).toEqual([ | ||
{ | ||
insert: 'ab', | ||
}, | ||
{ | ||
insert: 'cde', | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
italic: true, | ||
underline: true, | ||
strikethrough: true, | ||
inlineCode: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'fg', | ||
}, | ||
]); | ||
@@ -328,5 +355,2 @@ | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -336,6 +360,6 @@ { | ||
attributes: { | ||
type: 'base', | ||
italic: true, | ||
underline: true, | ||
strikethrough: true, | ||
inlineCode: true, | ||
}, | ||
@@ -345,5 +369,2 @@ }, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -357,5 +378,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -365,5 +383,5 @@ { | ||
attributes: { | ||
type: 'base', | ||
underline: true, | ||
strikethrough: true, | ||
inlineCode: true, | ||
}, | ||
@@ -373,5 +391,2 @@ }, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -385,5 +400,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -393,4 +405,4 @@ { | ||
attributes: { | ||
type: 'base', | ||
strikethrough: true, | ||
inlineCode: true, | ||
}, | ||
@@ -400,9 +412,23 @@ }, | ||
insert: 'fg', | ||
}, | ||
]); | ||
editorAStrikethrough.click(); | ||
delta = await getDeltaFromVirgoRichText(page); | ||
expect(delta).toEqual([ | ||
{ | ||
insert: 'ab', | ||
}, | ||
{ | ||
insert: 'cde', | ||
attributes: { | ||
type: 'base', | ||
inlineCode: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'fg', | ||
}, | ||
]); | ||
editorAStrikethrough.click(); | ||
editorAInlineCode.click(); | ||
delta = await getDeltaFromVirgoRichText(page); | ||
@@ -412,52 +438,207 @@ expect(delta).toEqual([ | ||
insert: 'abcdefg', | ||
}, | ||
]); | ||
}); | ||
test('overlapping styles', async ({ page }) => { | ||
await enterVirgoPlayground(page); | ||
await focusVirgoRichText(page); | ||
const editorA = page.locator('[data-virgo-root="true"]').nth(0); | ||
const editorB = page.locator('[data-virgo-root="true"]').nth(1); | ||
const editorABold = page.getByText('bold').nth(0); | ||
const editorAItalic = page.getByText('italic').nth(0); | ||
const editorAUndo = page.getByText('undo').nth(0); | ||
const editorARedo = page.getByText('redo').nth(0); | ||
expect(await editorA.innerText()).toBe(ZERO_WIDTH_SPACE); | ||
expect(await editorB.innerText()).toBe(ZERO_WIDTH_SPACE); | ||
await type(page, 'abcdefghijk'); | ||
expect(await editorA.innerText()).toBe('abcdefghijk'); | ||
expect(await editorB.innerText()).toBe('abcdefghijk'); | ||
let delta = await getDeltaFromVirgoRichText(page); | ||
expect(delta).toEqual([ | ||
{ | ||
insert: 'abcdefghijk', | ||
}, | ||
]); | ||
await setVirgoRichTextRange(page, { index: 1, length: 3 }); | ||
editorABold.click(); | ||
delta = await getDeltaFromVirgoRichText(page); | ||
expect(delta).toEqual([ | ||
{ | ||
insert: 'a', | ||
}, | ||
{ | ||
insert: 'bcd', | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'efghijk', | ||
}, | ||
]); | ||
editorAReset.click(); | ||
await setVirgoRichTextRange(page, { index: 7, length: 3 }); | ||
editorABold.click(); | ||
delta = await getDeltaFromVirgoRichText(page); | ||
expect(delta).toEqual([ | ||
{ | ||
insert: 'abcdefg', | ||
insert: 'a', | ||
}, | ||
{ | ||
insert: 'bcd', | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'efg', | ||
}, | ||
{ | ||
insert: 'hij', | ||
attributes: { | ||
bold: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'k', | ||
}, | ||
]); | ||
editorAInlineCode.click(); | ||
await setVirgoRichTextRange(page, { index: 3, length: 5 }); | ||
editorAItalic.click(); | ||
delta = await getDeltaFromVirgoRichText(page); | ||
expect(delta).toEqual([ | ||
{ | ||
insert: 'ab', | ||
insert: 'a', | ||
}, | ||
{ | ||
insert: 'bc', | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'cde', | ||
insert: 'd', | ||
attributes: { | ||
type: 'inline-code', | ||
bold: true, | ||
italic: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'fg', | ||
insert: 'efg', | ||
attributes: { | ||
type: 'base', | ||
italic: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'h', | ||
attributes: { | ||
bold: true, | ||
italic: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'ij', | ||
attributes: { | ||
bold: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'k', | ||
}, | ||
]); | ||
editorAInlineCode.click(); | ||
editorAUndo.click({ | ||
clickCount: 3, | ||
}); | ||
delta = await getDeltaFromVirgoRichText(page); | ||
expect(delta).toEqual([ | ||
{ | ||
insert: 'abcdefg', | ||
insert: 'abcdefghijk', | ||
}, | ||
]); | ||
editorARedo.click({ | ||
clickCount: 3, | ||
}); | ||
delta = await getDeltaFromVirgoRichText(page); | ||
expect(delta).toEqual([ | ||
{ | ||
insert: 'a', | ||
}, | ||
{ | ||
insert: 'bc', | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'd', | ||
attributes: { | ||
bold: true, | ||
italic: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'efg', | ||
attributes: { | ||
italic: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'h', | ||
attributes: { | ||
bold: true, | ||
italic: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'ij', | ||
attributes: { | ||
bold: true, | ||
}, | ||
}, | ||
{ | ||
insert: 'k', | ||
}, | ||
]); | ||
}); | ||
test('input continuous spaces', async ({ page }) => { | ||
await enterVirgoPlayground(page); | ||
await focusVirgoRichText(page); | ||
const editorA = page.locator('[data-virgo-root="true"]').nth(0); | ||
const editorB = page.locator('[data-virgo-root="true"]').nth(1); | ||
expect(await editorA.innerText()).toBe(ZERO_WIDTH_SPACE); | ||
expect(await editorB.innerText()).toBe(ZERO_WIDTH_SPACE); | ||
await type(page, 'abc def'); | ||
expect(await editorA.innerText()).toBe('abc def'); | ||
expect(await editorB.innerText()).toBe('abc def'); | ||
await focusVirgoRichText(page); | ||
await page.keyboard.press('ArrowLeft'); | ||
await page.keyboard.press('ArrowLeft'); | ||
await page.keyboard.press('ArrowLeft'); | ||
await page.keyboard.press('ArrowLeft'); | ||
await page.keyboard.press('Enter'); | ||
expect(await editorA.innerText()).toBe('abc \n' + ' def'); | ||
expect(await editorB.innerText()).toBe('abc \n' + ' def'); | ||
}); |
@@ -1,25 +0,3 @@ | ||
import type { BaseText } from './components/base-text.js'; | ||
import type { | ||
InlineCode, | ||
InlineCodeAttributes, | ||
} from './components/optional/inline-code.js'; | ||
import type { BaseText, BaseTextAttributes } from './components/base-text.js'; | ||
export interface BaseArrtiubtes { | ||
type: 'base'; | ||
bold?: true; | ||
italic?: true; | ||
underline?: true; | ||
strikethrough?: true; | ||
} | ||
export interface LineBreakAttributes { | ||
type: 'line-break'; | ||
} | ||
export type BaseTextElement = BaseText | InlineCode; | ||
export type BaseTextAttributes = | ||
| BaseArrtiubtes | ||
| LineBreakAttributes | ||
| InlineCodeAttributes; | ||
export interface CustomTypes { | ||
@@ -34,8 +12,8 @@ [key: string]: unknown; | ||
export type TextElement = ExtendedType<'Element', BaseTextElement>; | ||
export type TextAttributes = ExtendedType<'Attributes', BaseTextAttributes>; | ||
export type TextElement = ExtendedType<'Element', BaseText>; | ||
export type DeltaInsert<A extends TextAttributes = TextAttributes> = { | ||
insert: string; | ||
attributes: A; | ||
attributes?: A; | ||
}; |
import type { DeltaInsert } from '../types.js'; | ||
export function transformDelta(delta: DeltaInsert): (DeltaInsert | '\n')[] { | ||
const result: (DeltaInsert | '\n')[] = []; | ||
let tmpString = delta.insert; | ||
while (tmpString.length > 0) { | ||
const index = tmpString.indexOf('\n'); | ||
if (index === -1) { | ||
result.push({ | ||
insert: tmpString, | ||
attributes: delta.attributes, | ||
}); | ||
break; | ||
} | ||
if (tmpString.slice(0, index).length > 0) { | ||
result.push({ | ||
insert: tmpString.slice(0, index), | ||
attributes: delta.attributes, | ||
}); | ||
} | ||
result.push('\n'); | ||
tmpString = tmpString.slice(index + 1); | ||
} | ||
return result; | ||
} | ||
/** | ||
* convert a delta insert array to chunks, each chunk is a line | ||
*/ | ||
export function deltaInsersToChunks(delta: DeltaInsert[]): DeltaInsert[][] { | ||
export function deltaInsertsToChunks(delta: DeltaInsert[]): DeltaInsert[][] { | ||
if (delta.length === 0) { | ||
@@ -11,15 +39,17 @@ return [[]]; | ||
function* chunksGenerator(arr: DeltaInsert[]) { | ||
const transformedDelta = delta.flatMap(transformDelta); | ||
function* chunksGenerator(arr: (DeltaInsert | '\n')[]) { | ||
let start = 0; | ||
for (let i = 0; i < arr.length; i++) { | ||
if (arr[i].attributes.type === 'line-break') { | ||
if (arr[i] === '\n') { | ||
const chunk = arr.slice(start, i); | ||
start = i + 1; | ||
yield chunk; | ||
yield chunk as DeltaInsert[]; | ||
} else if (i === arr.length - 1) { | ||
yield arr.slice(start); | ||
yield arr.slice(start) as DeltaInsert[]; | ||
} | ||
} | ||
if (arr[arr.length - 1].attributes.type === 'line-break') { | ||
if (arr.at(-1) === '\n') { | ||
yield []; | ||
@@ -29,3 +59,3 @@ } | ||
return [...chunksGenerator(delta)]; | ||
return [...chunksGenerator(transformedDelta)]; | ||
} |
@@ -1,3 +0,3 @@ | ||
import { BaseText } from '../components/base-text.js'; | ||
import type { BaseArrtiubtes, DeltaInsert, TextElement } from '../types.js'; | ||
import { BaseText, baseTextAttributes } from '../components/base-text.js'; | ||
import type { DeltaInsert, TextElement } from '../types.js'; | ||
@@ -8,11 +8,11 @@ /** | ||
export function baseRenderElement(delta: DeltaInsert): TextElement { | ||
switch (delta.attributes.type) { | ||
case 'base': { | ||
const baseText = new BaseText(); | ||
baseText.delta = delta as DeltaInsert<BaseArrtiubtes>; | ||
return baseText; | ||
} | ||
default: | ||
throw new Error(`Unknown text type: ${delta.attributes.type}`); | ||
} | ||
const parseResult = baseTextAttributes.parse(delta.attributes); | ||
const baseText = new BaseText(); | ||
baseText.delta = { | ||
insert: delta.insert, | ||
attributes: parseResult, | ||
}; | ||
return baseText; | ||
} |
@@ -8,3 +8,3 @@ import { assertExists, Signal } from '@blocksuite/global/utils'; | ||
import type { DeltaInsert, TextAttributes, TextElement } from './types.js'; | ||
import { deltaInsersToChunks } from './utils/convert.js'; | ||
import { deltaInsertsToChunks } from './utils/convert.js'; | ||
import { baseRenderElement } from './utils/render.js'; | ||
@@ -60,3 +60,5 @@ | ||
if (onKeyDown) { | ||
this._onKeyDown = onKeyDown; | ||
this._onKeyDown = e => { | ||
onKeyDown(e); | ||
}; | ||
} | ||
@@ -131,12 +133,2 @@ | ||
getBaseElement(node: Node): TextElement | null { | ||
const element = node.parentElement?.closest('[data-virgo-element="true"]'); | ||
if (element) { | ||
return element as TextElement; | ||
} | ||
return null; | ||
} | ||
getNativeSelection(): Selection | null { | ||
@@ -226,17 +218,6 @@ const selectionRoot = findDocumentOrShadowRoot(this); | ||
// TODO add support for formatting | ||
insertText(vRange: VRange, text: string): void { | ||
const currentDelta = this.getDeltaByRangeIndex(vRange.index); | ||
this._transact(() => { | ||
this.yText.delete(vRange.index, vRange.length); | ||
if ( | ||
vRange.index > 0 && | ||
currentDelta && | ||
currentDelta.attributes.type !== 'line-break' | ||
) { | ||
this.yText.insert(vRange.index, text, currentDelta.attributes); | ||
} else { | ||
this.yText.insert(vRange.index, text, { type: 'base' }); | ||
} | ||
this.yText.insert(vRange.index, text); | ||
}); | ||
@@ -248,3 +229,3 @@ } | ||
this.yText.delete(vRange.index, vRange.length); | ||
this.yText.insert(vRange.index, '\n', { type: 'line-break' }); | ||
this.yText.insert(vRange.index, '\n'); | ||
}); | ||
@@ -255,3 +236,3 @@ } | ||
vRange: VRange, | ||
attributes: TextAttributes, | ||
attributes: NonNullable<TextAttributes>, | ||
options: { | ||
@@ -266,6 +247,2 @@ match?: (delta: DeltaInsert, deltaVRange: VRange) => boolean; | ||
for (const [delta, deltaVRange] of deltas) { | ||
if (delta.attributes.type === 'line-break') { | ||
continue; | ||
} | ||
if (match(delta, deltaVRange)) { | ||
@@ -307,3 +284,5 @@ const targetVRange = { | ||
coverDeltas.flatMap(delta => | ||
Object.keys(delta.attributes).map(key => [key, null]) | ||
delta.attributes | ||
? Object.keys(delta.attributes).map(key => [key, null]) | ||
: [] | ||
) | ||
@@ -315,3 +294,2 @@ ); | ||
...unset, | ||
type: 'base', | ||
}); | ||
@@ -325,3 +303,3 @@ }); | ||
syncVRange(): void { | ||
setTimeout(() => { | ||
requestAnimationFrame(() => { | ||
if (this._vRange) { | ||
@@ -648,12 +626,7 @@ const newRange = this.toDomRange(this._vRange); | ||
const deltas = (this.yText.toDelta() as DeltaInsert[]).flatMap(d => { | ||
if (d.attributes.type === 'line-break') { | ||
return d.insert | ||
.split('') | ||
.map(c => ({ insert: c, attributes: d.attributes })); | ||
} | ||
return d; | ||
}) as DeltaInsert[]; | ||
renderDeltas(deltas, this._rootElement, this._renderElement); | ||
renderDeltas( | ||
this.yText.toDelta() as DeltaInsert[], | ||
this._rootElement, | ||
this._renderElement | ||
); | ||
}; | ||
@@ -845,3 +818,3 @@ | ||
) { | ||
const chunks = deltaInsersToChunks(deltas); | ||
const chunks = deltaInsertsToChunks(deltas); | ||
@@ -848,0 +821,0 @@ // every chunk is a line |
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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
237201
77
3382
4
73
2
+ Addedzod@^3.20.6
+ Added@blocksuite/global@0.4.0-alpha.4(transitive)
+ Addedyjs@13.6.20(transitive)
- Removed@blocksuite/global@0.4.0-alpha.3(transitive)
- Removedyjs@13.6.21(transitive)