From 11aab1dc1a3597b35978046afe28eaf9818a1016 Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Mon, 3 Mar 2025 15:58:53 +0100 Subject: [PATCH] stash --- src/features/editor/context.ts | 19 ++++++- src/features/editor/editor.tsx | 17 +++++-- src/features/source/source.ts | 18 +++---- .../(editor)/experimental/editor.module.css | 40 +++++++++++---- src/routes/(editor)/experimental/editor.tsx | 50 +++++++++++++++---- 5 files changed, 106 insertions(+), 38 deletions(-) diff --git a/src/features/editor/context.ts b/src/features/editor/context.ts index 772483a..53af3dd 100644 --- a/src/features/editor/context.ts +++ b/src/features/editor/context.ts @@ -7,13 +7,23 @@ import { createMap } from './map'; import { splice } from "~/utilities"; import rehypeParse from "rehype-parse"; -type Editor = [Accessor, { select(range: Range): void, mutate(setter: (text: string) => string): void }]; +type Editor = [Accessor, { select(range: Range): void, mutate(setter: (text: string) => string): void, readonly selection: Accessor }]; + +interface EditorStoreType { + text: string; + isComposing: boolean; + selection: Range | undefined; + characterBounds: DOMRect[]; + controlBounds: DOMRect; + selectionBounds: DOMRect; +} export function createEditor(ref: Accessor, value: Accessor): Editor { if (isServer) { return [value, { select() { }, mutate() { }, + selection: () => undefined, }]; } @@ -25,9 +35,10 @@ export function createEditor(ref: Accessor, value: Accessor text: value(), }); - const [store, setStore] = createStore({ + const [store, setStore] = createStore({ text: value(), isComposing: false, + selection: undefined, // Bounds characterBounds: new Array(), @@ -84,6 +95,8 @@ export function createEditor(ref: Accessor, value: Accessor context.updateSelection(...indexMap.toHtmlIndices(range)); context.updateSelectionBounds(range.getBoundingClientRect()); + setStore('selection', range); + queueMicrotask(() => { const selection = window.getSelection(); @@ -198,6 +211,8 @@ export function createEditor(ref: Accessor, value: Accessor mutate(setter) { setStore('text', setter); }, + + selection: createMemo(() => store.selection), }]; } diff --git a/src/features/editor/editor.tsx b/src/features/editor/editor.tsx index 48117e0..c14be9f 100644 --- a/src/features/editor/editor.tsx +++ b/src/features/editor/editor.tsx @@ -1,11 +1,13 @@ import { createContextProvider } from "@solid-primitives/context"; -import { Accessor, createEffect, createSignal, on, ParentProps, Setter } from "solid-js"; +import { Accessor, createEffect, createMemo, createSignal, on, ParentProps, Setter } from "solid-js"; import { createEditor } from "./context"; import { createSource, Source } from "../source"; import { getTextNodes } from "@solid-primitives/selection"; +import { isServer } from "solid-js/web"; interface EditorContextType { - text: Accessor; + readonly text: Accessor; + readonly selection: Accessor; readonly source: Source; select(range: Range): void; mutate(setter: (prev: string) => string): void; @@ -19,7 +21,7 @@ interface EditorContextProps extends Record { const [EditorProvider, useEditor] = createContextProvider((props) => { const source = createSource(() => props.value); - const [text, { select, mutate }] = createEditor(props.ref, () => source.out); + const [text, { select, mutate, selection }] = createEditor(props.ref, () => source.out); createEffect(() => { props.oninput?.(source.in); @@ -46,8 +48,15 @@ const [EditorProvider, useEditor] = createContextProvider '', source: {} as Source, select() { }, mutate() { } }); +}, { + text: () => '', + selection: () => undefined, + source: {} as Source, + select() { }, + mutate() { }, +}); export { useEditor }; diff --git a/src/features/source/source.ts b/src/features/source/source.ts index 94c906a..40c16cf 100644 --- a/src/features/source/source.ts +++ b/src/features/source/source.ts @@ -1,4 +1,4 @@ -import { Accessor, createEffect } from "solid-js"; +import { Accessor, createEffect, createMemo } from "solid-js"; import { createStore } from "solid-js/store"; import { unified } from 'unified' import { visit } from "unist-util-visit"; @@ -17,7 +17,7 @@ interface SourceStore { in: string; out: string; plain: string; - query: string; + query: RegExp; metadata: { spellingErrors: [number, number][]; grammarErrors: [number, number][]; @@ -28,7 +28,7 @@ interface SourceStore { export interface Source { in: string; out: string; - query: string; + query: RegExp; readonly spellingErrors: [number, number][]; readonly grammarErrors: [number, number][]; readonly queryResults: [number, number][]; @@ -39,7 +39,7 @@ const inToOutProcessor = unified().use(remarkParse).use(remarkRehype).use(rehype const outToInProcessor = unified().use(isServer ? rehypeParse : rehypeDomParse).use(rehypeRemark).use(remarkStringify, { bullet: '-' }); export function createSource(value: Accessor): Source { - const [store, setStore] = createStore({ in: '', out: '', plain: '', query: '', metadata: { spellingErrors: [], grammarErrors: [], queryResults: [] } }); + const [store, setStore] = createStore({ in: '', out: '', plain: '', query: new RegExp('', 'gi'), metadata: { spellingErrors: [], grammarErrors: [], queryResults: [] } }); const src: Source = { get in() { @@ -121,13 +121,9 @@ function plainTextStringify() { }; } -function findMatches(text: string, query: string): [number, number][] { - if (query.length < 1) { - return []; - } - - return text.matchAll(new RegExp(query, 'gi')).map<[number, number]>(({ index }) => { - return [index, index + query.length]; +function findMatches(text: string, query: RegExp): [number, number][] { + return text.matchAll(query).map<[number, number]>(({ 0: match, index }) => { + return [index, index + match.length]; }).toArray(); } diff --git a/src/routes/(editor)/experimental/editor.module.css b/src/routes/(editor)/experimental/editor.module.css index 213165f..f756d0b 100644 --- a/src/routes/(editor)/experimental/editor.module.css +++ b/src/routes/(editor)/experimental/editor.module.css @@ -32,19 +32,37 @@ text-decoration-line: grammar-error; } - .search { - position: absolute; - inset-inline-end: 0; - inset-block-start: 0; - + .editor { display: block grid; - grid-auto-flow: row; + grid: auto 1fr / 100%; - padding: .5em; - gap: .5em; + .toolbar { + display: block grid; + grid-auto-flow: column; + place-content: start; + } - background-color: var(--surface-700); - border-radius: var(--radii-m); - box-shadow: var(--shadow-2); + .search { + position: absolute; + inset-inline-end: 0; + inset-block-start: 0; + + grid-template-columns: 1fr 1fr; + + padding: .5em; + gap: .5em; + + background-color: var(--surface-700); + border-radius: var(--radii-m); + box-shadow: var(--shadow-2); + + &:popover-open { + display: block grid; + } + + & > label { + display: contents; + } + } } } \ No newline at end of file diff --git a/src/routes/(editor)/experimental/editor.tsx b/src/routes/(editor)/experimental/editor.tsx index 25e2090..28d2a33 100644 --- a/src/routes/(editor)/experimental/editor.tsx +++ b/src/routes/(editor)/experimental/editor.tsx @@ -1,4 +1,4 @@ -import { createSignal } from "solid-js"; +import { createEffect, createMemo, createSignal, onMount } from "solid-js"; import { debounce } from "@solid-primitives/scheduled"; import { Editor, useEditor } from "~/features/editor"; import css from './editor.module.css'; @@ -37,23 +37,53 @@ export default function Formatter(props: {}) { return
- - - +
+ + + + +
; } +function Toolbar() { + const { mutate, selection } = useEditor(); + + const bold = () => { + console.log('toggle text bold', selection()); + }; + + return
+ +
+} + function SearchAndReplace() { const { mutate, source } = useEditor(); const [replacement, setReplacement] = createSignal(''); + const [term, setTerm] = createSignal(''); + const [caseInsensitive, setCaseInsensitive] = createSignal(true); - const replace = () => { - mutate(text => text.replaceAll(source.query, replacement())); + const query = createMemo(() => new RegExp(term(), caseInsensitive() ? 'gi' : 'g')); + + createEffect(() => { + source.query = query(); + }); + + const replace = (e: SubmitEvent) => { + e.preventDefault(); + + const form = e.target as HTMLFormElement; + form.reset(); + + mutate(text => text.replaceAll(query(), replacement())); }; - return
- source.query = e.target.value} /> - setReplacement(e.target.value)} /> - + return + + + + +
; }; \ No newline at end of file