calque/src/routes/(editor)/experimental/editor.tsx
Chris Kruining b1e617e74a
stash
2025-03-12 16:50:49 +01:00

128 lines
No EOL
4.1 KiB
TypeScript

import { createEffect, createMemo, createSignal, untrack } from "solid-js";
import { debounce } from "@solid-primitives/scheduled";
import { Editor, Index_Range, splitBy, createElement, useEditor, mergeNodes } from "~/features/editor";
import { visitParents } from "unist-util-visit-parents";
import type * as hast from 'hast';
import css from './editor.module.css';
const tempVal = `
# Header
this is **a string** that contains bolded text
this is *a string* that contains italicized text
> Dorothy followed her through many of the beautiful rooms in her castle.
> #### The quarterly results look great!
>
> - Revenue was off the chart.
> - Profits were higher than ever.
>
> > The Witch bade her clean the pots and kettles and sweep the floor and keep the fire fed with wood.
>
> *Everything* is going according to **plan**.
- First item
- Second item
- Third item
- Fourth item
`;
export default function Formatter(props: {}) {
const [value, setValue] = createSignal(tempVal);
const onInput = debounce((e: InputEvent) => {
setValue((e.target! as HTMLTextAreaElement).value);
}, 300);
return <div class={css.root}>
<textarea oninput={onInput} title="markdown">{value()}</textarea>
<div class={css.editor}>
<Editor value={untrack(value)} oninput={setValue}>
<Toolbar />
<SearchAndReplace />
</Editor>
</div>
</div>;
}
function Toolbar() {
const { mutate, selection } = useEditor();
const trimWhitespaceOn = ({ startNode: startContainer, endNode: endContainer, startOffset, endOffset, ...rest }: Index_Range): Index_Range => {
const matchStart = startContainer.value.slice(startOffset).match(/^(\s+).*?$/);
const matchEnd = endContainer.value.slice(0, endOffset).match(/^.*?(\s+)$/);
return {
startNode: startContainer,
startOffset: startOffset + (matchStart?.[1].length ?? 0),
endNode: endContainer,
endOffset: endOffset - (matchEnd?.[1].length ?? 0),
...rest
};
};
const bold = () => {
const range = selection();
if (!range) {
return;
}
mutate((ast) => {
const { startNode, endNode, startOffset, endOffset, commonAncestor } = trimWhitespaceOn(range);
const [left, toBold, right] = splitBy(commonAncestor(), [
{ node: startNode, offset: startOffset },
{ node: endNode, offset: endOffset },
]);
console.log(left, toBold, right);
const boldedElement = createElement('strong', toBold.flatMap(child => child.tagName === 'strong' ? mergeNodes(child.children) : child)) as hast.RootContent;
// THIS IS WHERE I LEFT OFF
// AST needs to be clean!!!!
commonAncestor().children = [...left, boldedElement, ...right];
return ast;
});
};
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 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 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>;
};