mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-11 15:16:28 +08:00
@@ -0,0 +1,12 @@
|
||||
import { html } from 'lit';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
export const EmbedGap = html`<span
|
||||
data-v-embed-gap="true"
|
||||
style=${styleMap({
|
||||
userSelect: 'text',
|
||||
padding: '0 0.5px',
|
||||
outline: 'none',
|
||||
})}
|
||||
><v-text></v-text
|
||||
></span>`;
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './v-element.js';
|
||||
export * from './v-line.js';
|
||||
export * from './v-text.js';
|
||||
@@ -0,0 +1,113 @@
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import { SignalWatcher } from '@blocksuite/global/lit';
|
||||
import type { BaseTextAttributes, DeltaInsert } from '@blocksuite/store';
|
||||
import { effect, signal } from '@preact/signals-core';
|
||||
import { html, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { ZERO_WIDTH_SPACE } from '../consts.js';
|
||||
import type { InlineEditor } from '../inline-editor.js';
|
||||
import { isInlineRangeIntersect } from '../utils/inline-range.js';
|
||||
|
||||
export class VElement<
|
||||
T extends BaseTextAttributes = BaseTextAttributes,
|
||||
> extends SignalWatcher(LitElement) {
|
||||
readonly disposables = new DisposableGroup();
|
||||
|
||||
readonly selected = signal(false);
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
const inlineRange = this.inlineEditor.inlineRange$.value;
|
||||
this.selected.value =
|
||||
!!inlineRange &&
|
||||
isInlineRangeIntersect(inlineRange, {
|
||||
index: this.startOffset,
|
||||
length: this.endOffset - this.startOffset,
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
override async getUpdateComplete(): Promise<boolean> {
|
||||
const result = await super.getUpdateComplete();
|
||||
const span = this.querySelector('[data-v-element="true"]') as HTMLElement;
|
||||
const el = span.firstElementChild as LitElement;
|
||||
await el.updateComplete;
|
||||
const vTexts = Array.from(this.querySelectorAll('v-text'));
|
||||
await Promise.all(vTexts.map(vText => vText.updateComplete));
|
||||
return result;
|
||||
}
|
||||
|
||||
override render() {
|
||||
const inlineEditor = this.inlineEditor;
|
||||
const attributeRenderer = inlineEditor.attributeService.attributeRenderer;
|
||||
const renderProps: Parameters<typeof attributeRenderer>[0] = {
|
||||
delta: this.delta,
|
||||
selected: this.selected.value,
|
||||
startOffset: this.startOffset,
|
||||
endOffset: this.endOffset,
|
||||
lineIndex: this.lineIndex,
|
||||
editor: inlineEditor,
|
||||
};
|
||||
|
||||
const isEmbed = inlineEditor.isEmbed(this.delta);
|
||||
if (isEmbed) {
|
||||
if (this.delta.insert.length !== 1) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.InlineEditorError,
|
||||
`The length of embed node should only be 1.
|
||||
This seems to be an internal issue with inline editor.
|
||||
Please go to https://github.com/toeverything/blocksuite/issues
|
||||
to report it.`
|
||||
);
|
||||
}
|
||||
|
||||
return html`<span
|
||||
data-v-embed="true"
|
||||
data-v-element="true"
|
||||
contenteditable="false"
|
||||
style=${styleMap({ userSelect: 'none' })}
|
||||
>${attributeRenderer(renderProps)}</span
|
||||
>`;
|
||||
}
|
||||
|
||||
// we need to avoid \n appearing before and after the span element, which will
|
||||
// cause the unexpected space
|
||||
return html`<span data-v-element="true"
|
||||
>${attributeRenderer(renderProps)}</span
|
||||
>`;
|
||||
}
|
||||
|
||||
@property({ type: Object })
|
||||
accessor delta: DeltaInsert<T> = {
|
||||
insert: ZERO_WIDTH_SPACE,
|
||||
};
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor endOffset!: number;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor inlineEditor!: InlineEditor;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor lineIndex!: number;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor startOffset!: number;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'v-element': VElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import type { DeltaInsert } from '@blocksuite/store';
|
||||
import { html, LitElement, type TemplateResult } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { INLINE_ROOT_ATTR, ZERO_WIDTH_SPACE } from '../consts.js';
|
||||
import type { InlineRootElement } from '../inline-editor.js';
|
||||
import { EmbedGap } from './embed-gap.js';
|
||||
|
||||
export class VLine extends LitElement {
|
||||
get inlineEditor() {
|
||||
const rootElement = this.closest(
|
||||
`[${INLINE_ROOT_ATTR}]`
|
||||
) as InlineRootElement;
|
||||
if (!rootElement) {
|
||||
throw new BlockSuiteError(
|
||||
BlockSuiteError.ErrorCode.ValueNotExists,
|
||||
'v-line must be inside a v-root'
|
||||
);
|
||||
}
|
||||
const inlineEditor = rootElement.inlineEditor;
|
||||
if (!inlineEditor) {
|
||||
throw new BlockSuiteError(
|
||||
BlockSuiteError.ErrorCode.ValueNotExists,
|
||||
'v-line must be inside a v-root with inline-editor'
|
||||
);
|
||||
}
|
||||
|
||||
return inlineEditor;
|
||||
}
|
||||
|
||||
get vElements() {
|
||||
return Array.from(this.querySelectorAll('v-element'));
|
||||
}
|
||||
|
||||
get vTextContent() {
|
||||
return this.vElements.reduce((acc, el) => acc + el.delta.insert, '');
|
||||
}
|
||||
|
||||
get vTextLength() {
|
||||
return this.vElements.reduce((acc, el) => acc + el.delta.insert.length, 0);
|
||||
}
|
||||
|
||||
// you should use vElements.length or vTextLength because v-element corresponds to the actual delta
|
||||
get vTexts() {
|
||||
return Array.from(this.querySelectorAll('v-text'));
|
||||
}
|
||||
|
||||
override createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
protected override firstUpdated(): void {
|
||||
this.style.display = 'block';
|
||||
|
||||
this.addEventListener('mousedown', e => {
|
||||
if (e.detail >= 2 && this.startOffset === this.endOffset) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.detail >= 3) {
|
||||
e.preventDefault();
|
||||
this.inlineEditor.setInlineRange({
|
||||
index: this.startOffset,
|
||||
length: this.endOffset - this.startOffset,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// vTexts.length > 0 does not mean the line is not empty,
|
||||
override async getUpdateComplete() {
|
||||
const result = await super.getUpdateComplete();
|
||||
await Promise.all(this.vElements.map(el => el.updateComplete));
|
||||
return result;
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (!this.isConnected) return;
|
||||
|
||||
if (this.inlineEditor.vLineRenderer) {
|
||||
return this.inlineEditor.vLineRenderer(this);
|
||||
}
|
||||
return this.renderVElements();
|
||||
}
|
||||
|
||||
renderVElements() {
|
||||
if (this.elements.length === 0) {
|
||||
// don't use v-element because it not correspond to the actual delta
|
||||
return html`<div><v-text .str=${ZERO_WIDTH_SPACE}></v-text></div>`;
|
||||
}
|
||||
|
||||
const inlineEditor = this.inlineEditor;
|
||||
const renderElements = this.elements.flatMap(([template, delta], index) => {
|
||||
if (inlineEditor.isEmbed(delta)) {
|
||||
if (delta.insert.length !== 1) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.InlineEditorError,
|
||||
`The length of embed node should only be 1.
|
||||
This seems to be an internal issue with inline editor.
|
||||
Please go to https://github.com/toeverything/blocksuite/issues
|
||||
to report it.`
|
||||
);
|
||||
}
|
||||
// we add `EmbedGap` to make cursor can be placed between embed elements
|
||||
if (index === 0) {
|
||||
const nextDelta = this.elements[index + 1]?.[1];
|
||||
if (!nextDelta || inlineEditor.isEmbed(nextDelta)) {
|
||||
return [EmbedGap, template, EmbedGap];
|
||||
} else {
|
||||
return [EmbedGap, template];
|
||||
}
|
||||
} else {
|
||||
const nextDelta = this.elements[index + 1]?.[1];
|
||||
if (!nextDelta || inlineEditor.isEmbed(nextDelta)) {
|
||||
return [template, EmbedGap];
|
||||
} else {
|
||||
return [template];
|
||||
}
|
||||
}
|
||||
}
|
||||
return template;
|
||||
});
|
||||
|
||||
// prettier will generate \n and cause unexpected space and line break
|
||||
// prettier-ignore
|
||||
return html`<div style=${styleMap({
|
||||
// this padding is used to make cursor can be placed at the
|
||||
// start and end of the line when the first and last element is embed element
|
||||
padding: '0 0.5px',
|
||||
display: 'inline-block',
|
||||
})}>${renderElements}</div>`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor elements: [TemplateResult<1>, DeltaInsert][] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor endOffset!: number;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor index!: number;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor startOffset!: number;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'v-line': VLine;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { html, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { ZERO_WIDTH_SPACE } from '../consts.js';
|
||||
|
||||
export class VText extends LitElement {
|
||||
override createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
override render() {
|
||||
// we need to avoid \n appearing before and after the span element, which will
|
||||
// cause the sync problem about the cursor position
|
||||
return html`<span
|
||||
style=${styleMap({
|
||||
'word-break': 'break-word',
|
||||
'text-wrap': 'wrap',
|
||||
'white-space-collapse': 'break-spaces',
|
||||
})}
|
||||
data-v-text="true"
|
||||
>${this.str}</span
|
||||
>`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor str: string = ZERO_WIDTH_SPACE;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'v-text': VText;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user