@blocksuite/virgo
Advanced tools
Comparing version 0.4.0-20230208200347-b55e9fe to 0.4.0-20230209191848-0a912e3
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; |
@@ -13,8 +13,3 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { | ||
super(...arguments); | ||
this.delta = { | ||
insert: ZERO_WIDTH_SPACE, | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}; | ||
this.str = ZERO_WIDTH_SPACE; | ||
} | ||
@@ -24,3 +19,3 @@ render() { | ||
// cause the sync problem about the cursor position | ||
return html `<span data-virgo-text="true">${this.delta.insert}</span>`; | ||
return html `<span data-virgo-text="true">${this.str}</span>`; | ||
} | ||
@@ -32,4 +27,4 @@ createRenderRoot() { | ||
__decorate([ | ||
property({ type: Object }) | ||
], VirgoUnitText.prototype, "delta", void 0); | ||
property() | ||
], VirgoUnitText.prototype, "str", void 0); | ||
VirgoUnitText = __decorate([ | ||
@@ -36,0 +31,0 @@ customElement('virgo-unit-text') |
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'); | ||
@@ -123,3 +124,2 @@ expect(await editorB.innerText()).toBe('abc\n' + ZERO_WIDTH_SPACE + '\nbbb'); | ||
const editorAInlineCode = page.getByText('inline-code').nth(0); | ||
const editorAReset = page.getByText('reset').nth(0); | ||
expect(await editorA.innerText()).toBe(ZERO_WIDTH_SPACE); | ||
@@ -134,5 +134,2 @@ expect(await editorB.innerText()).toBe(ZERO_WIDTH_SPACE); | ||
insert: 'abcdefg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -146,5 +143,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -154,3 +148,2 @@ { | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
@@ -161,5 +154,2 @@ }, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -172,5 +162,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -180,3 +167,2 @@ { | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
@@ -188,5 +174,2 @@ italic: true, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -199,5 +182,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -207,3 +187,2 @@ { | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
@@ -216,5 +195,2 @@ italic: true, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -227,5 +203,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -235,3 +208,2 @@ { | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
@@ -245,5 +217,2 @@ italic: true, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -256,5 +225,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -264,3 +230,2 @@ { | ||
attributes: { | ||
type: 'base', | ||
italic: true, | ||
@@ -273,5 +238,2 @@ underline: true, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -284,5 +246,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -292,3 +251,2 @@ { | ||
attributes: { | ||
type: 'base', | ||
underline: true, | ||
@@ -300,5 +258,2 @@ strikethrough: true, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -311,5 +266,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -319,3 +271,2 @@ { | ||
attributes: { | ||
type: 'base', | ||
strikethrough: true, | ||
@@ -326,5 +277,2 @@ }, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -337,17 +285,4 @@ ]); | ||
insert: 'abcdefg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
]); | ||
editorAReset.click(); | ||
delta = await getDeltaFromVirgoRichText(page); | ||
expect(delta).toEqual([ | ||
{ | ||
insert: 'abcdefg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
]); | ||
editorAInlineCode.click(); | ||
@@ -358,5 +293,2 @@ delta = await getDeltaFromVirgoRichText(page); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -366,3 +298,3 @@ { | ||
attributes: { | ||
type: 'inline-code', | ||
inlineCode: true, | ||
}, | ||
@@ -372,5 +304,2 @@ }, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -383,5 +312,2 @@ ]); | ||
insert: 'abcdefg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -388,0 +314,0 @@ ]); |
@@ -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 |
@@ -0,1 +1,25 @@ | ||
// TODO unit test | ||
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; | ||
} | ||
/** | ||
@@ -8,6 +32,7 @@ * convert a delta insert array to chunks, each chunk is a line | ||
} | ||
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); | ||
@@ -21,8 +46,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'; |
@@ -20,11 +20,3 @@ import { assertExists, Signal } from '@blocksuite/global/utils'; | ||
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); | ||
}; | ||
@@ -77,3 +69,5 @@ this._onSelectionChange = () => { | ||
if (onKeyDown) { | ||
this._onKeyDown = onKeyDown; | ||
this._onKeyDown = e => { | ||
onKeyDown(e); | ||
}; | ||
} | ||
@@ -124,9 +118,2 @@ this.signals = { | ||
} | ||
getBaseElement(node) { | ||
const element = node.parentElement?.closest('[data-virgo-element="true"]'); | ||
if (element) { | ||
return element; | ||
} | ||
return null; | ||
} | ||
getNativeSelection() { | ||
@@ -197,15 +184,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); | ||
}); | ||
@@ -216,3 +194,3 @@ } | ||
this.yText.delete(vRange.index, vRange.length); | ||
this.yText.insert(vRange.index, '\n', { type: 'line-break' }); | ||
this.yText.insert(vRange.index, '\n'); | ||
}); | ||
@@ -224,5 +202,2 @@ } | ||
for (const [delta, deltaVRange] of deltas) { | ||
if (delta.attributes.type === 'line-break') { | ||
continue; | ||
} | ||
if (match(delta, deltaVRange)) { | ||
@@ -250,7 +225,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', | ||
}); | ||
@@ -257,0 +233,0 @@ }); |
{ | ||
"name": "@blocksuite/virgo", | ||
"version": "0.4.0-20230208200347-b55e9fe", | ||
"version": "0.4.0-20230209191848-0a912e3", | ||
"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.45" | ||
}, | ||
"peerDependencies": { | ||
"yjs": "^13", | ||
"lit": "^2" | ||
"lit": "^2", | ||
"yjs": "^13" | ||
}, | ||
@@ -27,8 +27,10 @@ "exports": { | ||
"dependencies": { | ||
"@blocksuite/global": "0.4.0-20230208200347-b55e9fe" | ||
"@blocksuite/global": "0.4.0-20230209191848-0a912e3", | ||
"zod": "^3.20.2" | ||
}, | ||
"scripts": { | ||
"build": "tsc" | ||
"build": "tsc", | ||
"test": "playwright test" | ||
}, | ||
"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'; |
@@ -5,13 +5,7 @@ import { html, LitElement } from 'lit'; | ||
import { ZERO_WIDTH_SPACE } from '../constant.js'; | ||
import type { BaseArrtiubtes, DeltaInsert } from '../types.js'; | ||
@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; | ||
@@ -21,3 +15,3 @@ render() { | ||
// cause the sync problem about the cursor position | ||
return html`<span data-virgo-text="true">${this.delta.insert}</span>`; | ||
return html`<span data-virgo-text="true">${this.str}</span>`; | ||
} | ||
@@ -24,0 +18,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'); | ||
@@ -184,3 +186,2 @@ expect(await editorB.innerText()).toBe('abc\n' + ZERO_WIDTH_SPACE + '\nbbb'); | ||
const editorAInlineCode = page.getByText('inline-code').nth(0); | ||
const editorAReset = page.getByText('reset').nth(0); | ||
@@ -199,5 +200,2 @@ expect(await editorA.innerText()).toBe(ZERO_WIDTH_SPACE); | ||
insert: 'abcdefg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -213,5 +211,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -221,3 +216,2 @@ { | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
@@ -228,5 +222,2 @@ }, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -240,5 +231,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -248,3 +236,2 @@ { | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
@@ -256,5 +243,2 @@ italic: true, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -268,5 +252,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -276,3 +257,2 @@ { | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
@@ -285,5 +265,2 @@ italic: true, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -297,5 +274,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -305,3 +279,2 @@ { | ||
attributes: { | ||
type: 'base', | ||
bold: true, | ||
@@ -315,5 +288,2 @@ italic: true, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -327,5 +297,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -335,3 +302,2 @@ { | ||
attributes: { | ||
type: 'base', | ||
italic: true, | ||
@@ -344,5 +310,2 @@ underline: true, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -356,5 +319,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -364,3 +324,2 @@ { | ||
attributes: { | ||
type: 'base', | ||
underline: true, | ||
@@ -372,5 +331,2 @@ strikethrough: true, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -384,5 +340,2 @@ ]); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -392,3 +345,2 @@ { | ||
attributes: { | ||
type: 'base', | ||
strikethrough: true, | ||
@@ -399,5 +351,2 @@ }, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -411,19 +360,5 @@ ]); | ||
insert: 'abcdefg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
]); | ||
editorAReset.click(); | ||
delta = await getDeltaFromVirgoRichText(page); | ||
expect(delta).toEqual([ | ||
{ | ||
insert: 'abcdefg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
]); | ||
editorAInlineCode.click(); | ||
@@ -434,5 +369,2 @@ delta = await getDeltaFromVirgoRichText(page); | ||
insert: 'ab', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -442,3 +374,3 @@ { | ||
attributes: { | ||
type: 'inline-code', | ||
inlineCode: true, | ||
}, | ||
@@ -448,5 +380,2 @@ }, | ||
insert: 'fg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
@@ -460,7 +389,4 @@ ]); | ||
insert: 'abcdefg', | ||
attributes: { | ||
type: 'base', | ||
}, | ||
}, | ||
]); | ||
}); |
@@ -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'; | ||
// TODO unit test | ||
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; | ||
} | ||
/** | ||
@@ -11,15 +40,17 @@ * convert a delta insert array to chunks, each chunk is a line | ||
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 +60,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; | ||
} |
@@ -59,3 +59,5 @@ import { assertExists, Signal } from '@blocksuite/global/utils'; | ||
if (onKeyDown) { | ||
this._onKeyDown = onKeyDown; | ||
this._onKeyDown = e => { | ||
onKeyDown(e); | ||
}; | ||
} | ||
@@ -130,12 +132,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 { | ||
@@ -225,17 +217,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); | ||
}); | ||
@@ -247,3 +228,3 @@ } | ||
this.yText.delete(vRange.index, vRange.length); | ||
this.yText.insert(vRange.index, '\n', { type: 'line-break' }); | ||
this.yText.insert(vRange.index, '\n'); | ||
}); | ||
@@ -254,3 +235,3 @@ } | ||
vRange: VRange, | ||
attributes: TextAttributes, | ||
attributes: NonNullable<TextAttributes>, | ||
options: { | ||
@@ -265,6 +246,2 @@ match?: (delta: DeltaInsert, deltaVRange: VRange) => boolean; | ||
for (const [delta, deltaVRange] of deltas) { | ||
if (delta.attributes.type === 'line-break') { | ||
continue; | ||
} | ||
if (match(delta, deltaVRange)) { | ||
@@ -306,3 +283,5 @@ const targetVRange = { | ||
coverDeltas.flatMap(delta => | ||
Object.keys(delta.attributes).map(key => [key, null]) | ||
delta.attributes | ||
? Object.keys(delta.attributes).map(key => [key, null]) | ||
: [] | ||
) | ||
@@ -314,3 +293,2 @@ ); | ||
...unset, | ||
type: 'base', | ||
}); | ||
@@ -646,12 +624,7 @@ }); | ||
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 | ||
); | ||
}; | ||
@@ -658,0 +631,0 @@ |
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
202113
4
71
2635
73
+ Addedzod@^3.20.2
+ Added@blocksuite/global@0.4.0-20230209191848-0a912e3(transitive)
- Removed@blocksuite/global@0.4.0-20230208200347-b55e9fe(transitive)