@blocksuite/virgo
Advanced tools
Comparing version 0.5.0-20230228114851-7e82f3e to 0.5.0-20230301172958-2384904
@@ -10,3 +10,2 @@ import { LitElement } from 'lit'; | ||
code: z.ZodOptional<z.ZodBoolean>; | ||
link: z.ZodOptional<z.ZodString>; | ||
}, "strip", z.ZodTypeAny, { | ||
@@ -18,3 +17,2 @@ bold?: boolean | undefined; | ||
code?: boolean | undefined; | ||
link?: string | undefined; | ||
}, { | ||
@@ -26,3 +24,2 @@ bold?: boolean | undefined; | ||
code?: boolean | undefined; | ||
link?: string | undefined; | ||
}>; | ||
@@ -29,0 +26,0 @@ export type BaseTextAttributes = z.infer<typeof baseTextAttributes>; |
@@ -19,3 +19,2 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { | ||
code: z.boolean().optional(), | ||
link: z.string().optional(), | ||
}); | ||
@@ -22,0 +21,0 @@ function virgoTextStyles(props) { |
@@ -10,3 +10,9 @@ import { Signal } from '@blocksuite/global/utils'; | ||
export type DeltaEntry = [DeltaInsert, VRange]; | ||
export interface DomPoint { | ||
text: Text; | ||
index: number; | ||
} | ||
export declare class VEditor { | ||
static nativePointToTextPoint(node: unknown, offset: number): readonly [Text, number] | null; | ||
static textPointToDomPoint(text: Text, offset: number, rootElement: HTMLElement): DomPoint | null; | ||
private _rootElement; | ||
@@ -13,0 +19,0 @@ private _rootElementAbort; |
@@ -8,2 +8,61 @@ import { assertExists, Signal } from '@blocksuite/global/utils'; | ||
export class VEditor { | ||
static nativePointToTextPoint(node, offset) { | ||
let text = null; | ||
let textOffset = offset; | ||
if (isVText(node)) { | ||
text = node; | ||
textOffset = offset; | ||
} | ||
else if (isVElement(node)) { | ||
const textNode = getTextNodeFromElement(node); | ||
if (textNode) { | ||
text = textNode; | ||
textOffset = offset; | ||
} | ||
} | ||
else if (isVLine(node)) { | ||
const firstTextElement = node.querySelector('v-text'); | ||
if (firstTextElement) { | ||
const textNode = getTextNodeFromElement(firstTextElement); | ||
if (textNode) { | ||
text = textNode; | ||
textOffset = 0; | ||
} | ||
} | ||
} | ||
if (!text) { | ||
return null; | ||
} | ||
return [text, textOffset]; | ||
} | ||
static textPointToDomPoint(text, offset, rootElement) { | ||
if (rootElement.dataset.virgoRoot !== 'true') { | ||
throw new Error('textRangeToDomPoint should be called with editor root element'); | ||
} | ||
if (!rootElement.contains(text)) { | ||
return null; | ||
} | ||
const textNodes = Array.from(rootElement.querySelectorAll('[data-virgo-text="true"]')).map(textElement => getTextNodeFromElement(textElement)); | ||
const goalIndex = textNodes.indexOf(text); | ||
let index = 0; | ||
for (const textNode of textNodes.slice(0, goalIndex)) { | ||
if (!textNode) { | ||
return null; | ||
} | ||
index += calculateTextLength(textNode); | ||
} | ||
if (text.wholeText !== ZERO_WIDTH_SPACE) { | ||
index += offset; | ||
} | ||
const textElement = text.parentElement; | ||
if (!textElement) { | ||
throw new Error('text element not found'); | ||
} | ||
const lineElement = textElement.closest('virgo-line'); | ||
if (!lineElement) { | ||
throw new Error('line element not found'); | ||
} | ||
const lineIndex = Array.from(rootElement.querySelectorAll('virgo-line')).indexOf(lineElement); | ||
return { text, index: index + lineIndex }; | ||
} | ||
constructor(yText, opts = {}) { | ||
@@ -36,7 +95,5 @@ this._rootElement = null; | ||
return; | ||
const { anchorNode, focusNode } = selection; | ||
if (!this._rootElement.contains(anchorNode) || | ||
!this._rootElement.contains(focusNode)) { | ||
const range = selection.getRangeAt(0); | ||
if (!range || !range.intersectsNode(this._rootElement)) | ||
return; | ||
} | ||
const vRange = this.toVRange(selection); | ||
@@ -89,2 +146,5 @@ if (vRange) { | ||
}; | ||
if (!yText.doc) { | ||
throw new Error('yText must be attached to a Y.Doc'); | ||
} | ||
this.yText = yText; | ||
@@ -355,2 +415,3 @@ const { renderElement, onKeyDown } = opts; | ||
assertExists(this._rootElement); | ||
const root = this._rootElement; | ||
const { anchorNode, anchorOffset, focusNode, focusOffset } = selection; | ||
@@ -360,8 +421,13 @@ if (!anchorNode || !focusNode) { | ||
} | ||
const [anchorText, anchorTextOffset] = getTextAndOffset(anchorNode, anchorOffset); | ||
const [focusText, focusTextOffset] = getTextAndOffset(focusNode, focusOffset); | ||
const anchorTextPoint = VEditor.nativePointToTextPoint(anchorNode, anchorOffset); | ||
const focusTextPoint = VEditor.nativePointToTextPoint(focusNode, focusOffset); | ||
if (!anchorTextPoint || !focusTextPoint) { | ||
return null; | ||
} | ||
const [anchorText, anchorTextOffset] = anchorTextPoint; | ||
const [focusText, focusTextOffset] = focusTextPoint; | ||
// case 1 | ||
if (anchorText && focusText) { | ||
const anchorDomPoint = textPointToDomPoint(anchorText, anchorTextOffset, this._rootElement); | ||
const focusDomPoint = textPointToDomPoint(focusText, focusTextOffset, this._rootElement); | ||
if (root.contains(anchorText) && root.contains(focusText)) { | ||
const anchorDomPoint = VEditor.textPointToDomPoint(anchorText, anchorTextOffset, this._rootElement); | ||
const focusDomPoint = VEditor.textPointToDomPoint(focusText, focusTextOffset, this._rootElement); | ||
if (!anchorDomPoint || !focusDomPoint) { | ||
@@ -375,37 +441,45 @@ return null; | ||
} | ||
// case 2 | ||
if (anchorText && !focusText) { | ||
const anchorDomPoint = textPointToDomPoint(anchorText, anchorTextOffset, this._rootElement); | ||
if (!anchorDomPoint) { | ||
return null; | ||
} | ||
// case 2.1 | ||
if (!root.contains(anchorText) && root.contains(focusText)) { | ||
if (isSelectionBackwards(selection)) { | ||
const anchorDomPoint = VEditor.textPointToDomPoint(anchorText, anchorTextOffset, this._rootElement); | ||
if (!anchorDomPoint) { | ||
return null; | ||
} | ||
return { | ||
index: 0, | ||
length: anchorDomPoint.index, | ||
index: anchorDomPoint.index, | ||
length: this.yText.length - anchorDomPoint.index, | ||
}; | ||
} | ||
else { | ||
const focusDomPoint = VEditor.textPointToDomPoint(focusText, focusTextOffset, this._rootElement); | ||
if (!focusDomPoint) { | ||
return null; | ||
} | ||
return { | ||
index: anchorDomPoint.index, | ||
length: anchorDomPoint.text.wholeText.length - anchorDomPoint.index, | ||
index: 0, | ||
length: focusDomPoint.index, | ||
}; | ||
} | ||
} | ||
// case 2 | ||
if (!anchorText && focusText) { | ||
const focusDomPoint = textPointToDomPoint(focusText, focusTextOffset, this._rootElement); | ||
if (!focusDomPoint) { | ||
return null; | ||
} | ||
// case 2.2 | ||
if (root.contains(anchorText) && !root.contains(focusText)) { | ||
if (isSelectionBackwards(selection)) { | ||
const focusDomPoint = VEditor.textPointToDomPoint(focusText, focusTextOffset, this._rootElement); | ||
if (!focusDomPoint) { | ||
return null; | ||
} | ||
return { | ||
index: focusDomPoint.index, | ||
length: focusDomPoint.text.wholeText.length - focusDomPoint.index, | ||
index: 0, | ||
length: focusDomPoint.index, | ||
}; | ||
} | ||
else { | ||
const anchorDomPoint = VEditor.textPointToDomPoint(anchorText, anchorTextOffset, this._rootElement); | ||
if (!anchorDomPoint) { | ||
return null; | ||
} | ||
return { | ||
index: 0, | ||
length: focusDomPoint.index, | ||
index: anchorDomPoint.index, | ||
length: this.yText.length - anchorDomPoint.index, | ||
}; | ||
@@ -415,5 +489,3 @@ } | ||
// case 3 | ||
if (!anchorText && | ||
!focusText && | ||
selection.containsNode(this._rootElement)) { | ||
if (!root.contains(anchorText) && !root.contains(focusText)) { | ||
return { | ||
@@ -514,32 +586,2 @@ index: 0, | ||
} | ||
function textPointToDomPoint(text, offset, rootElement) { | ||
if (rootElement.dataset.virgoRoot !== 'true') { | ||
throw new Error('textRangeToDomPoint should be called with editor root element'); | ||
} | ||
if (!rootElement.contains(text)) { | ||
throw new Error('text is not in root element'); | ||
} | ||
const textNodes = Array.from(rootElement.querySelectorAll('[data-virgo-text="true"]')).map(textElement => getTextNodeFromElement(textElement)); | ||
const goalIndex = textNodes.indexOf(text); | ||
let index = 0; | ||
for (const textNode of textNodes.slice(0, goalIndex)) { | ||
if (!textNode) { | ||
return null; | ||
} | ||
index += calculateTextLength(textNode); | ||
} | ||
if (text.wholeText !== ZERO_WIDTH_SPACE) { | ||
index += offset; | ||
} | ||
const textElement = text.parentElement; | ||
if (!textElement) { | ||
throw new Error('text element not found'); | ||
} | ||
const lineElement = textElement.closest('virgo-line'); | ||
if (!lineElement) { | ||
throw new Error('line element not found'); | ||
} | ||
const lineIndex = Array.from(rootElement.querySelectorAll('virgo-line')).indexOf(lineElement); | ||
return { text, index: index + lineIndex }; | ||
} | ||
function isSelectionBackwards(selection) { | ||
@@ -591,28 +633,2 @@ let backwards = false; | ||
} | ||
function getTextAndOffset(node, offset) { | ||
let text = null; | ||
let textOffset = offset; | ||
if (isVText(node)) { | ||
text = node; | ||
textOffset = offset; | ||
} | ||
else if (isVElement(node)) { | ||
const textNode = getTextNodeFromElement(node); | ||
if (textNode) { | ||
text = textNode; | ||
textOffset = offset; | ||
} | ||
} | ||
else if (isVLine(node)) { | ||
const firstTextElement = node.querySelector('v-text'); | ||
if (firstTextElement) { | ||
const textNode = getTextNodeFromElement(firstTextElement); | ||
if (textNode) { | ||
text = textNode; | ||
textOffset = 0; | ||
} | ||
} | ||
} | ||
return [text, textOffset]; | ||
} | ||
function findDocumentOrShadowRoot(editor) { | ||
@@ -619,0 +635,0 @@ const el = editor.getRootElement(); |
{ | ||
"name": "@blocksuite/virgo", | ||
"version": "0.5.0-20230228114851-7e82f3e", | ||
"version": "0.5.0-20230301172958-2384904", | ||
"description": "A micro editor.", | ||
@@ -26,3 +26,3 @@ "main": "dist/index.js", | ||
"dependencies": { | ||
"@blocksuite/global": "0.5.0-20230228114851-7e82f3e", | ||
"@blocksuite/global": "0.5.0-20230301172958-2384904", | ||
"zod": "^3.20.6" | ||
@@ -29,0 +29,0 @@ }, |
@@ -16,3 +16,2 @@ import { html, LitElement } from 'lit'; | ||
code: z.boolean().optional(), | ||
link: z.string().optional(), | ||
}); | ||
@@ -19,0 +18,0 @@ |
278
src/virgo.ts
@@ -20,3 +20,3 @@ import { assertExists, Signal } from '@blocksuite/global/utils'; | ||
interface DomPoint { | ||
export interface DomPoint { | ||
// which text node this point is in | ||
@@ -29,2 +29,85 @@ text: Text; | ||
export class VEditor { | ||
static nativePointToTextPoint( | ||
node: unknown, | ||
offset: number | ||
): readonly [Text, number] | null { | ||
let text: Text | null = null; | ||
let textOffset = offset; | ||
if (isVText(node)) { | ||
text = node; | ||
textOffset = offset; | ||
} else if (isVElement(node)) { | ||
const textNode = getTextNodeFromElement(node); | ||
if (textNode) { | ||
text = textNode; | ||
textOffset = offset; | ||
} | ||
} else if (isVLine(node)) { | ||
const firstTextElement = node.querySelector('v-text'); | ||
if (firstTextElement) { | ||
const textNode = getTextNodeFromElement(firstTextElement); | ||
if (textNode) { | ||
text = textNode; | ||
textOffset = 0; | ||
} | ||
} | ||
} | ||
if (!text) { | ||
return null; | ||
} | ||
return [text, textOffset] as const; | ||
} | ||
static textPointToDomPoint( | ||
text: Text, | ||
offset: number, | ||
rootElement: HTMLElement | ||
): DomPoint | null { | ||
if (rootElement.dataset.virgoRoot !== 'true') { | ||
throw new Error( | ||
'textRangeToDomPoint should be called with editor root element' | ||
); | ||
} | ||
if (!rootElement.contains(text)) { | ||
return null; | ||
} | ||
const textNodes = Array.from( | ||
rootElement.querySelectorAll('[data-virgo-text="true"]') | ||
).map(textElement => getTextNodeFromElement(textElement)); | ||
const goalIndex = textNodes.indexOf(text); | ||
let index = 0; | ||
for (const textNode of textNodes.slice(0, goalIndex)) { | ||
if (!textNode) { | ||
return null; | ||
} | ||
index += calculateTextLength(textNode); | ||
} | ||
if (text.wholeText !== ZERO_WIDTH_SPACE) { | ||
index += offset; | ||
} | ||
const textElement = text.parentElement; | ||
if (!textElement) { | ||
throw new Error('text element not found'); | ||
} | ||
const lineElement = textElement.closest('virgo-line'); | ||
if (!lineElement) { | ||
throw new Error('line element not found'); | ||
} | ||
const lineIndex = Array.from( | ||
rootElement.querySelectorAll('virgo-line') | ||
).indexOf(lineElement); | ||
return { text, index: index + lineIndex }; | ||
} | ||
private _rootElement: HTMLElement | null = null; | ||
@@ -53,2 +136,6 @@ private _rootElementAbort: AbortController | null = null; | ||
) { | ||
if (!yText.doc) { | ||
throw new Error('yText must be attached to a Y.Doc'); | ||
} | ||
this.yText = yText; | ||
@@ -408,2 +495,3 @@ const { renderElement, onKeyDown } = opts; | ||
assertExists(this._rootElement); | ||
const root = this._rootElement; | ||
@@ -415,7 +503,7 @@ const { anchorNode, anchorOffset, focusNode, focusOffset } = selection; | ||
const [anchorText, anchorTextOffset] = getTextAndOffset( | ||
const anchorTextPoint = VEditor.nativePointToTextPoint( | ||
anchorNode, | ||
anchorOffset | ||
); | ||
const [focusText, focusTextOffset] = getTextAndOffset( | ||
const focusTextPoint = VEditor.nativePointToTextPoint( | ||
focusNode, | ||
@@ -425,5 +513,12 @@ focusOffset | ||
if (!anchorTextPoint || !focusTextPoint) { | ||
return null; | ||
} | ||
const [anchorText, anchorTextOffset] = anchorTextPoint; | ||
const [focusText, focusTextOffset] = focusTextPoint; | ||
// case 1 | ||
if (anchorText && focusText) { | ||
const anchorDomPoint = textPointToDomPoint( | ||
if (root.contains(anchorText) && root.contains(focusText)) { | ||
const anchorDomPoint = VEditor.textPointToDomPoint( | ||
anchorText, | ||
@@ -433,3 +528,3 @@ anchorTextOffset, | ||
); | ||
const focusDomPoint = textPointToDomPoint( | ||
const focusDomPoint = VEditor.textPointToDomPoint( | ||
focusText, | ||
@@ -450,23 +545,33 @@ focusTextOffset, | ||
// case 2 | ||
if (anchorText && !focusText) { | ||
const anchorDomPoint = textPointToDomPoint( | ||
anchorText, | ||
anchorTextOffset, | ||
this._rootElement | ||
); | ||
// case 2.1 | ||
if (!root.contains(anchorText) && root.contains(focusText)) { | ||
if (isSelectionBackwards(selection)) { | ||
const anchorDomPoint = VEditor.textPointToDomPoint( | ||
anchorText, | ||
anchorTextOffset, | ||
this._rootElement | ||
); | ||
if (!anchorDomPoint) { | ||
return null; | ||
} | ||
if (!anchorDomPoint) { | ||
return null; | ||
} | ||
if (isSelectionBackwards(selection)) { | ||
return { | ||
index: 0, | ||
length: anchorDomPoint.index, | ||
index: anchorDomPoint.index, | ||
length: this.yText.length - anchorDomPoint.index, | ||
}; | ||
} else { | ||
const focusDomPoint = VEditor.textPointToDomPoint( | ||
focusText, | ||
focusTextOffset, | ||
this._rootElement | ||
); | ||
if (!focusDomPoint) { | ||
return null; | ||
} | ||
return { | ||
index: anchorDomPoint.index, | ||
length: anchorDomPoint.text.wholeText.length - anchorDomPoint.index, | ||
index: 0, | ||
length: focusDomPoint.index, | ||
}; | ||
@@ -476,23 +581,33 @@ } | ||
// case 2 | ||
if (!anchorText && focusText) { | ||
const focusDomPoint = textPointToDomPoint( | ||
focusText, | ||
focusTextOffset, | ||
this._rootElement | ||
); | ||
// case 2.2 | ||
if (root.contains(anchorText) && !root.contains(focusText)) { | ||
if (isSelectionBackwards(selection)) { | ||
const focusDomPoint = VEditor.textPointToDomPoint( | ||
focusText, | ||
focusTextOffset, | ||
this._rootElement | ||
); | ||
if (!focusDomPoint) { | ||
return null; | ||
} | ||
if (!focusDomPoint) { | ||
return null; | ||
} | ||
if (isSelectionBackwards(selection)) { | ||
return { | ||
index: focusDomPoint.index, | ||
length: focusDomPoint.text.wholeText.length - focusDomPoint.index, | ||
index: 0, | ||
length: focusDomPoint.index, | ||
}; | ||
} else { | ||
const anchorDomPoint = VEditor.textPointToDomPoint( | ||
anchorText, | ||
anchorTextOffset, | ||
this._rootElement | ||
); | ||
if (!anchorDomPoint) { | ||
return null; | ||
} | ||
return { | ||
index: 0, | ||
length: focusDomPoint.index, | ||
index: anchorDomPoint.index, | ||
length: this.yText.length - anchorDomPoint.index, | ||
}; | ||
@@ -503,7 +618,3 @@ } | ||
// case 3 | ||
if ( | ||
!anchorText && | ||
!focusText && | ||
selection.containsNode(this._rootElement) | ||
) { | ||
if (!root.contains(anchorText) && !root.contains(focusText)) { | ||
return { | ||
@@ -635,9 +746,4 @@ index: 0, | ||
const { anchorNode, focusNode } = selection; | ||
if ( | ||
!this._rootElement.contains(anchorNode) || | ||
!this._rootElement.contains(focusNode) | ||
) { | ||
return; | ||
} | ||
const range = selection.getRangeAt(0); | ||
if (!range || !range.intersectsNode(this._rootElement)) return; | ||
@@ -709,52 +815,2 @@ const vRange = this.toVRange(selection); | ||
function textPointToDomPoint( | ||
text: Text, | ||
offset: number, | ||
rootElement: HTMLElement | ||
): DomPoint | null { | ||
if (rootElement.dataset.virgoRoot !== 'true') { | ||
throw new Error( | ||
'textRangeToDomPoint should be called with editor root element' | ||
); | ||
} | ||
if (!rootElement.contains(text)) { | ||
throw new Error('text is not in root element'); | ||
} | ||
const textNodes = Array.from( | ||
rootElement.querySelectorAll('[data-virgo-text="true"]') | ||
).map(textElement => getTextNodeFromElement(textElement)); | ||
const goalIndex = textNodes.indexOf(text); | ||
let index = 0; | ||
for (const textNode of textNodes.slice(0, goalIndex)) { | ||
if (!textNode) { | ||
return null; | ||
} | ||
index += calculateTextLength(textNode); | ||
} | ||
if (text.wholeText !== ZERO_WIDTH_SPACE) { | ||
index += offset; | ||
} | ||
const textElement = text.parentElement; | ||
if (!textElement) { | ||
throw new Error('text element not found'); | ||
} | ||
const lineElement = textElement.closest('virgo-line'); | ||
if (!lineElement) { | ||
throw new Error('line element not found'); | ||
} | ||
const lineIndex = Array.from( | ||
rootElement.querySelectorAll('virgo-line') | ||
).indexOf(lineElement); | ||
return { text, index: index + lineIndex }; | ||
} | ||
function isSelectionBackwards(selection: Selection): boolean { | ||
@@ -821,28 +877,2 @@ let backwards = false; | ||
function getTextAndOffset(node: unknown, offset: number) { | ||
let text: Text | null = null; | ||
let textOffset = offset; | ||
if (isVText(node)) { | ||
text = node; | ||
textOffset = offset; | ||
} else if (isVElement(node)) { | ||
const textNode = getTextNodeFromElement(node); | ||
if (textNode) { | ||
text = textNode; | ||
textOffset = offset; | ||
} | ||
} else if (isVLine(node)) { | ||
const firstTextElement = node.querySelector('v-text'); | ||
if (firstTextElement) { | ||
const textNode = getTextNodeFromElement(firstTextElement); | ||
if (textNode) { | ||
text = textNode; | ||
textOffset = 0; | ||
} | ||
} | ||
} | ||
return [text, textOffset] as const; | ||
} | ||
function findDocumentOrShadowRoot(editor: VEditor): Document { | ||
@@ -849,0 +879,0 @@ const el = editor.getRootElement(); |
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
312290
4016
+ Added@blocksuite/global@0.5.0-20230301172958-2384904(transitive)
- Removed@blocksuite/global@0.5.0-20230228114851-7e82f3e(transitive)