diff --git a/ivette/src/dome/renderer/text/richtext.tsx b/ivette/src/dome/renderer/text/richtext.tsx index 69482245025062a22ee68d5848836768e56b8fc0..29786da13d65a618986691e14318c7cbf8af1ca4 100644 --- a/ivette/src/dome/renderer/text/richtext.tsx +++ b/ivette/src/dome/renderer/text/richtext.tsx @@ -34,6 +34,9 @@ export interface Range { offset: number; length: number } export interface Position { offset: number; line: number } export interface Selection extends Range { fromLine: number, toLine: number } +export const empty : Range & Selection = + { offset: 0, length: 0, fromLine: 0, toLine: 0 }; + export function byDepth(a : Range, b : Range): number { return (a.length - b.length) || (b.offset - a.offset); @@ -312,14 +315,46 @@ OnChange.pack( } )); +/* -------------------------------------------------------------------------- */ +/* --- Selection Change Listener --- */ +/* -------------------------------------------------------------------------- */ + +export type SelectionCallback = (S: Selection) => void; + +const OnSelect = new Field<SelectionCallback|null>(null); + +OnSelect.pack( + CM.EditorView.updateListener.computeN( + [OnSelect.field], + (state) => { + const callback = state.field(OnSelect.field); + if (callback !== null) + return [ + (updates: CM.ViewUpdate) => { + const oldSel = updates.startState.selection.main; + const newSel = updates.state.selection.main; + const doc = updates.state.doc; + if (!newSel.eq(oldSel)) { + const { from: offset, to: endOffset } = newSel; + const fromLine = doc.lineAt(offset).number; + const toLine = doc.lineAt(endOffset).number; + callback({ + offset, length: endOffset - offset, + fromLine, toLine, + }); + } + }]; + return []; + } +)); + /* -------------------------------------------------------------------------- */ /* --- Editor View --- */ /* -------------------------------------------------------------------------- */ function createView(parent: Element): CM.EditorView { const extensions : CS.Extension[] = [ - ReadOnly, - OnChange, + ReadOnly, OnChange, OnSelect, ]; const state = CS.EditorState.create({ extensions }); return new CM.EditorView({ state, parent }); @@ -333,6 +368,8 @@ export interface RichTextProps { text?: TextProxy; readOnly?: boolean; onChange?: Callback; + selection?: Range; + onSelection?: SelectionCallback; display?: boolean; visible?: boolean; className?: string; @@ -352,10 +389,24 @@ export function TextView(props: RichTextProps) : JSX.Element { return undefined; }, [text, view]); - // ---- readOnly, onChange - const { readOnly = false, onChange = null } = props; + // ---- readOnly, onChange, onSelection + const { + readOnly = false, onChange = null, + onSelection: onSelect = null, + } = props; React.useEffect(() => ReadOnly.dispatch(view, readOnly), [view, readOnly]); React.useEffect(() => OnChange.dispatch(view, onChange), [view, onChange]); + React.useEffect(() => OnSelect.dispatch(view, onSelect), [view, onSelect]); + + // ---- Selection + const { selection } = props; + React.useEffect(() => { + if (selection) { + const anchor = selection.offset; + const head = anchor + selection.length; + view?.dispatch({ scrollIntoView: true, selection: { anchor, head } }); + } + }, [view, selection]); // ---- Mount & Unmount Editor const [nodeRef, setRef] = React.useState<Element | null>(null); diff --git a/ivette/src/sandbox/text.tsx b/ivette/src/sandbox/text.tsx index 68135a5d1c2782f23e8dfa550fc6824abd5b8cd1..dad9ec8cfa28bc94c9b292919e9a5ed45f06e87a 100644 --- a/ivette/src/sandbox/text.tsx +++ b/ivette/src/sandbox/text.tsx @@ -30,7 +30,7 @@ import * as Dome from 'dome'; import { ToolBar, Filler } from 'dome/frame/toolbars'; import { Code } from 'dome/controls/labels'; import { Button } from 'dome/controls/buttons'; -import { TextView, TextProxy, TextBuffer } from 'dome/text/richtext'; +import { TextView, TextProxy, TextBuffer, empty, } from 'dome/text/richtext'; import { registerSandbox } from 'ivette'; /* -------------------------------------------------------------------------- */ @@ -40,8 +40,9 @@ import { registerSandbox } from 'ivette'; function UseText(): JSX.Element { const [prefix, setPrefix] = React.useState(''); const [readOnly, flipReadOnly] = Dome.useFlipState(false); - const [useProxy, flipUseProxy] = Dome.useFlipState(true); + const [useProxy, flipUseProxy] = Dome.useFlipState(false); const [changes, setChanges] = React.useState(0); + const [s, onSelection] = React.useState(empty); const proxy = React.useMemo(() => new TextProxy(), []); const buffer = React.useMemo(() => new TextBuffer(), []); const text = useProxy ? proxy : buffer; @@ -68,6 +69,8 @@ function UseText(): JSX.Element { title={useProxy ? 'Use TextProxy' : 'Use TextBuffer (persistent)'} onClick={flipUseProxy} /> + <Code label={`Offset ${s.offset}-${s.offset + s.length}`} /> + <Code label={`Line ${s.fromLine}-${s.toLine}`} /> <Filler /> <Code>{`"${prefix}" (${changes})`}</Code> <Button label="Push" onClick={push} /> @@ -77,6 +80,7 @@ function UseText(): JSX.Element { text={text} readOnly={readOnly} onChange={onChange} + onSelection={onSelection} /> </> );