diff --git a/packages/blocky-example/app/noTitle/noTitle.tsx b/packages/blocky-example/app/noTitle/noTitle.tsx index c54c898..2e63eeb 100644 --- a/packages/blocky-example/app/noTitle/noTitle.tsx +++ b/packages/blocky-example/app/noTitle/noTitle.tsx @@ -1,15 +1,15 @@ -import { Component, ReactNode, createRef } from "react"; +import { useRef } from "react"; import { BlockyEditor, makeReactToolbar, makeImageBlockPlugin, + useBlockyController, } from "blocky-react"; import { EditorController, IPlugin } from "blocky-core"; import ImagePlaceholder from "@pkg/components/imagePlaceholder"; import { makeCommandPanelPlugin } from "@pkg/app/plugins/commandPanel"; import { makeAtPanelPlugin } from "@pkg/app/plugins/atPanel"; import ToolbarMenu from "@pkg/app/toolbarMenu"; -import { timer, Subject, takeUntil } from "rxjs"; function makeEditorPlugins(): IPlugin[] { return [ @@ -44,35 +44,18 @@ function makeController( }); } -class NoTitleEditor extends Component { - controller: EditorController; - containerRef = createRef(); - dispose$ = new Subject(); +function NoTitleEditor() { + const containerRef = useRef(null); - constructor(props: any) { - super(props); - this.controller = makeController("user", () => this.containerRef.current!); - } + const controller = useBlockyController(() => { + return makeController("user", () => containerRef.current!); + }, []); - componentDidMount(): void { - timer(0) - .pipe(takeUntil(this.dispose$)) - .subscribe(() => { - this.controller.focus(); - }); - } - - componentWillUnmount(): void { - this.dispose$.next(); - } - - render(): ReactNode { - return ( -
- -
- ); - } + return ( +
+ +
+ ); } export default NoTitleEditor; diff --git a/packages/blocky-example/docs/api.md b/packages/blocky-example/docs/api.md index a7b8037..36b4e70 100644 --- a/packages/blocky-example/docs/api.md +++ b/packages/blocky-example/docs/api.md @@ -154,6 +154,24 @@ When the user begins to type, the content will be passed to the widget by the me As usual, there are two ways to implement a follower widget: using the raw API or using Preact. +### React + +Use the method `makePreactFollowerWidget`. + +```tsx +import { makeReactFollowerWidget } from "blocky-react"; + +editor.insertFollowerWidget( + makeReactFollowerWidget(({ controller, editingValue, closeWidget }) => ( + + )) +); +``` + ### VanillaJS Extend the class `FollowerWidget`. @@ -178,21 +196,3 @@ export class MyFollowWidget extends FollowerWidget { editor.insertFollowerWidget(new MyFollowWidget()); ``` - -### Preact - -Use the method `makePreactFollowerWidget`. - -```tsx -import { makePreactFollowerWidget } from "blocky-preact"; - -editor.insertFollowerWidget( - makePreactFollowerWidget(({ controller, editingValue, closeWidget }) => ( - - )) -); -``` diff --git a/packages/blocky-example/docs/builtin-plugins.md b/packages/blocky-example/docs/builtin-plugins.md index 751b338..4be1782 100644 --- a/packages/blocky-example/docs/builtin-plugins.md +++ b/packages/blocky-example/docs/builtin-plugins.md @@ -18,32 +18,9 @@ interface TextBlockAttributes { Builtin types: - Checkbox +- Numbered - Bulleted - Normal - Heading1 - Heading2 - Heading3 - -## Styled text plugin - -Add styles of bold/italic/underline. - -```typescript -import makeStyledTextPlugin from "blocky-core/dist/plugins/styledTextPlugin"; -``` - -## Headings plugin - -Add styles of h1/h2/h3. - -```typescript -import makeHeadingsPlugin from "blocky-core/dist/plugins/headingsPlugin"; -``` - -## Bullet list plugin - -Add commands of bullet list. - -```typescript -import makeBulletListPlugin from "blocky-core/dist/plugins/bulletListPlugin"; -``` diff --git a/packages/blocky-example/docs/faq.md b/packages/blocky-example/docs/faq.md index 4514b4c..180a14d 100644 --- a/packages/blocky-example/docs/faq.md +++ b/packages/blocky-example/docs/faq.md @@ -1 +1,9 @@ # FAQ + +## Is the Blocky editor based on other editors? + +No, the Blocky editor is written from scratch using the native DOM API. + +## Can I use the Blocky editor with Vue/Angular? + +In theory, yes. The Blocky editor provides a full VanillaJS API, and you can add your bindings to your favorite frameworks. However, official support for Vue and Angular is not currently in the plans. diff --git a/packages/blocky-example/docs/get-started.md b/packages/blocky-example/docs/get-started.md index 3119db8..ff09daa 100644 --- a/packages/blocky-example/docs/get-started.md +++ b/packages/blocky-example/docs/get-started.md @@ -71,19 +71,26 @@ function makeController(): EditorController { Pass the editor to the component. ```tsx +import { + BlockyEditor, + makeReactToolbar, + makeImageBlockPlugin, + useBlockyController, +} from "blocky-react"; import { EditorController } from "blocky-core"; -class App extends Component { - private editorController: EditorController; +function App() { + const containerRef = useRef(null); - constructor(props: {}) { - super(props); - this.editorController = makeController(); - } + const controller = useBlockyController(() => { + return makeController("user", () => containerRef.current!); + }, []); - render() { - return ; - } + return ( +
+ +
+ ); } ``` @@ -103,25 +110,50 @@ The data model in Blocky Editor is represented as an XML Document: Example: -```xml - - - - </head> - <body> - <Text /> - <Text /> - <Image src="" /> - </Text> - </body> -</document> +```json +{ + "t": "document", + "title": { + "t": "title", + "textContent": { "t": "rich-text", "ops": [] } + }, + "body": { + "t": "body", + "children": [ + /** Content */ + ] + } +} ``` -## Write a block +## Define a block You can use the plugin mechanism to extend the editor with your custom block. +### Define a block with React + +Implementing a block in Preact is more easier. + +```tsx +import { type Editor, type IPlugin } from "blocky-core"; +import { makeReactBlock, DefaultBlockOutline } from "blocky-preact"; + +export function makeMyBlockPlugin(): IPlugin { + return { + name: "plugin-name", + blocks: [ + makeReactBlock({ + name: "BlockName", + component: () => ( + <DefaultBlockOutline>Write the block in Preact</DefaultBlockOutline> + ), + }), + ], + }; +} +``` + ### VanillaJS To implement a block, you need to implement two interfaces. @@ -188,29 +220,6 @@ export function makeMyBlockPlugin(): IPlugin { } ``` -### Write a block in React - -Implementing a block in Preact is more easier. - -```tsx -import { type Editor, type IPlugin } from "blocky-core"; -import { makeReactBlock, DefaultBlockOutline } from "blocky-preact"; - -export function makeMyBlockPlugin(): IPlugin { - return { - name: "plugin-name", - blocks: [ - makeReactBlock({ - name: "BlockName", - component: () => ( - <DefaultBlockOutline>Write the block in Preact</DefaultBlockOutline> - ), - }), - ], - }; -} -``` - ### Add the plugin to the controller ```tsx diff --git a/packages/blocky-react/src/editor.tsx b/packages/blocky-react/src/editor.tsx index 7d817a1..c7d4e08 100644 --- a/packages/blocky-react/src/editor.tsx +++ b/packages/blocky-react/src/editor.tsx @@ -1,8 +1,26 @@ -import { Component, createRef, type RefObject } from "react"; -import { Editor, type EditorController, CursorState } from "blocky-core"; +import React, { useEffect, useState, useRef } from "react"; +import { Editor, EditorController, CursorState } from "blocky-core"; + +export function useBlockyController( + generator: () => EditorController, + deps?: React.DependencyList | undefined +): EditorController | null { + const [controller, setController] = useState<EditorController | null>(null); + + useEffect(() => { + const controller = generator(); + setController(controller); + + return () => { + controller.dispose(); + }; + }, deps); + + return controller; +} export interface Props { - controller: EditorController; + controller: EditorController | null; /** * If this flag is false, @@ -14,32 +32,35 @@ export interface Props { autoFocus?: boolean; } -export class BlockyEditor extends Component<Props> { - private editor: Editor | undefined; - private containerRef: RefObject<HTMLDivElement> = createRef(); +export function BlockyEditor(props: Props) { + const { controller, autoFocus, ignoreInitEmpty } = props; + const containerRef = useRef<HTMLDivElement>(null); - override componentDidMount() { - const { controller, autoFocus } = this.props; - this.editor = Editor.fromController(this.containerRef.current!, controller); - const editor = this.editor; - if (this.props.ignoreInitEmpty !== true) { + useEffect(() => { + if (!controller) { + return; + } + const editor = Editor.fromController(containerRef.current!, controller); + if (ignoreInitEmpty !== true) { editor.initFirstEmptyBlock(); } editor.fullRender(() => { if (autoFocus) { - controller.setCursorState(CursorState.collapse("title", 0)); + if (controller.state.document.title) { + controller.setCursorState(CursorState.collapse("title", 0)); + } else { + controller.focus(); + } } }); - } - override componentWillUnmount() { - this.editor?.dispose(); - this.editor = undefined; - } + return () => { + editor.dispose(); + }; + }, [controller, autoFocus, ignoreInitEmpty]); - render() { - return ( - <div className="blocky-editor-container" ref={this.containerRef}></div> - ); + if (!controller) { + return null; } + return <div className="blocky-editor-container" ref={containerRef}></div>; }