refactor(editor): move editor components to frontend core (#10335)

### TL;DR
Moved editor components from BlockSuite presets to AFFiNE core and updated imports accordingly.

### What changed?
- Relocated `EdgelessEditor` and `PageEditor` components from BlockSuite presets to AFFiNE core
- Removed basic editor examples from playground
- Updated import paths across the codebase to reference new component locations
- Added editor effects registration in AFFiNE core
- Removed editor exports from BlockSuite presets

### How to test?
1. Launch the application
2. Verify both page and edgeless editors load correctly
3. Confirm editor functionality remains intact including:
   - Document editing
   - Mode switching
   - Editor toolbars and controls
   - Multiple editor instances

### Why make this change?
This change better aligns with AFFiNE's architecture by moving editor components closer to where they are used. It reduces coupling with BlockSuite presets and gives AFFiNE more direct control over editor customization and implementation.
This commit is contained in:
Saul-Mirone
2025-02-21 04:28:54 +00:00
parent 7f833f8c15
commit adcc6b578c
34 changed files with 102 additions and 324 deletions
@@ -1,23 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Basic EdgelessEditor Example</title>
<style>
html,
body {
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
background-color: var(--affine-white-90);
transition: background-color 0.3s;
}
</style>
</head>
<body>
<script type="module" src="./main.ts"></script>
</body>
</html>
@@ -1,15 +0,0 @@
import '../../../style.css';
import { effects as blocksEffects } from '@blocksuite/blocks/effects';
import { EdgelessEditor } from '@blocksuite/presets';
import { effects as presetsEffects } from '@blocksuite/presets/effects';
import { createEmptyDoc } from '../../../apps/_common/helper';
blocksEffects();
presetsEffects();
const doc = createEmptyDoc().init();
const editor = new EdgelessEditor();
editor.doc = doc;
document.body.append(editor);
@@ -1,23 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Basic PageEditor Example</title>
<style>
html,
body {
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
background-color: var(--affine-white-90);
transition: background-color 0.3s;
}
</style>
</head>
<body>
<script type="module" src="./main.ts"></script>
</body>
</html>
@@ -1,20 +0,0 @@
import '../../../style.css';
import { effects as blocksEffects } from '@blocksuite/blocks/effects';
import { PageEditor } from '@blocksuite/presets';
import { effects as presetsEffects } from '@blocksuite/presets/effects';
import { Text } from '@blocksuite/store';
import { createEmptyDoc } from '../../../apps/_common/helper';
blocksEffects();
presetsEffects();
const doc = createEmptyDoc().init();
const editor = new PageEditor();
editor.doc = doc;
document.body.append(editor);
const paragraphs = doc.getBlockByFlavour('affine:paragraph');
const paragraph = paragraphs[0];
doc.updateBlock(paragraph, { text: new Text('Hello World!') });
@@ -1,23 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Edgeless+Edgeless Multiple-Editors Example</title>
<style>
html,
body {
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
background-color: var(--affine-white-90);
transition: background-color 0.3s;
}
</style>
</head>
<body>
<script type="module" src="./main.ts"></script>
</body>
</html>
@@ -1,30 +0,0 @@
import '../../../style.css';
import { effects as blocksEffects } from '@blocksuite/blocks/effects';
import { EdgelessEditor } from '@blocksuite/presets';
import { effects as presetsEffects } from '@blocksuite/presets/effects';
import { createEmptyDoc } from '../../../apps/_common/helper';
blocksEffects();
presetsEffects();
const container = document.createElement('div');
container.style.display = 'flex';
container.style.height = '100%';
container.style.width = '100%';
document.body.append(container);
const doc1 = createEmptyDoc().init();
const editor1 = new EdgelessEditor();
editor1.doc = doc1;
editor1.style.flex = '1';
editor1.style.borderRight = '1px solid #ccc';
container.append(editor1);
const doc2 = createEmptyDoc().init();
const editor2 = new EdgelessEditor();
editor2.doc = doc2;
editor2.style.flex = '1';
editor2.style.borderLeft = '1px solid #ccc';
container.append(editor2);
@@ -1,23 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Doc+Edgeless Multiple-Editors Example</title>
<style>
html,
body {
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
background-color: var(--affine-white-90);
transition: background-color 0.3s;
}
</style>
</head>
<body>
<script type="module" src="./main.ts"></script>
</body>
</html>
@@ -1,30 +0,0 @@
import '../../../style.css';
import { effects as blocksEffects } from '@blocksuite/blocks/effects';
import { EdgelessEditor, PageEditor } from '@blocksuite/presets';
import { effects as presetsEffects } from '@blocksuite/presets/effects';
import { createEmptyDoc } from '../../../apps/_common/helper';
blocksEffects();
presetsEffects();
const container = document.createElement('div');
container.style.display = 'flex';
container.style.height = '100%';
container.style.width = '100%';
document.body.append(container);
const doc1 = createEmptyDoc().init();
const editor1 = new PageEditor();
editor1.doc = doc1;
editor1.style.flex = '2';
editor1.style.borderRight = '1px solid #ccc';
container.append(editor1);
const doc2 = createEmptyDoc().init();
const editor2 = new EdgelessEditor();
editor2.doc = doc2;
editor2.style.flex = '3';
editor2.style.borderLeft = '1px solid #ccc';
container.append(editor2);
@@ -1,23 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Doc+Doc Multiple-Editors Example</title>
<style>
html,
body {
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
background-color: var(--affine-white-90);
transition: background-color 0.3s;
}
</style>
</head>
<body>
<script type="module" src="./main.ts"></script>
</body>
</html>
@@ -1,30 +0,0 @@
import '../../../style.css';
import { effects as blocksEffects } from '@blocksuite/blocks/effects';
import { PageEditor } from '@blocksuite/presets';
import { effects as presetsEffects } from '@blocksuite/presets/effects';
import { createEmptyDoc } from '../../../apps/_common/helper';
blocksEffects();
presetsEffects();
const container = document.createElement('div');
container.style.display = 'flex';
container.style.height = '100%';
container.style.width = '100%';
document.body.append(container);
const doc1 = createEmptyDoc().init();
const editor1 = new PageEditor();
editor1.doc = doc1;
editor1.style.flex = '1';
editor1.style.borderRight = '1px solid #ccc';
container.append(editor1);
const doc2 = createEmptyDoc().init();
const editor2 = new PageEditor();
editor2.doc = doc2;
editor2.style.flex = '1';
editor2.style.borderLeft = '1px solid #ccc';
container.append(editor2);
+13
View File
@@ -44,6 +44,19 @@
margin-top: 0px;
}
}
affine-editor-container {
display: block;
height: 100%;
}
.affine-page-viewport {
height: 100%;
}
.playground-page-editor-container {
height: 100%;
}
</style>
</head>
-20
View File
@@ -99,26 +99,6 @@ export default defineConfig(({ mode }) => {
},
input: {
main: resolve(__dirname, 'index.html'),
'examples/basic/page': resolve(
__dirname,
'examples/basic/page/index.html'
),
'examples/basic/edgeless': resolve(
__dirname,
'examples/basic/edgeless/index.html'
),
'examples/multiple-editors/page-page': resolve(
__dirname,
'examples/multiple-editors/page-page/index.html'
),
'examples/multiple-editors/page-edgeless': resolve(
__dirname,
'examples/multiple-editors/page-edgeless/index.html'
),
'examples/multiple-editors/edgeless-edgeless': resolve(
__dirname,
'examples/multiple-editors/edgeless-edgeless/index.html'
),
'examples/inline': resolve(__dirname, 'examples/inline/index.html'),
},
treeshake: true,
@@ -1,104 +0,0 @@
import { BlockStdScope, ShadowlessElement } from '@blocksuite/block-std';
import { EdgelessEditorBlockSpecs, ThemeProvider } from '@blocksuite/blocks';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
import type { Store } from '@blocksuite/store';
import { css, html, nothing, type TemplateResult } from 'lit';
import { property, state } from 'lit/decorators.js';
import { guard } from 'lit/directives/guard.js';
export class EdgelessEditor extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
static override styles = css`
edgeless-editor {
font-family: var(--affine-font-family);
background: var(--affine-background-primary-color);
}
edgeless-editor * {
box-sizing: border-box;
}
@media print {
edgeless-editor {
height: auto;
}
}
.affine-edgeless-viewport {
display: block;
height: 100%;
position: relative;
overflow: clip;
container-name: viewport;
container-type: inline-size;
}
`;
get host() {
try {
return this.std.host;
} catch {
return null;
}
}
override connectedCallback() {
super.connectedCallback();
this._disposables.add(
this.doc.slots.rootAdded.on(() => this.requestUpdate())
);
this.std = new BlockStdScope({
store: this.doc,
extensions: this.specs,
});
}
override async getUpdateComplete(): Promise<boolean> {
const result = await super.getUpdateComplete();
await this.host?.updateComplete;
return result;
}
override render() {
if (!this.doc.root) return nothing;
const std = this.std;
const theme = std.get(ThemeProvider).edgeless$.value;
return html`
<div class="affine-edgeless-viewport" data-theme=${theme}>
${guard([std], () => std.render())}
</div>
`;
}
override willUpdate(
changedProperties: Map<string | number | symbol, unknown>
) {
super.willUpdate(changedProperties);
if (changedProperties.has('doc')) {
this.std = new BlockStdScope({
store: this.doc,
extensions: this.specs,
});
}
}
@property({ attribute: false })
accessor doc!: Store;
@property({ attribute: false })
accessor editor!: TemplateResult;
@property({ attribute: false })
accessor specs = EdgelessEditorBlockSpecs;
@state()
accessor std!: BlockStdScope;
}
declare global {
interface HTMLElementTagNameMap {
'edgeless-editor': EdgelessEditor;
}
}
-2
View File
@@ -1,3 +1 @@
export * from './edgeless-editor.js';
export * from './editor-container.js';
export * from './page-editor.js';
@@ -1,112 +0,0 @@
import {
BlockStdScope,
EditorHost,
ShadowlessElement,
} from '@blocksuite/block-std';
import { PageEditorBlockSpecs, ThemeProvider } from '@blocksuite/blocks';
import { noop, SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
import type { Store } from '@blocksuite/store';
import { css, html, nothing } from 'lit';
import { property, state } from 'lit/decorators.js';
import { guard } from 'lit/directives/guard.js';
noop(EditorHost);
export class PageEditor extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
static override styles = css`
page-editor {
font-family: var(--affine-font-family);
background: var(--affine-background-primary-color);
}
page-editor * {
box-sizing: border-box;
}
@media print {
page-editor {
height: auto;
}
}
.affine-page-viewport {
position: relative;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
container-name: viewport;
container-type: inline-size;
}
.page-editor-container {
display: block;
height: 100%;
}
`;
get host() {
try {
return this.std.host;
} catch {
return null;
}
}
override connectedCallback() {
super.connectedCallback();
this._disposables.add(
this.doc.slots.rootAdded.on(() => this.requestUpdate())
);
this.std = new BlockStdScope({
store: this.doc,
extensions: this.specs,
});
}
override async getUpdateComplete(): Promise<boolean> {
const result = await super.getUpdateComplete();
await this.host?.updateComplete;
return result;
}
override render() {
if (!this.doc.root) return nothing;
const std = this.std;
const theme = std.get(ThemeProvider).app$.value;
return html`
<div data-theme=${theme} class="page-editor-container">
${guard([std], () => std.render())}
</div>
`;
}
override willUpdate(
changedProperties: Map<string | number | symbol, unknown>
) {
super.willUpdate(changedProperties);
if (changedProperties.has('doc')) {
this.std = new BlockStdScope({
store: this.doc,
extensions: this.specs,
});
}
}
@property({ attribute: false })
accessor doc!: Store;
@property({ attribute: false })
accessor specs = PageEditorBlockSpecs;
@state()
accessor std!: BlockStdScope;
}
declare global {
interface HTMLElementTagNameMap {
'page-editor': PageEditor;
}
}
+1 -7
View File
@@ -1,18 +1,12 @@
import '@blocksuite/affine-shared/commands';
import '@blocksuite/blocks/effects';
import {
AffineEditorContainer,
EdgelessEditor,
PageEditor,
} from './editors/index.js';
import { AffineEditorContainer } from './editors/index.js';
import { CommentInput } from './fragments/comment/comment-input.js';
import { CommentPanel } from './fragments/index.js';
export function effects() {
customElements.define('page-editor', PageEditor);
customElements.define('comment-input', CommentInput);
customElements.define('comment-panel', CommentPanel);
customElements.define('affine-editor-container', AffineEditorContainer);
customElements.define('edgeless-editor', EdgelessEditor);
}
@@ -1,17 +1,14 @@
import type { Page } from '@playwright/test';
import { currentEditorIndex } from './multiple-editor.js';
export async function getStringFromRichText(
page: Page,
index = 0
): Promise<string> {
await page.waitForTimeout(50);
return page.evaluate(
([index, currentEditorIndex]) => {
const editorHost =
document.querySelectorAll('editor-host')[currentEditorIndex];
const richTexts = editorHost.querySelectorAll('rich-text');
([index]) => {
const editorHost = document.querySelector('editor-host');
const richTexts = editorHost?.querySelectorAll('rich-text');
if (!richTexts) {
throw new Error('Cannot find rich-text');
@@ -20,6 +17,6 @@ export async function getStringFromRichText(
const editor = (richTexts[index] as any).inlineEditor;
return editor.yText.toString();
},
[index, currentEditorIndex]
[index]
);
}