This commit is contained in:
Chris Kruining 2025-02-11 16:55:12 +11:00
parent f3069b12af
commit 487e41c2d7
No known key found for this signature in database
GPG key ID: EB894A3560CCCAD2
10 changed files with 163 additions and 69 deletions

View file

@ -0,0 +1,8 @@
const regex = /\w+\s+\w+/gi;
export function defaultChecker(subject: string, lang: string): [number, number][] {
return Array.from<RegExpExecArray>(subject.matchAll(regex)).filter(() => Math.random() >= .5).map(({ 0: match, index }) => {
return [index, index + match.length - 1];
});
}

View file

@ -0,0 +1,3 @@
export { Textarea } from './textarea';

View file

@ -0,0 +1,8 @@
const regex = /\w+/gi;
export function defaultChecker(subject: string, lang: string): [number, number][] {
return Array.from<RegExpExecArray>(subject.matchAll(regex)).filter(() => Math.random() >= .5).map(({ 0: match, index }) => {
return [index, index + match.length - 1];
});
}

View file

@ -0,0 +1,24 @@
.textarea {
/* Make sure resizing works as intended */
display: block;
overflow: clip auto;
resize: block;
white-space: wrap;
min-block-size: max(2em, 100%);
max-block-size: 50em;
unicode-bidi: plaintext;
white-space-collapse: preserve;
text-wrap-mode: wrap;
overflow-wrap: break-word;
cursor: text;
}
.spellingError {
text-decoration-line: spelling-error;
}
.grammarError {
text-decoration-line: grammar-error;
}

View file

@ -0,0 +1,80 @@
import { createEffect, createMemo, createSignal } from 'solid-js';
import { decode } from '~/utilities';
import { debounce } from '@solid-primitives/scheduled';
import { defaultChecker as spellChecker } from './spellChecker';
import { defaultChecker as grammarChecker } from './grammarChecker';
import css from './textarea.module.css';
interface TextareaProps {
class?: string;
value: string;
lang: string;
oninput?: (event: InputEvent) => any;
spellChecker?: any;
grammarChecker?: any;
}
export function Textarea(props: TextareaProps) {
const [value, setValue] = createSignal<string>(decode(props.value));
const [element, setElement] = createSignal<HTMLTextAreaElement>();
createEffect(() => {
setValue(decode(props.value));
});
const resize = () => {
const el = element();
if (!el) {
return;
}
el.style.height = `1px`;
el.style.height = `${2 + element()!.scrollHeight}px`;
};
const mutate = debounce(() => {
props.oninput?.(new InputEvent('input', {
data: value(),
}))
}, 300);
const onKeyUp = (e: KeyboardEvent) => {
e.stopPropagation();
e.preventDefault();
setValue(element()!.innerText.trim());
resize();
mutate();
return false;
};
const spellingErrors = createMemo(() => spellChecker(value(), props.lang));
const grammarErrors = createMemo(() => grammarChecker(value(), props.lang));
const html = createMemo(() => {
return value().split('').map((letter, index) => {
const spellingOpen = spellingErrors().some(([start]) => start === index) ? `<span class="${css.spellingError}">` : '';
const spellingClose = spellingErrors().some(([, end]) => end === index) ? `</span>` : '';
const grammarOpen = grammarErrors().some(([start]) => start === index) ? `<span class="${css.grammarError}">` : '';
const grammarClose = grammarErrors().some(([, end]) => end === index) ? `</span>` : '';
return `${grammarOpen}${spellingOpen}${letter}${spellingClose}${grammarClose}`;
}).join('');
});
return <div
ref={setElement}
class={`${css.textarea} ${props.class}`}
lang={props.lang}
dir="auto"
onkeyup={onKeyUp}
on:keydown={e => e.stopPropagation()}
on:pointerdown={e => e.stopPropagation()}
contentEditable
innerHTML={html()}
/>;
}