Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: react adapters #6490

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
471 changes: 97 additions & 374 deletions examples/pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions examples/pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
packages:
- ./*
- '!./react-custom'
18 changes: 18 additions & 0 deletions examples/react-custom/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
};
24 changes: 24 additions & 0 deletions examples/react-custom/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
15 changes: 15 additions & 0 deletions examples/react-custom/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# React Basic Example

This example encapsulates the BlockSuite editor and doc collection in React in a more customized way.

## Development

```sh
git clone https://github.com/toeverything/blocksuite.git
cd blocksuite/examples

pnpm install
pnpm react-custom
```

This project is created using the `pnpm create vite` cli.
12 changes: 12 additions & 0 deletions examples/react-custom/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BlockSuite Example</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
39 changes: 39 additions & 0 deletions examples/react-custom/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "react-custom",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@blocksuite/react": "workspace:*",
"@blocksuite/blocks": "workspace:*",
"@blocksuite/store": "workspace:*",
"@blocksuite/presets": "workspace:*",
"@blocksuite/block-std": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"lit": "^3.1.2",
"@types/react": "^18.2.56",
"@types/react-dom": "^18.2.19",
"@types/sql.js": "^1.4.9",
"@typescript-eslint/eslint-plugin": "^7.0.2",
"@typescript-eslint/parser": "^7.0.2",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"typescript": "^5.2.2",
"vite": "^5.1.4"
},
"stackblitz": {
"installDependencies": false,
"startCommand": "pnpm i && pnpm dev"
}
}
67 changes: 67 additions & 0 deletions examples/react-custom/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useCallback, useMemo, useState } from 'react';
import {
appStateContext,
createDocCollection,
initEmptyPage,
useActiveDoc,
useSetAppState,
} from './data';
import './index.css';

import '@blocksuite/presets/themes/affine.css';
import { EditorContainer } from './editor-container';
import { Sidebar } from './sidebar';

const AppStateProvider = ({ children }: { children: React.ReactNode }) => {
const docCollection = useMemo(() => {
return createDocCollection();
}, []);
const startPage = useMemo(() => {
return initEmptyPage(docCollection, 'page1');
}, [docCollection]);
return (
<appStateContext.Provider
value={useState(() => ({
activeDocId: startPage.id,
docCollection: docCollection,
}))}
>
{children}
</appStateContext.Provider>
);
};

function EditorWrapper() {
const doc = useActiveDoc();
const updateState = useSetAppState();

const onDocChange = useCallback(
(docId: string) => {
updateState(state => {
return {
...state,
activeDocId: docId,
};
});
},
[updateState]
);

if (!doc) {
return null;
}
return <EditorContainer doc={doc} onDocChange={onDocChange} />;
}

function App() {
return (
<AppStateProvider>
<div className="root">
<Sidebar />
<EditorWrapper />
</div>
</AppStateProvider>
);
}

export default App;
55 changes: 55 additions & 0 deletions examples/react-custom/src/custom-page-reference.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { AffineReference } from '@blocksuite/blocks';
import { useState, useEffect } from 'react';

// This function generates a random color in hex format
const generateRandomColor = () => {
return '#' + Math.floor(Math.random() * 16777215).toString(16);
};

// Custom hook that changes color smoothly over time
const useRandomColor = (interval = 1000) => {
const [color, setColor] = useState(generateRandomColor());

useEffect(() => {
const colorInterval = setInterval(() => {
setColor(generateRandomColor());
}, interval);

return () => clearInterval(colorInterval);
}, [interval]);

return color;
};

export const CustomPageReference = ({
reference,
}: {
reference: AffineReference;
}) => {
const [title, setTitle] = useState(reference.title);

useEffect(() => {
const refId = reference.delta.attributes?.reference?.pageId;
if (refId) {
const t =
reference.doc.collection.meta.docMetas.find(m => m.id === refId)
?.title ?? 'title';
setTitle(t);
}
}, [
reference.delta.attributes?.reference?.pageId,
reference.doc.collection.meta.docMetas,
]);

return (
<span
style={{
backgroundColor: useRandomColor(1000),
transition: 'background-color 1s',
padding: '0 0.5em',
}}
>
📜 {title}
</span>
);
};
52 changes: 52 additions & 0 deletions examples/react-custom/src/custom-specs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { BlockSpec } from '@blocksuite/block-std';
import { AffineReference, ParagraphService } from '@blocksuite/blocks';
import { ElementOrFactory } from '@blocksuite/react';
import type { TemplateResult } from 'lit';
import { CustomPageReference } from './custom-page-reference';
import React from 'react';

type PageReferenceRenderer = (reference: AffineReference) => React.ReactElement;

function patchSpecsWithReferenceRenderer(
specs: BlockSpec<string>[],
pageReferenceRenderer: PageReferenceRenderer,
toLitTemplate: (element: ElementOrFactory) => TemplateResult
) {
const renderer = (reference: AffineReference) => {
const node = pageReferenceRenderer(reference);
return toLitTemplate(node);
};
return specs.map(spec => {
if (
['affine:paragraph', 'affine:list', 'affine:database'].includes(
spec.schema.model.flavour
)
) {
// todo: remove these type assertions
spec.service = class extends (spec.service as typeof ParagraphService) {
override mounted() {
super.mounted();
this.referenceNodeConfig.setCustomContent(renderer);
}
};
}

return spec;
});
}

export function patchSpecs(
specs: BlockSpec<string>[],
toLitTemplate: (element: ElementOrFactory) => TemplateResult
) {
let newSpecs = specs;
newSpecs = patchSpecsWithReferenceRenderer(
newSpecs,
(reference: AffineReference) =>
React.createElement(CustomPageReference, {
reference,
}),
toLitTemplate
);
return newSpecs;
}
64 changes: 64 additions & 0 deletions examples/react-custom/src/data.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { AffineSchemas } from '@blocksuite/blocks';
import { Schema, DocCollection, Doc } from '@blocksuite/store';
import { useState, useEffect, createContext, useContext } from 'react';

interface AppState {
docCollection: DocCollection;
activeDocId: string;
}

export const appStateContext = createContext<
[AppState, React.Dispatch<React.SetStateAction<AppState>>]
// eslint-disable-next-line @typescript-eslint/no-explicit-any
>([] as any);

export const createDocCollection = () => {
const schema = new Schema().register(AffineSchemas);
const collection = new DocCollection({ schema });
return collection;
};

export const initEmptyPage = (docCollection: DocCollection, pageId: string) => {
const doc = docCollection.createDoc({ id: pageId });
doc.load(() => {
const pageBlockId = doc.addBlock('affine:page', {});
doc.addBlock('affine:surface', {}, pageBlockId);
const noteId = doc.addBlock('affine:note', {}, pageBlockId);
doc.addBlock('affine:paragraph', {}, noteId);
});
return doc;
};

export const useAppStateValue = () => {
const [state] = useContext(appStateContext);
return state;
};

export const useSetAppState = () => {
const [, setState] = useContext(appStateContext);
return setState;
};

export const useAppState = () => {
return useContext(appStateContext);
};

export const useDocs = () => {
const [docs, setDocs] = useState<Doc[]>([]);
const docCollection = useAppStateValue().docCollection;
useEffect(() => {
const updateDocs = () => {
setDocs([...docCollection.docs.values()]);
};
updateDocs();
const disposable = [docCollection.slots.docUpdated.on(updateDocs)];
return () => disposable.forEach(d => d.dispose());
}, [docCollection.docs, docCollection.slots.docUpdated]);
return docs;
};

export const useActiveDoc = () => {
const { activeDocId } = useAppStateValue();
const docs = useDocs();
return docs.find(d => d.id === activeDocId);
};
Loading
Loading