quick and dirty search and replace
This commit is contained in:
parent
fc22ce6027
commit
5f6138d30b
6 changed files with 36 additions and 83 deletions
|
@ -62,7 +62,7 @@
|
||||||
"build": "vinxi build",
|
"build": "vinxi build",
|
||||||
"start": "vinxi start",
|
"start": "vinxi start",
|
||||||
"version": "vinxi version",
|
"version": "vinxi version",
|
||||||
"test": "vitest --coverage --browser=chromium",
|
"test": "vitest --coverage",
|
||||||
"test:ci": "vitest run"
|
"test:ci": "vitest run"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -27,6 +27,10 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset-inline-end: 0;
|
inset-inline-end: 0;
|
||||||
inset-block-start: 0;
|
inset-block-start: 0;
|
||||||
|
|
||||||
|
display: block grid;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
gap: .5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions {
|
.suggestions {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { isServer } from 'solid-js/web';
|
||||||
import { createEditContext } from '~/features/editor';
|
import { createEditContext } from '~/features/editor';
|
||||||
import { createSource } from '~/features/source';
|
import { createSource } from '~/features/source';
|
||||||
import css from './textarea.module.css';
|
import css from './textarea.module.css';
|
||||||
|
import { debounce } from '@solid-primitives/scheduled';
|
||||||
|
|
||||||
interface TextareaProps {
|
interface TextareaProps {
|
||||||
class?: string;
|
class?: string;
|
||||||
|
@ -17,9 +18,11 @@ interface TextareaProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Textarea(props: TextareaProps) {
|
export function Textarea(props: TextareaProps) {
|
||||||
|
const [replacement, setReplacement] = createSignal('');
|
||||||
const [editorRef, setEditorRef] = createSignal<HTMLElement>();
|
const [editorRef, setEditorRef] = createSignal<HTMLElement>();
|
||||||
|
|
||||||
const source = createSource(() => props.value);
|
const source = createSource(() => props.value);
|
||||||
const [text] = createEditContext(editorRef, () => source.out);
|
const [text, { mutate }] = createEditContext(editorRef, () => source.out);
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
source.out = text();
|
source.out = text();
|
||||||
|
@ -41,9 +44,17 @@ export function Textarea(props: TextareaProps) {
|
||||||
createHighlights(ref, 'search-results', errors);
|
createHighlights(ref, 'search-results', errors);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const replace = () => {
|
||||||
|
mutate(text => text.replaceAll(source.query, replacement()));
|
||||||
|
};
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Suggestions />
|
<div class={css.search}>
|
||||||
<input class={css.search} type="search" title={`${props.title ?? ''}-search`} oninput={e => source.query = e.target.value} />
|
<input type="search" title={`${props.title ?? ''}-search`} placeholder="search for" oninput={e => source.query = e.target.value} />
|
||||||
|
<input type="search" title={`${props.title ?? ''}-replace`} placeholder="replace with" oninput={e => setReplacement(e.target.value)} />
|
||||||
|
<button onclick={() => replace()}>replace</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={setEditorRef}
|
ref={setEditorRef}
|
||||||
class={`${css.textarea} ${props.class}`}
|
class={`${css.textarea} ${props.class}`}
|
||||||
|
@ -58,82 +69,6 @@ export function Textarea(props: TextareaProps) {
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Suggestions: Component = () => {
|
|
||||||
const [selection] = createSelection();
|
|
||||||
const [suggestionRef, setSuggestionRef] = createSignal<HTMLElement>();
|
|
||||||
const [suggestions, setSuggestions] = createSignal<string[]>([]);
|
|
||||||
|
|
||||||
const marker = createMemo(() => {
|
|
||||||
if (isServer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [n] = selection();
|
|
||||||
const s = window.getSelection();
|
|
||||||
|
|
||||||
if (n === null || s === null || s.rangeCount < 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (findMarkerNode(s.getRangeAt(0)?.commonAncestorContainer) ?? undefined) as HTMLElement | undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
createEffect<HTMLElement | undefined>((prev) => {
|
|
||||||
if (prev) {
|
|
||||||
prev.style.setProperty('anchor-name', null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const m = marker();
|
|
||||||
const ref = untrack(() => suggestionRef()!);
|
|
||||||
|
|
||||||
if (m === undefined) {
|
|
||||||
ref.hidePopover();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m.style.setProperty('anchor-name', '--suggestions');
|
|
||||||
ref.showPopover();
|
|
||||||
ref.focus()
|
|
||||||
|
|
||||||
return m;
|
|
||||||
});
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
marker();
|
|
||||||
|
|
||||||
setSuggestions(Array(Math.ceil(Math.random() * 5)).fill('').map((_, i) => `suggestion ${i}`));
|
|
||||||
});
|
|
||||||
|
|
||||||
const onPointerDown = (e: PointerEvent) => {
|
|
||||||
marker()?.replaceWith(document.createTextNode(e.target.textContent));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <menu ref={setSuggestionRef} class={css.suggestions} popover="manual" onkeydown={onKeyDown}>
|
|
||||||
<For each={suggestions()}>{
|
|
||||||
suggestion => <li onpointerdown={onPointerDown}>{suggestion}</li>
|
|
||||||
}</For>
|
|
||||||
</menu>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const findMarkerNode = (node: Node | null) => {
|
|
||||||
while (node !== null) {
|
|
||||||
if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).hasAttribute('data-marker')) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
node = node.parentNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
return node;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const createHighlights = (node: Node, type: string, ranges: [number, number][]) => {
|
const createHighlights = (node: Node, type: string, ranges: [number, number][]) => {
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
const nodes = getTextNodes(node);
|
const nodes = getTextNodes(node);
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 21 KiB |
|
@ -7,11 +7,14 @@ import { createMap } from './map';
|
||||||
import { splice } from "~/utilities";
|
import { splice } from "~/utilities";
|
||||||
import rehypeParse from "rehype-parse";
|
import rehypeParse from "rehype-parse";
|
||||||
|
|
||||||
type Editor = [Accessor<string>];
|
type Editor = [Accessor<string>, { select(range: Range): void, mutate(setter: (text: string) => string): void }];
|
||||||
|
|
||||||
export function createEditor(ref: Accessor<Element | undefined>, value: Accessor<string>): Editor {
|
export function createEditor(ref: Accessor<Element | undefined>, value: Accessor<string>): Editor {
|
||||||
if (isServer) {
|
if (isServer) {
|
||||||
return [value];
|
return [value, {
|
||||||
|
select(range) { },
|
||||||
|
mutate() { },
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!("EditContext" in window)) {
|
if (!("EditContext" in window)) {
|
||||||
|
@ -187,7 +190,17 @@ export function createEditor(ref: Accessor<Element | undefined>, value: Accessor
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return [createMemo(() => store.text)];
|
return [
|
||||||
|
createMemo(() => store.text),
|
||||||
|
{
|
||||||
|
select(range: Range) {
|
||||||
|
updateSelection(range);
|
||||||
|
},
|
||||||
|
|
||||||
|
mutate(setter) {
|
||||||
|
setStore('text', setter);
|
||||||
|
},
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
const equals = (a: Range, b: Range): boolean => {
|
const equals = (a: Range, b: Range): boolean => {
|
||||||
|
|
|
@ -43,6 +43,7 @@ function reportWith(...reporter: CoverageReporter[]): Plugin {
|
||||||
provider: 'playwright',
|
provider: 'playwright',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
headless: true,
|
headless: true,
|
||||||
|
screenshotFailures: false,
|
||||||
instances: [{ browser: 'chromium' }]
|
instances: [{ browser: 'chromium' }]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue