stash
This commit is contained in:
parent
fa6bf5bbac
commit
11aab1dc1a
5 changed files with 106 additions and 38 deletions
|
@ -7,13 +7,23 @@ import { createMap } from './map';
|
|||
import { splice } from "~/utilities";
|
||||
import rehypeParse from "rehype-parse";
|
||||
|
||||
type Editor = [Accessor<string>, { select(range: Range): void, mutate(setter: (text: string) => string): void }];
|
||||
type Editor = [Accessor<string>, { select(range: Range): void, mutate(setter: (text: string) => string): void, readonly selection: Accessor<Range | undefined> }];
|
||||
|
||||
interface EditorStoreType {
|
||||
text: string;
|
||||
isComposing: boolean;
|
||||
selection: Range | undefined;
|
||||
characterBounds: DOMRect[];
|
||||
controlBounds: DOMRect;
|
||||
selectionBounds: DOMRect;
|
||||
}
|
||||
|
||||
export function createEditor(ref: Accessor<Element | undefined>, value: Accessor<string>): Editor {
|
||||
if (isServer) {
|
||||
return [value, {
|
||||
select() { },
|
||||
mutate() { },
|
||||
selection: () => undefined,
|
||||
}];
|
||||
}
|
||||
|
||||
|
@ -25,9 +35,10 @@ export function createEditor(ref: Accessor<Element | undefined>, value: Accessor
|
|||
text: value(),
|
||||
});
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
const [store, setStore] = createStore<EditorStoreType>({
|
||||
text: value(),
|
||||
isComposing: false,
|
||||
selection: undefined,
|
||||
|
||||
// Bounds
|
||||
characterBounds: new Array<DOMRect>(),
|
||||
|
@ -84,6 +95,8 @@ export function createEditor(ref: Accessor<Element | undefined>, 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<Element | undefined>, value: Accessor
|
|||
mutate(setter) {
|
||||
setStore('text', setter);
|
||||
},
|
||||
|
||||
selection: createMemo(() => store.selection),
|
||||
}];
|
||||
}
|
||||
|
||||
|
|
|
@ -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<string>;
|
||||
readonly text: Accessor<string>;
|
||||
readonly selection: Accessor<Range | undefined>;
|
||||
readonly source: Source;
|
||||
select(range: Range): void;
|
||||
mutate(setter: (prev: string) => string): void;
|
||||
|
@ -19,7 +21,7 @@ interface EditorContextProps extends Record<string, unknown> {
|
|||
|
||||
const [EditorProvider, useEditor] = createContextProvider<EditorContextType, EditorContextProps>((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<EditorContextType, Edi
|
|||
select,
|
||||
mutate,
|
||||
source,
|
||||
selection,
|
||||
};
|
||||
}, { text: () => '', source: {} as Source, select() { }, mutate() { } });
|
||||
}, {
|
||||
text: () => '',
|
||||
selection: () => undefined,
|
||||
source: {} as Source,
|
||||
select() { },
|
||||
mutate() { },
|
||||
});
|
||||
|
||||
export { useEditor };
|
||||
|
||||
|
|
|
@ -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<string>): Source {
|
||||
const [store, setStore] = createStore<SourceStore>({ in: '', out: '', plain: '', query: '', metadata: { spellingErrors: [], grammarErrors: [], queryResults: [] } });
|
||||
const [store, setStore] = createStore<SourceStore>({ 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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <div class={css.root}>
|
||||
<textarea oninput={onInput} title="markdown">{value()}</textarea>
|
||||
|
||||
<Editor value={value()} oninput={setValue}>
|
||||
<SearchAndReplace />
|
||||
</Editor>
|
||||
<div class={css.editor}>
|
||||
<Editor value={value()} oninput={setValue}>
|
||||
<Toolbar />
|
||||
<SearchAndReplace />
|
||||
</Editor>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function Toolbar() {
|
||||
const { mutate, selection } = useEditor();
|
||||
|
||||
const bold = () => {
|
||||
console.log('toggle text bold', selection());
|
||||
};
|
||||
|
||||
return <div class={css.toolbar}>
|
||||
<button onclick={bold}>bold</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
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 <form class={css.search}>
|
||||
<input type="search" title="editor-search" placeholder="search for" oninput={e => source.query = e.target.value} />
|
||||
<input type="search" title="editor-replace" placeholder="replace with" oninput={e => setReplacement(e.target.value)} />
|
||||
<button onclick={() => replace()}>replace</button>
|
||||
return <form on:submit={replace} class={css.search} popover="manual">
|
||||
<label><span>Case insensitive</span><input type="checkbox" checked={caseInsensitive()} oninput={e => setCaseInsensitive(e.target.checked)} /></label>
|
||||
<label><span>Search for</span><input type="search" title="editor-search" oninput={e => setTerm(e.target.value)} /></label>
|
||||
<label><span>Replace with</span><input type="search" title="editor-replace" oninput={e => setReplacement(e.target.value)} /></label>
|
||||
|
||||
<button>replace</button>
|
||||
</form>;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue