diff --git a/src/lib/richtext/editor-dom.tsx b/src/lib/richtext/editor-dom.tsx index f7ac17e..157d696 100644 --- a/src/lib/richtext/editor-dom.tsx +++ b/src/lib/richtext/editor-dom.tsx @@ -1,16 +1,23 @@ "use dom"; -import { useCallback, useState } from "react"; +import { Ref, useCallback, useState } from "react"; import { StyleSheet } from "react-native"; -import { createEditor, Editor, Path, Selection } from "slate"; +import { createEditor, Editor, Path, Selection, Transforms } from "slate"; import { withHistory } from "slate-history"; import { Editable, ReactEditor, RenderElementProps, + RenderLeafProps, Slate, withReact, } from "slate-react"; +import { + DOMImperativeFactory, + DOMProps, + useDOMImperativeHandle, +} from "expo/dom"; +import { JSONValue } from "expo/build/dom/dom.types"; const formatStyles = { default: { @@ -59,21 +66,30 @@ function ParagraphElement(props: RenderElementProps) { ); } -function isEmptySelection(selection: Selection) { - return ( - selection?.anchor === undefined || - (Path.compare(selection?.anchor.path, selection.focus.path) === 0 && - selection.anchor.offset == selection.focus.offset) - ); +export interface DOMEditorRef extends DOMImperativeFactory { + toggleStyle: (style: JSONValue) => void; } export function DOMEditor({ onMoveToolbar, + ref, }: { onMoveToolbar: (pos?: DOMRect) => void; + ref?: Ref; + dom?: DOMProps; }) { const [editor] = useState(() => withReact(withHistory(createEditor()))); + ref && + useDOMImperativeHandle(ref, () => { + return { + toggleStyle: (style) => { + const set = (Editor.marks(editor) ?? {} as any)[style as string] as (boolean | undefined) ?? false; + Editor.addMark(editor, style as string, !set); + }, + }; + }); + const initialValue = [ { type: "paragraph", @@ -86,20 +102,33 @@ export function DOMEditor({ case "paragraph": return ; default: - return <>; + return {props.children}; } }, []); - const selectionChange = (selection: Selection) => { - const empty = isEmptySelection(selection); - if (empty) { - onMoveToolbar(undefined); + const renderLeaf = useCallback((props: RenderLeafProps) => { + const leaf = props.leaf as any; + return ( + + {props.children} + + ); + }, []); + + const selectionChange = () => { + const selection = window.getSelection() + if (selection && selection.rangeCount > 0 && !selection.isCollapsed) { + onMoveToolbar(selection.getRangeAt(0).getBoundingClientRect()) } else { - const node = ReactEditor.toDOMNode( - editor, - editor.node(selection!.anchor)[0], - ); - onMoveToolbar(node.getBoundingClientRect()); + onMoveToolbar(undefined) } }; @@ -111,7 +140,8 @@ export function DOMEditor({ > onMoveToolbar(undefined)} + renderLeaf={renderLeaf} + onBlur={selectionChange} /> ); diff --git a/src/lib/richtext/editor.tsx b/src/lib/richtext/editor.tsx index b6c28ce..6b989df 100644 --- a/src/lib/richtext/editor.tsx +++ b/src/lib/richtext/editor.tsx @@ -1,6 +1,6 @@ import { Pressable, StyleSheet, View, ViewProps } from "react-native"; -import { DOMEditor } from "./editor-dom"; -import { useState } from "react"; +import { DOMEditor, DOMEditorRef } from "./editor-dom"; +import { useRef, useState } from "react"; import FontAwesome from "@expo/vector-icons/FontAwesome"; const styles = StyleSheet.create({ @@ -35,13 +35,13 @@ function ToolbarButton({ ); } -function Toolbar({ pos }: { pos: [number, number] }) { +function Toolbar({ pos, toggleStyle }: { pos: [number, number]; toggleStyle: (style: string) => void }) { return ( - - - - + toggleStyle('bold')} /> + toggleStyle('italic')} /> + toggleStyle('strike')} /> + toggleStyle('small')} /> ); } @@ -51,15 +51,21 @@ export function RichTextEditor({ minHeight, ...props }: ViewProps & { minHeight?: number }) { + const editor = useRef(null); const [pos, setPos] = useState<[number, number] | undefined>(undefined); + const onMoveToolbar = (rect?: DOMRect) => { setPos(rect ? [rect.left, rect.top - rect.height - 20] : undefined); }; + const toggleStyle = (style: string) => { + editor.current?.toggleStyle(style) + } + return ( - - {pos && } + + {pos && } ); }