Skip to content

Commit

Permalink
refactor(page): enhance rich-text and replace virgo-input with `r…
Browse files Browse the repository at this point in the history
…ich-text` (toeverything#4956)
  • Loading branch information
Flrande authored Oct 8, 2023
1 parent f71df00 commit 06fc7b7
Show file tree
Hide file tree
Showing 21 changed files with 700 additions and 879 deletions.
18 changes: 8 additions & 10 deletions packages/blocks/src/__internal__/clipboard/edgeless-clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import type { Clipboard } from './type.js';
import {
clipboardData2Blocks,
copyBlocksInPage,
copyOnPhasorElementWithText,
getBlockClipboardInfo,
} from './utils/commons.js';
import {
Expand Down Expand Up @@ -152,6 +151,8 @@ export class EdgelessClipboard implements Clipboard {

const { state, elements } = this.selection;
if (state.editing) {
// use build-in cut handler in rich-text when cut in surface text element
if (isPhasorElementWithText(elements[0])) return;
deleteModelsByTextSelection(this._edgeless.root);
return;
}
Expand All @@ -176,11 +177,9 @@ export class EdgelessClipboard implements Clipboard {
const elements = getCopyElements(this.surface, this.selection.elements);
// when note active, handle copy like page mode
if (state.editing) {
if (isPhasorElementWithText(elements[0])) {
copyOnPhasorElementWithText(this._edgeless);
} else {
await copyBlocksInPage(this._edgeless.root);
}
// use build-in copy handler in rich-text when copy in surface text element
if (isPhasorElementWithText(elements[0])) return;
await copyBlocksInPage(this._edgeless.root);
return;
}
const data = await prepareClipboardData(elements);
Expand All @@ -199,10 +198,9 @@ export class EdgelessClipboard implements Clipboard {
e.preventDefault();
const { state, elements } = this.selection;
if (state.editing) {
if (!isPhasorElementWithText(elements[0])) {
this._pasteInTextNote(e);
}
// use build-in paste handler in virgo-input when paste in surface text element
// use build-in paste handler in rich-text when paste in surface text element
if (isPhasorElementWithText(elements[0])) return;
await this._pasteInTextNote(e);
return;
}

Expand Down
31 changes: 0 additions & 31 deletions packages/blocks/src/__internal__/clipboard/utils/commons.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import type { TextSelection } from '@blocksuite/block-std';
import { assertExists } from '@blocksuite/global/utils';
import type { BlockSuiteRoot } from '@blocksuite/lit';
import { BaseBlockModel, type Page } from '@blocksuite/store';

import type { EdgelessPageBlockComponent } from '../../../page-block/edgeless/edgeless-page-block.js';
import { getSelectedContentModels } from '../../../page-block/utils/selection.js';
import { ContentParser } from '../../content-parser/index.js';
import type { SelectedBlock } from '../../content-parser/types.js';
import { getService } from '../../service/index.js';
import { registerAllBlocks } from '../../service/legacy-services/index.js';
import {
getCurrentNativeRange,
getEdgelessCanvasTextEditor,
hasNativeSelection,
resetNativeSelection,
type SerializedBlock,
Expand Down Expand Up @@ -306,31 +303,3 @@ export async function clipboardData2Blocks(
export function removeFragmentFromHtmlClipboardString(html: string) {
return html.replace(/<!--StartFragment-->([^]*)<!--EndFragment-->/g, '$1');
}

export function copyOnPhasorElementWithText(
edgeless: EdgelessPageBlockComponent
) {
const edgelessTextEditor = getEdgelessCanvasTextEditor(edgeless);
if (edgelessTextEditor) {
const vEditor = edgelessTextEditor.vEditor;
assertExists(vEditor);
const vRange = vEditor.getVRange();
if (vRange) {
const text = vEditor.yText
.toString()
.slice(vRange.index, vRange.index + vRange.length);
const clipboardItem = new ClipboardItem(CLIPBOARD_MIMETYPE.TEXT, text);

edgelessTextEditor.setKeeping(true);
performNativeCopy([clipboardItem]);
edgelessTextEditor.setKeeping(false);

// `performNativeCopy` will trigger selection change and make virgo
// execute `setVRange(null)`, so we need to use `setTimeout` to
// make sure `setVRange(vRange)` is executed after `setVRange(null)`.
setTimeout(() => {
vEditor.setVRange(vRange);
});
}
}
}
20 changes: 10 additions & 10 deletions packages/blocks/src/code-block/code-block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import { HoverController } from '../components/index.js';
import { createLitPortal } from '../components/portal.js';
import { bindContainerHotkey } from '../components/rich-text/keymap/index.js';
import type { RichText } from '../components/rich-text/rich-text.js';
import type { AffineTextSchema } from '../components/rich-text/virgo/types.js';
import { tooltipStyle } from '../components/tooltip/tooltip.js';
import { ArrowDownIcon } from '../icons/index.js';
import type { CodeBlockModel } from './code-model.js';
Expand Down Expand Up @@ -196,14 +195,12 @@ export class CodeBlockComponent extends BlockElement<CodeBlockModel> {
return this.model.page.readonly;
}

readonly textSchema: AffineTextSchema = {
attributesSchema: z.object({}),
textRenderer: () =>
getCodeLineRenderer(() => ({
lang: this.model.language.toLowerCase() as Lang,
highlighter: this._highlighter,
})),
};
readonly attributesSchema = z.object({});
readonly getAttributeRenderer = () =>
getCodeLineRenderer(() => ({
lang: this.model.language.toLowerCase() as Lang,
highlighter: this._highlighter,
}));

private _richTextResizeObserver: ResizeObserver = new ResizeObserver(() => {
this._updateLineNumbers();
Expand Down Expand Up @@ -617,9 +614,12 @@ export class CodeBlockComponent extends BlockElement<CodeBlockModel> {
<rich-text
.yText=${this.model.text.yText}
.undoManager=${this.model.page.history}
.textSchema=${this.textSchema}
.attributesSchema=${this.attributesSchema}
.attributeRenderer=${this.getAttributeRenderer()}
.readonly=${this.model.page.readonly}
.vRangeProvider=${this._vRangeProvider}
.enableClipboard=${false}
.enableUndoRedo=${false}
>
</rich-text>
</div>
Expand Down
140 changes: 115 additions & 25 deletions packages/blocks/src/components/rich-text/rich-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,20 @@ import { ShadowlessElement, WithDisposable } from '@blocksuite/lit';
import type { Y } from '@blocksuite/store';
import { Workspace } from '@blocksuite/store';
import {
type AttributeRenderer,
createVirgoKeyDownHandler,
VEditor,
type VRange,
type VRangeProvider,
} from '@blocksuite/virgo';
import { css, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js';
import type { z } from 'zod';

import { tryFormatInlineStyle } from './markdown/inline.js';
import { onVBeforeinput, onVCompositionEnd } from './virgo/hooks.js';
import {
type AffineTextAttributes,
type AffineTextSchema,
type AffineVEditor,
} from './virgo/types.js';

Expand Down Expand Up @@ -50,16 +51,32 @@ export class RichText extends WithDisposable(ShadowlessElement) {
yText!: Y.Text;

@property({ attribute: false })
textSchema!: AffineTextSchema;
attributesSchema?: z.ZodSchema;
@property({ attribute: false })
attributeRenderer?: AttributeRenderer;

@property({ attribute: false })
readonly = false;

@property({ attribute: false })
vRangeProvider?: VRangeProvider;
// rich-text will create a undoManager if it is not provided.
@property({ attribute: false })
undoManager!: Y.UndoManager;

// If it is true rich-test will prevent events related to clipboard bubbling up and handle them by itself.
@property({ attribute: false })
enableClipboard = true;
// If it is true rich-text will handle undo/redo by itself. (including v-range restore)
// It will listen ctrl+z/ctrl+shift+z and call undoManager.undo/redo, keydown event will not
// bubble up if pressed ctrl+z/ctrl+shift+z.
@property({ attribute: false })
enableUndoRedo = true;
@property({ attribute: false })
enableAutoScrollVertically = true;
@property({ attribute: false })
enableAutoScrollHorizontally = true;

private _vEditor: AffineVEditor | null = null;
get vEditor() {
return this._vEditor;
Expand All @@ -80,8 +97,12 @@ export class RichText extends WithDisposable(ShadowlessElement) {
},
vRangeProvider: this.vRangeProvider,
});
this._vEditor.setAttributeSchema(this.textSchema.attributesSchema);
this._vEditor.setAttributeRenderer(this.textSchema.textRenderer());
if (this.attributesSchema) {
this._vEditor.setAttributeSchema(this.attributesSchema);
}
if (this.attributeRenderer) {
this._vEditor.setAttributeRenderer(this.attributeRenderer);
}

assertExists(this._vEditor);
const vEditor = this._vEditor;
Expand Down Expand Up @@ -109,27 +130,31 @@ export class RichText extends WithDisposable(ShadowlessElement) {
const range = vEditor.toDomRange(vRange);
if (!range) return;

this.scrollIntoView({
block: 'nearest',
});

// make sure the result of moveX is expected
this.scrollLeft = 0;
const thisRect = this.getBoundingClientRect();
const rangeRect = range.getBoundingClientRect();
let moveX = 0;
if (
rangeRect.left + rangeRect.width >
thisRect.left + thisRect.width
) {
moveX =
rangeRect.left +
rangeRect.width -
(thisRect.left + thisRect.width);
moveX = Math.max(this._lastScrollLeft, moveX);
if (this.enableAutoScrollVertically) {
this.scrollIntoView({
block: 'nearest',
});
}

this.scrollLeft = moveX;
if (this.enableAutoScrollHorizontally) {
// make sure the result of moveX is expected
this.scrollLeft = 0;
const thisRect = this.getBoundingClientRect();
const rangeRect = range.getBoundingClientRect();
let moveX = 0;
if (
rangeRect.left + rangeRect.width >
thisRect.left + thisRect.width
) {
moveX =
rangeRect.left +
rangeRect.width -
(thisRect.left + thisRect.width);
moveX = Math.max(this._lastScrollLeft, moveX);
}

this.scrollLeft = moveX;
}
});
})
);
Expand All @@ -152,6 +177,65 @@ export class RichText extends WithDisposable(ShadowlessElement) {
}
};

private _onCopy = (e: ClipboardEvent) => {
const vEditor = this.vEditor;
assertExists(vEditor);

const vRange = vEditor.getVRange();
if (!vRange) return;

const text = vEditor.yTextString.slice(
vRange.index,
vRange.index + vRange.length
);

e.clipboardData?.setData('text/plain', text);
e.preventDefault();
e.stopPropagation();
};

private _onCut = (e: ClipboardEvent) => {
const vEditor = this.vEditor;
assertExists(vEditor);

const vRange = vEditor.getVRange();
if (!vRange) return;

const text = vEditor.yTextString.slice(
vRange.index,
vRange.index + vRange.length
);
vEditor.deleteText(vRange);
vEditor.setVRange({
index: vRange.index,
length: 0,
});

e.clipboardData?.setData('text/plain', text);
e.preventDefault();
e.stopPropagation();
};

private _onPaste = (e: ClipboardEvent) => {
const vEditor = this.vEditor;
assertExists(vEditor);

const vRange = vEditor.getVRange();
if (!vRange) return;

const text = e.clipboardData?.getData('text/plain');
if (!text) return;

vEditor.insertText(vRange, text);
vEditor.setVRange({
index: vRange.index + text.length,
length: 0,
});

e.preventDefault();
e.stopPropagation();
};

private _unmount() {
if (this.vEditor?.mounted) {
this.vEditor.unmount();
Expand All @@ -170,14 +254,14 @@ export class RichText extends WithDisposable(ShadowlessElement) {

assertExists(this.yText, 'rich-text need yText to init.');
assertExists(this.yText.doc, 'yText should be bind to yDoc.');
assertExists(this.textSchema, 'rich-text need textSchema to init.');

// Rich-Text controls undo-redo itself if undoManager is not provided
if (!this.undoManager) {
this.undoManager = new Workspace.Y.UndoManager(this.yText, {
trackedOrigins: new Set([this.yText.doc.clientID]),
});
}

if (this.enableUndoRedo) {
this.disposables.addFromEvent(this, 'keydown', (e: KeyboardEvent) => {
if (e.ctrlKey || e.metaKey) {
if (e.key === 'z') {
Expand All @@ -201,6 +285,12 @@ export class RichText extends WithDisposable(ShadowlessElement) {
});
}

if (this.enableClipboard) {
this.disposables.addFromEvent(this, 'copy', this._onCopy);
this.disposables.addFromEvent(this, 'cut', this._onCut);
this.disposables.addFromEvent(this, 'paste', this._onPaste);
}

this.updateComplete.then(() => {
this._unmount();
this._init();
Expand Down
Loading

0 comments on commit 06fc7b7

Please sign in to comment.