got started on a dedicated editor component
This commit is contained in:
parent
44549c36be
commit
925ea142fb
7 changed files with 165 additions and 78 deletions
|
@ -12,7 +12,7 @@ type Editor = [Accessor<string>, { select(range: Range): void, mutate(setter: (t
|
|||
export function createEditor(ref: Accessor<Element | undefined>, value: Accessor<string>): Editor {
|
||||
if (isServer) {
|
||||
return [value, {
|
||||
select(range) { },
|
||||
select() { },
|
||||
mutate() { },
|
||||
}];
|
||||
}
|
||||
|
@ -101,8 +101,6 @@ export function createEditor(ref: Accessor<Element | undefined>, value: Accessor
|
|||
selection.removeAllRanges();
|
||||
}
|
||||
|
||||
console.log('is it me?');
|
||||
|
||||
selection.addRange(range);
|
||||
});
|
||||
}
|
||||
|
@ -149,7 +147,7 @@ export function createEditor(ref: Accessor<Element | undefined>, value: Accessor
|
|||
|
||||
updateText(start, end, ' ');
|
||||
} else if (e.key === 'Enter') {
|
||||
updateText(start, end, '\n');
|
||||
updateText(start, end, '</p><p> ');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -270,7 +268,7 @@ declare global {
|
|||
|
||||
interface EditContextConstructor {
|
||||
new(): EditContext;
|
||||
new(options: Partial<{ text: string, selectionStart: number, selectionEnd: number }>): EditContext;
|
||||
new(options: Partial<Pick<EditContext, 'text' | 'selectionStart' | 'selectionEnd'>>): EditContext;
|
||||
readonly prototype: EditContext;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,20 +1,101 @@
|
|||
import { createContextProvider } from "@solid-primitives/context";
|
||||
import { createEffect, ParentProps } from "solid-js";
|
||||
import { Accessor, createEffect, createSignal, on, ParentProps, Setter } from "solid-js";
|
||||
import { createEditor } from "./context";
|
||||
import { createSource, Source } from "../source";
|
||||
import { getTextNodes } from "@solid-primitives/selection";
|
||||
|
||||
interface EditorContextType {
|
||||
text: Accessor<string>;
|
||||
readonly source: Source;
|
||||
select(range: Range): void;
|
||||
mutate(setter: (prev: string) => string): void;
|
||||
}
|
||||
|
||||
const [EditorProvider, useEditor] = createContextProvider((props: { ref: Element, value: string }) => {
|
||||
const [text] = createEditor(() => props.ref, () => props.value);
|
||||
interface EditorContextProps extends Record<string, unknown> {
|
||||
ref: Accessor<Element | undefined>;
|
||||
value: string;
|
||||
oninput?: (value: string) => void;
|
||||
}
|
||||
|
||||
const [EditorProvider, useEditor] = createContextProvider<EditorContextType, EditorContextProps>((props) => {
|
||||
const source = createSource(() => props.value);
|
||||
const [text, { select, mutate }] = createEditor(props.ref, () => source.out);
|
||||
|
||||
createEffect(() => {
|
||||
console.log(text());
|
||||
props.oninput?.(source.in);
|
||||
});
|
||||
|
||||
return { text };
|
||||
});
|
||||
createEffect(() => {
|
||||
source.out = text();
|
||||
});
|
||||
|
||||
createEffect(on(() => [props.ref()!, source.spellingErrors] as const, ([ref, errors]) => {
|
||||
createHighlights(ref, 'spelling-error', errors);
|
||||
}));
|
||||
|
||||
createEffect(on(() => [props.ref()!, source.grammarErrors] as const, ([ref, errors]) => {
|
||||
createHighlights(ref, 'grammar-error', errors);
|
||||
}));
|
||||
|
||||
createEffect(on(() => [props.ref()!, source.queryResults] as const, ([ref, results]) => {
|
||||
createHighlights(ref, 'search-results', results);
|
||||
}));
|
||||
|
||||
return {
|
||||
text,
|
||||
select,
|
||||
mutate,
|
||||
source,
|
||||
};
|
||||
}, { text: () => '', source: {} as Source, select() { }, mutate() { } });
|
||||
|
||||
export { useEditor };
|
||||
|
||||
export function Editor(props: ParentProps<{ ref: Element, value: string }>) {
|
||||
return <EditorProvider ref={props.ref} value={props.value}>{props.children}</EditorProvider>;
|
||||
}
|
||||
export function Editor(props: ParentProps<{ value: string, oninput?: (value: string) => void }>) {
|
||||
const [ref, setRef] = createSignal<Element>();
|
||||
|
||||
return <EditorProvider ref={ref} value={props.value} oninput={props.oninput}>
|
||||
{props.children}
|
||||
|
||||
<Content ref={setRef} />
|
||||
</EditorProvider>;
|
||||
}
|
||||
|
||||
function Content(props: { ref: Setter<Element | undefined> }) {
|
||||
const { text } = useEditor();
|
||||
|
||||
return <div ref={props.ref} innerHTML={text()} />;
|
||||
}
|
||||
|
||||
const createHighlights = (node: Node, type: string, indices: [number, number][]) => {
|
||||
queueMicrotask(() => {
|
||||
const nodes = getTextNodes(node);
|
||||
|
||||
CSS.highlights.set(type, new Highlight(...indices.map(([start, end]) => indicesToRange(start, end, nodes))));
|
||||
});
|
||||
};
|
||||
|
||||
const indicesToRange = (start: number, end: number, textNodes: Node[]) => {
|
||||
const [startNode, startPos] = getRangeArgs(start, textNodes);
|
||||
const [endNode, endPos] = start === end ? [startNode, startPos] : getRangeArgs(end, textNodes);
|
||||
|
||||
const range = new Range();
|
||||
|
||||
if (startNode && endNode && startPos !== -1 && endPos !== -1) {
|
||||
range.setStart(startNode, startPos);
|
||||
range.setEnd(endNode, endPos);
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
const getRangeArgs = (offset: number, texts: Node[]): [node: Node | null, offset: number] =>
|
||||
texts.reduce(
|
||||
([node, pos], text) =>
|
||||
node
|
||||
? [node, pos]
|
||||
: pos <= (text as Text).data.length
|
||||
? [text, pos]
|
||||
: [null, pos - (text as Text).data.length],
|
||||
[null, offset] as [node: Node | null, pos: number],
|
||||
);
|
|
@ -121,26 +121,14 @@ function plainTextStringify() {
|
|||
};
|
||||
}
|
||||
|
||||
function* findMatches(text: string, query: string): Generator<[number, number], void, unknown> {
|
||||
function findMatches(text: string, query: string): [number, number][] {
|
||||
if (query.length < 1) {
|
||||
return;
|
||||
return [];
|
||||
}
|
||||
|
||||
let startIndex = 0;
|
||||
|
||||
while (startIndex < text.length) {
|
||||
const index = text.indexOf(query, startIndex);
|
||||
|
||||
if (index === -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
const end = index + query.length;
|
||||
|
||||
yield [index, end];
|
||||
|
||||
startIndex = end;
|
||||
}
|
||||
return text.matchAll(new RegExp(query, 'gi')).map<[number, number]>(({ index }) => {
|
||||
return [index, index + query.length];
|
||||
}).toArray();
|
||||
}
|
||||
|
||||
const spellChecker = checker(/\w+/gi);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue