@blocksuite/virgo
Advanced tools
Comparing version 0.5.0-20230304192152-02cfe2b to 0.5.0-20230305195734-5878b62
@@ -0,4 +1,4 @@ | ||
export * from './virgo-element.js'; | ||
export * from './virgo-line.js'; | ||
export * from './virgo-text.js'; | ||
export * from './virgo-unit-text.js'; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -0,4 +1,4 @@ | ||
export * from './virgo-element.js'; | ||
export * from './virgo-line.js'; | ||
export * from './virgo-text.js'; | ||
export * from './virgo-unit-text.js'; | ||
//# sourceMappingURL=index.js.map |
import { LitElement } from 'lit'; | ||
import type { BaseTextAttributes } from '../utils/index.js'; | ||
import type { VirgoText } from './virgo-text.js'; | ||
import type { VirgoElement } from './virgo-element.js'; | ||
export declare class VirgoLine<TextAttributes extends BaseTextAttributes = BaseTextAttributes> extends LitElement { | ||
elements: VirgoText<TextAttributes>[]; | ||
elements: VirgoElement<TextAttributes>[]; | ||
get textLength(): number; | ||
@@ -13,5 +13,5 @@ get textContent(): string; | ||
interface HTMLElementTagNameMap { | ||
'virgo-line': VirgoLine; | ||
'v-line': VirgoLine; | ||
} | ||
} | ||
//# sourceMappingURL=virgo-line.d.ts.map |
@@ -22,3 +22,3 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { | ||
return html `<style> | ||
virgo-line { | ||
v-line { | ||
display: block; | ||
@@ -37,5 +37,5 @@ } | ||
VirgoLine = __decorate([ | ||
customElement('virgo-line') | ||
customElement('v-line') | ||
], VirgoLine); | ||
export { VirgoLine }; | ||
//# sourceMappingURL=virgo-line.js.map |
@@ -1,9 +0,5 @@ | ||
import { LitElement, TemplateResult } from 'lit'; | ||
import type { DeltaInsert } from '../types.js'; | ||
import type { BaseTextAttributes } from '../utils/base-attributes.js'; | ||
import { VirgoUnitText } from './virgo-unit-text.js'; | ||
export declare class VirgoText<T extends BaseTextAttributes = BaseTextAttributes> extends LitElement { | ||
delta: DeltaInsert<T>; | ||
attributesRenderer: (unitText: VirgoUnitText, attributes?: T) => TemplateResult<1>; | ||
render(): TemplateResult<1>; | ||
import { LitElement } from 'lit'; | ||
export declare class VText extends LitElement { | ||
str: string; | ||
render(): import("lit-html").TemplateResult<1>; | ||
createRenderRoot(): this; | ||
@@ -13,5 +9,5 @@ } | ||
interface HTMLElementTagNameMap { | ||
'v-text': VirgoText; | ||
'v-text': VText; | ||
} | ||
} | ||
//# sourceMappingURL=virgo-text.d.ts.map |
@@ -9,20 +9,17 @@ 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'; | ||
import { getDefaultAttributeRenderer } from '../utils/attributes-renderer.js'; | ||
import { VirgoUnitText } from './virgo-unit-text.js'; | ||
let VirgoText = class VirgoText extends LitElement { | ||
const unitTextStyles = styleMap({ | ||
whiteSpace: 'break-spaces', | ||
}); | ||
let VText = class VText extends LitElement { | ||
constructor() { | ||
super(...arguments); | ||
this.delta = { | ||
insert: ZERO_WIDTH_SPACE, | ||
}; | ||
this.attributesRenderer = getDefaultAttributeRenderer(); | ||
this.str = ZERO_WIDTH_SPACE; | ||
} | ||
render() { | ||
const unitText = new VirgoUnitText(); | ||
unitText.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(unitText, this.delta.attributes)}</span | ||
// cause the sync problem about the cursor position | ||
return html `<span style=${unitTextStyles} data-virgo-text="true" | ||
>${this.str}</span | ||
>`; | ||
@@ -35,11 +32,8 @@ } | ||
__decorate([ | ||
property({ type: Object }) | ||
], VirgoText.prototype, "delta", void 0); | ||
__decorate([ | ||
property({ type: Function, attribute: false }) | ||
], VirgoText.prototype, "attributesRenderer", void 0); | ||
VirgoText = __decorate([ | ||
property() | ||
], VText.prototype, "str", void 0); | ||
VText = __decorate([ | ||
customElement('v-text') | ||
], VirgoText); | ||
export { VirgoText }; | ||
], VText); | ||
export { VText }; | ||
//# sourceMappingURL=virgo-text.js.map |
@@ -250,3 +250,83 @@ import { expect, test } from 'vitest'; | ||
]); | ||
expect(virgo.getDeltasByVRange({ | ||
index: 4, | ||
length: 0, | ||
})).toEqual([ | ||
[ | ||
{ | ||
insert: 'bbb', | ||
attributes: { | ||
italic: true, | ||
}, | ||
}, | ||
{ | ||
index: 3, | ||
length: 3, | ||
}, | ||
], | ||
]); | ||
expect(virgo.getDeltasByVRange({ | ||
index: 4, | ||
length: 1, | ||
})).toEqual([ | ||
[ | ||
{ | ||
insert: 'bbb', | ||
attributes: { | ||
italic: true, | ||
}, | ||
}, | ||
{ | ||
index: 3, | ||
length: 3, | ||
}, | ||
], | ||
]); | ||
expect(virgo.getDeltasByVRange({ | ||
index: 4, | ||
length: 2, | ||
})).toEqual([ | ||
[ | ||
{ | ||
insert: 'bbb', | ||
attributes: { | ||
italic: true, | ||
}, | ||
}, | ||
{ | ||
index: 3, | ||
length: 3, | ||
}, | ||
], | ||
]); | ||
expect(virgo.getDeltasByVRange({ | ||
index: 4, | ||
length: 4, | ||
})).toEqual([ | ||
[ | ||
{ | ||
insert: 'bbb', | ||
attributes: { | ||
italic: true, | ||
}, | ||
}, | ||
{ | ||
index: 3, | ||
length: 3, | ||
}, | ||
], | ||
[ | ||
{ | ||
insert: 'ccc', | ||
attributes: { | ||
underline: true, | ||
}, | ||
}, | ||
{ | ||
index: 6, | ||
length: 3, | ||
}, | ||
], | ||
]); | ||
}); | ||
//# sourceMappingURL=editor.unit.spec.js.map |
import type { TemplateResult } from 'lit'; | ||
import type { VirgoUnitText } from './components/index.js'; | ||
import type { VText } from './components/index.js'; | ||
import type { BaseTextAttributes } from './utils/index.js'; | ||
@@ -14,4 +14,4 @@ export interface CustomTypes { | ||
}; | ||
export type AttributesRenderer<TextAttributes extends BaseTextAttributes = BaseTextAttributes> = (unitText: VirgoUnitText, attributes?: TextAttributes) => TemplateResult<1>; | ||
export type AttributesRenderer<TextAttributes extends BaseTextAttributes = BaseTextAttributes> = (vText: VText, attributes?: TextAttributes) => TemplateResult<1>; | ||
export {}; | ||
//# sourceMappingURL=types.d.ts.map |
@@ -1,8 +0,5 @@ | ||
import { VirgoText } from '../components/virgo-text.js'; | ||
import { VirgoElement } from '../components/virgo-element.js'; | ||
import type { AttributesRenderer, DeltaInsert } from '../types.js'; | ||
import type { BaseTextAttributes } from './base-attributes.js'; | ||
/** | ||
* a default render function for text element | ||
*/ | ||
export declare function renderElement<TextAttributes extends BaseTextAttributes>(delta: DeltaInsert<TextAttributes>, parseAttributes: (textAttributes?: TextAttributes) => TextAttributes | undefined, attributesRenderer: AttributesRenderer<TextAttributes>): VirgoText<TextAttributes>; | ||
export declare function renderElement<TextAttributes extends BaseTextAttributes>(delta: DeltaInsert<TextAttributes>, parseAttributes: (textAttributes?: TextAttributes) => TextAttributes | undefined, attributesRenderer: AttributesRenderer<TextAttributes>): VirgoElement<TextAttributes>; | ||
//# sourceMappingURL=render.d.ts.map |
@@ -1,14 +0,11 @@ | ||
import { VirgoText } from '../components/virgo-text.js'; | ||
/** | ||
* a default render function for text element | ||
*/ | ||
import { VirgoElement } from '../components/virgo-element.js'; | ||
export function renderElement(delta, parseAttributes, attributesRenderer) { | ||
const baseText = new VirgoText(); | ||
baseText.delta = { | ||
const vElement = new VirgoElement(); | ||
vElement.delta = { | ||
insert: delta.insert, | ||
attributes: parseAttributes(delta.attributes), | ||
}; | ||
baseText.attributesRenderer = attributesRenderer; | ||
return baseText; | ||
vElement.attributesRenderer = attributesRenderer; | ||
return vElement; | ||
} | ||
//# sourceMappingURL=render.js.map |
@@ -22,2 +22,3 @@ import { Slot } from '@blocksuite/global/utils'; | ||
static textPointToDomPoint(text: Text, offset: number, rootElement: HTMLElement): DomPoint | null; | ||
static getTextNodesFromElement(element: Element): Text[]; | ||
private _rootElement; | ||
@@ -33,2 +34,3 @@ private _mountAbort; | ||
private _handlers; | ||
private _defaultHandlers; | ||
private _parseSchema; | ||
@@ -59,3 +61,3 @@ private _renderDeltas; | ||
deleteText(vRange: VRange): void; | ||
insertText(vRange: VRange, text: string): void; | ||
insertText(vRange: VRange, text: string, attributes?: TextAttributes): void; | ||
insertLineBreak(vRange: VRange): void; | ||
@@ -62,0 +64,0 @@ formatText(vRange: VRange, attributes: Partial<Record<keyof TextAttributes, TextAttributes[keyof TextAttributes] | null>>, options?: { |
import { assertExists, Slot } from '@blocksuite/global/utils'; | ||
import { VirgoElement } from './components/virgo-element.js'; | ||
import { VirgoLine } from './components/virgo-line.js'; | ||
import { VirgoText } from './components/virgo-text.js'; | ||
import { ZERO_WIDTH_SPACE } from './constant.js'; | ||
@@ -18,15 +18,68 @@ import { getDefaultAttributeRenderer } from './utils/attributes-renderer.js'; | ||
else if (isVElement(node)) { | ||
const textNode = getTextNodeFromElement(node); | ||
if (textNode) { | ||
text = textNode; | ||
textOffset = offset; | ||
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)) { | ||
const firstTextElement = node.querySelector('v-text'); | ||
if (firstTextElement) { | ||
const textNode = getTextNodeFromElement(firstTextElement); | ||
if (textNode) { | ||
text = textNode; | ||
textOffset = 0; | ||
else if (isVLine(node) || isVRoot(node)) { | ||
const texts = VEditor.getTextNodesFromElement(node); | ||
if (texts.length > 0) { | ||
text = texts[0]; | ||
textOffset = 0; | ||
} | ||
} | ||
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[0]; | ||
textOffset = 0; | ||
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 = 0; | ||
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[0]; | ||
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[0]; | ||
textOffset = calculateTextLength(text); | ||
break; | ||
} | ||
} | ||
} | ||
@@ -47,10 +100,7 @@ } | ||
} | ||
const textNodes = Array.from(rootElement.querySelectorAll('[data-virgo-text="true"]')).map(textElement => getTextNodeFromElement(textElement)); | ||
const goalIndex = textNodes.indexOf(text); | ||
const texts = VEditor.getTextNodesFromElement(rootElement); | ||
const goalIndex = texts.indexOf(text); | ||
let index = 0; | ||
for (const textNode of textNodes.slice(0, goalIndex)) { | ||
if (!textNode) { | ||
return null; | ||
} | ||
index += calculateTextLength(textNode); | ||
for (const text of texts.slice(0, goalIndex)) { | ||
index += calculateTextLength(text); | ||
} | ||
@@ -60,13 +110,24 @@ if (text.wholeText !== ZERO_WIDTH_SPACE) { | ||
} | ||
const textElement = text.parentElement; | ||
if (!textElement) { | ||
throw new Error('text element not found'); | ||
const textParentElement = text.parentElement; | ||
if (!textParentElement) { | ||
throw new Error('text element parent not found'); | ||
} | ||
const lineElement = textElement.closest('virgo-line'); | ||
const lineElement = textParentElement.closest('v-line'); | ||
if (!lineElement) { | ||
throw new Error('line element not found'); | ||
} | ||
const lineIndex = Array.from(rootElement.querySelectorAll('virgo-line')).indexOf(lineElement); | ||
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; | ||
} | ||
get yText() { | ||
@@ -89,2 +150,20 @@ return this._yText; | ||
this._handlers = {}; | ||
this._defaultHandlers = { | ||
paste: (event) => { | ||
const data = event.clipboardData?.getData('text/plain'); | ||
if (data) { | ||
const vRange = this._vRange; | ||
if (vRange) { | ||
this.insertText(vRange, data); | ||
this.slots.updateVRange.emit([ | ||
{ | ||
index: vRange.index + data.length, | ||
length: 0, | ||
}, | ||
'input', | ||
]); | ||
} | ||
} | ||
}, | ||
}; | ||
this._parseSchema = (textAttributes) => { | ||
@@ -101,3 +180,3 @@ return this._attributesSchema.optional().parse(textAttributes); | ||
if (chunk.length === 0) { | ||
virgoLine.elements.push(new VirgoText()); | ||
virgoLine.elements.push(new VirgoElement()); | ||
} | ||
@@ -180,20 +259,3 @@ else { | ||
} | ||
bindHandlers(handlers = { | ||
paste: (event) => { | ||
const data = event.clipboardData?.getData('text/plain'); | ||
if (data) { | ||
const vRange = this._vRange; | ||
if (vRange) { | ||
this.insertText(vRange, data); | ||
this.slots.updateVRange.emit([ | ||
{ | ||
index: vRange.index + data.length, | ||
length: 0, | ||
}, | ||
'input', | ||
]); | ||
} | ||
} | ||
}, | ||
}) { | ||
bindHandlers(handlers = this._defaultHandlers) { | ||
this._handlers = handlers; | ||
@@ -252,2 +314,3 @@ if (this._handlerAbort) { | ||
} | ||
this._handlers = this._defaultHandlers; | ||
this._rootElement?.replaceChildren(); | ||
@@ -279,16 +342,16 @@ this._rootElement = null; | ||
assertExists(this._rootElement); | ||
const textElements = Array.from(this._rootElement.querySelectorAll('[data-virgo-text="true"]')); | ||
const vLines = Array.from(this._rootElement.querySelectorAll('v-line')); | ||
let index = 0; | ||
for (const textElement of textElements) { | ||
if (!textElement.textContent) { | ||
throw new Error('text element should have textContent'); | ||
} | ||
if (index + textElement.textContent.length >= rangeIndex) { | ||
const text = getTextNodeFromElement(textElement); | ||
if (!text) { | ||
throw new Error('text node should have text content'); | ||
for (const vLine of vLines) { | ||
const texts = VEditor.getTextNodesFromElement(vLine); | ||
for (const text of texts) { | ||
if (!text.textContent) { | ||
throw new Error('text element should have textContent'); | ||
} | ||
return [text, rangeIndex - index]; | ||
if (index + text.textContent.length >= rangeIndex) { | ||
return [text, rangeIndex - index]; | ||
} | ||
index += text.textContent.length; | ||
} | ||
index += textElement.textContent.length; | ||
index += 1; | ||
} | ||
@@ -300,3 +363,3 @@ throw new Error('failed to find leaf'); | ||
assertExists(this._rootElement); | ||
const lineElements = Array.from(this._rootElement.querySelectorAll('virgo-line')); | ||
const lineElements = Array.from(this._rootElement.querySelectorAll('v-line')); | ||
let index = 0; | ||
@@ -354,2 +417,3 @@ for (const lineElement of lineElements) { | ||
setReadOnly(isReadOnly) { | ||
this.rootElement.contentEditable = isReadOnly ? 'false' : 'true'; | ||
this._isReadOnly = isReadOnly; | ||
@@ -372,6 +436,6 @@ } | ||
} | ||
insertText(vRange, text) { | ||
insertText(vRange, text, attributes = {}) { | ||
this._transact(() => { | ||
this.yText.delete(vRange.index, vRange.length); | ||
this.yText.insert(vRange.index, text, {}); | ||
this.yText.insert(vRange.index, text, attributes); | ||
}); | ||
@@ -443,3 +507,3 @@ } | ||
assertExists(this._rootElement); | ||
const lineElements = Array.from(this._rootElement.querySelectorAll('virgo-line')); | ||
const lineElements = Array.from(this._rootElement.querySelectorAll('v-line')); | ||
// calculate anchorNode and focusNode | ||
@@ -455,20 +519,16 @@ let anchorText = null; | ||
} | ||
const textElements = Array.from(lineElements[i].querySelectorAll('[data-virgo-text="true"]')); | ||
for (let j = 0; j < textElements.length; j++) { | ||
if (anchorText && focusText) { | ||
break; | ||
} | ||
const textNode = getTextNodeFromElement(textElements[j]); | ||
if (!textNode) { | ||
return null; | ||
} | ||
const textLength = calculateTextLength(textNode); | ||
const texts = VEditor.getTextNodesFromElement(lineElements[i]); | ||
for (const text of texts) { | ||
const textLength = calculateTextLength(text); | ||
if (!anchorText && index + textLength >= vRange.index) { | ||
anchorText = textNode; | ||
anchorText = text; | ||
anchorOffset = vRange.index - index; | ||
} | ||
if (!focusText && index + textLength >= vRange.index + vRange.length) { | ||
focusText = textNode; | ||
focusText = text; | ||
focusOffset = vRange.index + vRange.length - index; | ||
} | ||
if (anchorText && focusText) { | ||
break; | ||
} | ||
index += textLength; | ||
@@ -596,2 +656,9 @@ } | ||
event.preventDefault(); | ||
let ifSkip = false; | ||
if (this._handlers.virgoInput) { | ||
ifSkip = this._handlers.virgoInput(event); | ||
} | ||
if (ifSkip) { | ||
return; | ||
} | ||
if (this._isReadOnly) { | ||
@@ -702,19 +769,2 @@ return; | ||
} | ||
function getTextNodeFromElement(element) { | ||
let spanElement = element; | ||
if (element instanceof HTMLElement && element.dataset.virgoText === 'true') { | ||
spanElement = element; | ||
} | ||
else { | ||
spanElement = element.querySelector('[data-virgo-text="true"]'); | ||
} | ||
if (!spanElement) { | ||
return null; | ||
} | ||
const textNode = Array.from(spanElement.childNodes).find((node) => node instanceof Text); | ||
if (textNode) { | ||
return textNode; | ||
} | ||
return null; | ||
} | ||
function isVText(text) { | ||
@@ -728,4 +778,7 @@ return (text instanceof Text && | ||
function isVLine(element) { | ||
return (element instanceof HTMLElement && element.parentElement instanceof VirgoLine); | ||
return element instanceof HTMLElement && element instanceof VirgoLine; | ||
} | ||
function isVRoot(element) { | ||
return element instanceof HTMLElement && element.dataset.virgoRoot === 'true'; | ||
} | ||
function findDocumentOrShadowRoot(editor) { | ||
@@ -732,0 +785,0 @@ const el = editor.rootElement; |
{ | ||
"name": "@blocksuite/virgo", | ||
"version": "0.5.0-20230304192152-02cfe2b", | ||
"version": "0.5.0-20230305195734-5878b62", | ||
"description": "A micro editor.", | ||
@@ -26,3 +26,3 @@ "main": "dist/index.js", | ||
"dependencies": { | ||
"@blocksuite/global": "0.5.0-20230304192152-02cfe2b", | ||
"@blocksuite/global": "0.5.0-20230305195734-5878b62", | ||
"zod": "^3.20.6" | ||
@@ -29,0 +29,0 @@ }, |
@@ -0,3 +1,3 @@ | ||
export * from './virgo-element.js'; | ||
export * from './virgo-line.js'; | ||
export * from './virgo-text.js'; | ||
export * from './virgo-unit-text.js'; |
@@ -5,5 +5,5 @@ import { html, LitElement } from 'lit'; | ||
import type { BaseTextAttributes } from '../utils/index.js'; | ||
import type { VirgoText } from './virgo-text.js'; | ||
import type { VirgoElement } from './virgo-element.js'; | ||
@customElement('virgo-line') | ||
@customElement('v-line') | ||
export class VirgoLine< | ||
@@ -13,3 +13,3 @@ TextAttributes extends BaseTextAttributes = BaseTextAttributes | ||
@property({ attribute: false }) | ||
elements: VirgoText<TextAttributes>[] = []; | ||
elements: VirgoElement<TextAttributes>[] = []; | ||
@@ -26,3 +26,3 @@ get textLength() { | ||
return html`<style> | ||
virgo-line { | ||
v-line { | ||
display: block; | ||
@@ -41,4 +41,4 @@ } | ||
interface HTMLElementTagNameMap { | ||
'virgo-line': VirgoLine; | ||
'v-line': VirgoLine; | ||
} | ||
} |
@@ -1,33 +0,21 @@ | ||
import { html, LitElement, TemplateResult } from 'lit'; | ||
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 { DeltaInsert } from '../types.js'; | ||
import { getDefaultAttributeRenderer } from '../utils/attributes-renderer.js'; | ||
import type { BaseTextAttributes } from '../utils/base-attributes.js'; | ||
import { VirgoUnitText } from './virgo-unit-text.js'; | ||
const unitTextStyles = styleMap({ | ||
whiteSpace: 'break-spaces', | ||
}); | ||
@customElement('v-text') | ||
export class VirgoText< | ||
T extends BaseTextAttributes = BaseTextAttributes | ||
> extends LitElement { | ||
@property({ type: Object }) | ||
delta: DeltaInsert<T> = { | ||
insert: ZERO_WIDTH_SPACE, | ||
}; | ||
export class VText extends LitElement { | ||
@property() | ||
str: string = ZERO_WIDTH_SPACE; | ||
@property({ type: Function, attribute: false }) | ||
attributesRenderer: ( | ||
unitText: VirgoUnitText, | ||
attributes?: T | ||
) => TemplateResult<1> = getDefaultAttributeRenderer<T>(); | ||
render() { | ||
const unitText = new VirgoUnitText(); | ||
unitText.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(unitText, this.delta.attributes)}</span | ||
// cause the sync problem about the cursor position | ||
return html`<span style=${unitTextStyles} data-virgo-text="true" | ||
>${this.str}</span | ||
>`; | ||
@@ -43,4 +31,4 @@ } | ||
interface HTMLElementTagNameMap { | ||
'v-text': VirgoText; | ||
'v-text': VText; | ||
} | ||
} |
@@ -278,2 +278,94 @@ import { expect, test } from 'vitest'; | ||
]); | ||
expect( | ||
virgo.getDeltasByVRange({ | ||
index: 4, | ||
length: 0, | ||
}) | ||
).toEqual([ | ||
[ | ||
{ | ||
insert: 'bbb', | ||
attributes: { | ||
italic: true, | ||
}, | ||
}, | ||
{ | ||
index: 3, | ||
length: 3, | ||
}, | ||
], | ||
]); | ||
expect( | ||
virgo.getDeltasByVRange({ | ||
index: 4, | ||
length: 1, | ||
}) | ||
).toEqual([ | ||
[ | ||
{ | ||
insert: 'bbb', | ||
attributes: { | ||
italic: true, | ||
}, | ||
}, | ||
{ | ||
index: 3, | ||
length: 3, | ||
}, | ||
], | ||
]); | ||
expect( | ||
virgo.getDeltasByVRange({ | ||
index: 4, | ||
length: 2, | ||
}) | ||
).toEqual([ | ||
[ | ||
{ | ||
insert: 'bbb', | ||
attributes: { | ||
italic: true, | ||
}, | ||
}, | ||
{ | ||
index: 3, | ||
length: 3, | ||
}, | ||
], | ||
]); | ||
expect( | ||
virgo.getDeltasByVRange({ | ||
index: 4, | ||
length: 4, | ||
}) | ||
).toEqual([ | ||
[ | ||
{ | ||
insert: 'bbb', | ||
attributes: { | ||
italic: true, | ||
}, | ||
}, | ||
{ | ||
index: 3, | ||
length: 3, | ||
}, | ||
], | ||
[ | ||
{ | ||
insert: 'ccc', | ||
attributes: { | ||
underline: true, | ||
}, | ||
}, | ||
{ | ||
index: 6, | ||
length: 3, | ||
}, | ||
], | ||
]); | ||
}); |
import type { TemplateResult } from 'lit'; | ||
import type { VirgoUnitText } from './components/index.js'; | ||
import type { VText } from './components/index.js'; | ||
import type { BaseTextAttributes } from './utils/index.js'; | ||
@@ -26,2 +26,2 @@ | ||
TextAttributes extends BaseTextAttributes = BaseTextAttributes | ||
> = (unitText: VirgoUnitText, attributes?: TextAttributes) => TemplateResult<1>; | ||
> = (vText: VText, attributes?: TextAttributes) => TemplateResult<1>; |
@@ -1,8 +0,5 @@ | ||
import { VirgoText } from '../components/virgo-text.js'; | ||
import { VirgoElement } from '../components/virgo-element.js'; | ||
import type { AttributesRenderer, DeltaInsert } from '../types.js'; | ||
import type { BaseTextAttributes } from './base-attributes.js'; | ||
/** | ||
* a default render function for text element | ||
*/ | ||
export function renderElement<TextAttributes extends BaseTextAttributes>( | ||
@@ -14,11 +11,11 @@ delta: DeltaInsert<TextAttributes>, | ||
attributesRenderer: AttributesRenderer<TextAttributes> | ||
): VirgoText<TextAttributes> { | ||
const baseText = new VirgoText<TextAttributes>(); | ||
baseText.delta = { | ||
): VirgoElement<TextAttributes> { | ||
const vElement = new VirgoElement<TextAttributes>(); | ||
vElement.delta = { | ||
insert: delta.insert, | ||
attributes: parseAttributes(delta.attributes), | ||
}; | ||
baseText.attributesRenderer = attributesRenderer; | ||
vElement.attributesRenderer = attributesRenderer; | ||
return baseText; | ||
return vElement; | ||
} |
276
src/virgo.ts
@@ -5,4 +5,4 @@ import { assertExists, Slot } from '@blocksuite/global/utils'; | ||
import { VirgoElement } from './components/virgo-element.js'; | ||
import { VirgoLine } from './components/virgo-line.js'; | ||
import { VirgoText } from './components/virgo-text.js'; | ||
import { ZERO_WIDTH_SPACE } from './constant.js'; | ||
@@ -45,2 +45,3 @@ import type { AttributesRenderer, DeltaInsert } from './types.js'; | ||
let textOffset = offset; | ||
if (isVText(node)) { | ||
@@ -50,14 +51,71 @@ text = node; | ||
} else if (isVElement(node)) { | ||
const textNode = getTextNodeFromElement(node); | ||
if (textNode) { | ||
text = textNode; | ||
textOffset = offset; | ||
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)) { | ||
const firstTextElement = node.querySelector('v-text'); | ||
if (firstTextElement) { | ||
const textNode = getTextNodeFromElement(firstTextElement); | ||
if (textNode) { | ||
text = textNode; | ||
textOffset = 0; | ||
} else if (isVLine(node) || isVRoot(node)) { | ||
const texts = VEditor.getTextNodesFromElement(node); | ||
if (texts.length > 0) { | ||
text = texts[0]; | ||
textOffset = 0; | ||
} | ||
} 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[0]; | ||
textOffset = 0; | ||
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 = 0; | ||
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[0]; | ||
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[0]; | ||
textOffset = calculateTextLength(text); | ||
break; | ||
} | ||
} | ||
} | ||
@@ -89,13 +147,7 @@ } | ||
const textNodes = Array.from( | ||
rootElement.querySelectorAll('[data-virgo-text="true"]') | ||
).map(textElement => getTextNodeFromElement(textElement)); | ||
const goalIndex = textNodes.indexOf(text); | ||
const texts = VEditor.getTextNodesFromElement(rootElement); | ||
const goalIndex = texts.indexOf(text); | ||
let index = 0; | ||
for (const textNode of textNodes.slice(0, goalIndex)) { | ||
if (!textNode) { | ||
return null; | ||
} | ||
index += calculateTextLength(textNode); | ||
for (const text of texts.slice(0, goalIndex)) { | ||
index += calculateTextLength(text); | ||
} | ||
@@ -107,8 +159,8 @@ | ||
const textElement = text.parentElement; | ||
if (!textElement) { | ||
throw new Error('text element not found'); | ||
const textParentElement = text.parentElement; | ||
if (!textParentElement) { | ||
throw new Error('text element parent not found'); | ||
} | ||
const lineElement = textElement.closest('virgo-line'); | ||
const lineElement = textParentElement.closest('v-line'); | ||
@@ -120,3 +172,3 @@ if (!lineElement) { | ||
const lineIndex = Array.from( | ||
rootElement.querySelectorAll('virgo-line') | ||
rootElement.querySelectorAll('v-line') | ||
).indexOf(lineElement); | ||
@@ -127,2 +179,21 @@ | ||
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 _rootElement: HTMLElement | null = null; | ||
@@ -145,4 +216,24 @@ private _mountAbort: AbortController | null = null; | ||
paste?: (event: ClipboardEvent) => void; | ||
virgoInput?: (event: InputEvent) => boolean; | ||
} = {}; | ||
private _defaultHandlers: VEditor['_handlers'] = { | ||
paste: (event: ClipboardEvent) => { | ||
const data = event.clipboardData?.getData('text/plain'); | ||
if (data) { | ||
const vRange = this._vRange; | ||
if (vRange) { | ||
this.insertText(vRange, data); | ||
this.slots.updateVRange.emit([ | ||
{ | ||
index: vRange.index + data.length, | ||
length: 0, | ||
}, | ||
'input', | ||
]); | ||
} | ||
} | ||
}, | ||
}; | ||
private _parseSchema = (textAttributes?: TextAttributes) => { | ||
@@ -163,3 +254,3 @@ return this._attributesSchema.optional().parse(textAttributes); | ||
if (chunk.length === 0) { | ||
virgoLine.elements.push(new VirgoText()); | ||
virgoLine.elements.push(new VirgoElement()); | ||
} else { | ||
@@ -218,22 +309,3 @@ chunk.forEach(delta => { | ||
bindHandlers( | ||
handlers: VEditor['_handlers'] = { | ||
paste: (event: ClipboardEvent) => { | ||
const data = event.clipboardData?.getData('text/plain'); | ||
if (data) { | ||
const vRange = this._vRange; | ||
if (vRange) { | ||
this.insertText(vRange, data); | ||
this.slots.updateVRange.emit([ | ||
{ | ||
index: vRange.index + data.length, | ||
length: 0, | ||
}, | ||
'input', | ||
]); | ||
} | ||
} | ||
}, | ||
} | ||
) { | ||
bindHandlers(handlers: VEditor['_handlers'] = this._defaultHandlers) { | ||
this._handlers = handlers; | ||
@@ -316,2 +388,4 @@ | ||
this._handlers = this._defaultHandlers; | ||
this._rootElement?.replaceChildren(); | ||
@@ -348,19 +422,20 @@ | ||
assertExists(this._rootElement); | ||
const textElements = Array.from( | ||
this._rootElement.querySelectorAll('[data-virgo-text="true"]') | ||
); | ||
const vLines = Array.from(this._rootElement.querySelectorAll('v-line')); | ||
let index = 0; | ||
for (const textElement of textElements) { | ||
if (!textElement.textContent) { | ||
throw new Error('text element should have textContent'); | ||
} | ||
if (index + textElement.textContent.length >= rangeIndex) { | ||
const text = getTextNodeFromElement(textElement); | ||
if (!text) { | ||
throw new Error('text node should have text content'); | ||
for (const vLine of vLines) { | ||
const texts = VEditor.getTextNodesFromElement(vLine); | ||
for (const text of texts) { | ||
if (!text.textContent) { | ||
throw new Error('text element should have textContent'); | ||
} | ||
return [text, rangeIndex - index]; | ||
if (index + text.textContent.length >= rangeIndex) { | ||
return [text, rangeIndex - index]; | ||
} | ||
index += text.textContent.length; | ||
} | ||
index += textElement.textContent.length; | ||
index += 1; | ||
} | ||
@@ -375,3 +450,3 @@ | ||
const lineElements = Array.from( | ||
this._rootElement.querySelectorAll('virgo-line') | ||
this._rootElement.querySelectorAll('v-line') | ||
); | ||
@@ -446,2 +521,3 @@ | ||
setReadOnly(isReadOnly: boolean): void { | ||
this.rootElement.contentEditable = isReadOnly ? 'false' : 'true'; | ||
this._isReadOnly = isReadOnly; | ||
@@ -469,6 +545,10 @@ } | ||
insertText(vRange: VRange, text: string): void { | ||
insertText( | ||
vRange: VRange, | ||
text: string, | ||
attributes: TextAttributes = {} as TextAttributes | ||
): void { | ||
this._transact(() => { | ||
this.yText.delete(vRange.index, vRange.length); | ||
this.yText.insert(vRange.index, text, {}); | ||
this.yText.insert(vRange.index, text, attributes); | ||
}); | ||
@@ -572,5 +652,4 @@ } | ||
assertExists(this._rootElement); | ||
const lineElements = Array.from( | ||
this._rootElement.querySelectorAll('virgo-line') | ||
this._rootElement.querySelectorAll('v-line') | ||
); | ||
@@ -590,27 +669,19 @@ | ||
const textElements = Array.from( | ||
lineElements[i].querySelectorAll('[data-virgo-text="true"]') | ||
); | ||
const texts = VEditor.getTextNodesFromElement(lineElements[i]); | ||
for (const text of texts) { | ||
const textLength = calculateTextLength(text); | ||
for (let j = 0; j < textElements.length; j++) { | ||
if (anchorText && focusText) { | ||
break; | ||
} | ||
const textNode = getTextNodeFromElement(textElements[j]); | ||
if (!textNode) { | ||
return null; | ||
} | ||
const textLength = calculateTextLength(textNode); | ||
if (!anchorText && index + textLength >= vRange.index) { | ||
anchorText = textNode; | ||
anchorText = text; | ||
anchorOffset = vRange.index - index; | ||
} | ||
if (!focusText && index + textLength >= vRange.index + vRange.length) { | ||
focusText = textNode; | ||
focusText = text; | ||
focusOffset = vRange.index + vRange.length - index; | ||
} | ||
if (anchorText && focusText) { | ||
break; | ||
} | ||
index += textLength; | ||
@@ -791,2 +862,11 @@ } | ||
let ifSkip = false; | ||
if (this._handlers.virgoInput) { | ||
ifSkip = this._handlers.virgoInput(event); | ||
} | ||
if (ifSkip) { | ||
return; | ||
} | ||
if (this._isReadOnly) { | ||
@@ -973,24 +1053,2 @@ return; | ||
function getTextNodeFromElement(element: Element): Text | null { | ||
let spanElement: Element | null = element; | ||
if (element instanceof HTMLElement && element.dataset.virgoText === 'true') { | ||
spanElement = element; | ||
} else { | ||
spanElement = element.querySelector('[data-virgo-text="true"]'); | ||
} | ||
if (!spanElement) { | ||
return null; | ||
} | ||
const textNode = Array.from(spanElement.childNodes).find( | ||
(node): node is Text => node instanceof Text | ||
); | ||
if (textNode) { | ||
return textNode; | ||
} | ||
return null; | ||
} | ||
function isVText(text: unknown): text is Text { | ||
@@ -1010,7 +1068,9 @@ return ( | ||
function isVLine(element: unknown): element is HTMLElement { | ||
return ( | ||
element instanceof HTMLElement && element.parentElement instanceof VirgoLine | ||
); | ||
return element instanceof HTMLElement && element instanceof VirgoLine; | ||
} | ||
function isVRoot(element: unknown): element is HTMLElement { | ||
return element instanceof HTMLElement && element.dataset.virgoRoot === 'true'; | ||
} | ||
function findDocumentOrShadowRoot<TextAttributes extends BaseTextAttributes>( | ||
@@ -1017,0 +1077,0 @@ editor: VEditor<TextAttributes> |
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
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
353274
4754
+ Added@blocksuite/global@0.5.0-20230305195734-5878b62(transitive)
- Removed@blocksuite/global@0.5.0-20230304192152-02cfe2b(transitive)