oooh wow, I overcomplicated this sooooooooo much. just stick to dom manipulations.
This commit is contained in:
parent
e88d727d8e
commit
5a813627ea
11 changed files with 146 additions and 325 deletions
|
@ -1,5 +1,8 @@
|
|||
// @refresh reload
|
||||
import { mount, StartClient } from "@solidjs/start/client";
|
||||
import { installIntoGlobal } from "iterator-helpers-polyfill";
|
||||
import 'solid-devtools';
|
||||
|
||||
installIntoGlobal();
|
||||
|
||||
mount(() => <StartClient />, document.body);
|
||||
|
|
|
@ -13,17 +13,21 @@ interface SplitPoint {
|
|||
export const splitBy = (tree: Parent, splitPoints: SplitPoint[]): RootContent[][] => {
|
||||
const result: RootContent[][] = [];
|
||||
let remaining: RootContent[] = Object.hasOwn(tree, 'children') ? (tree as Parent).children : [];
|
||||
|
||||
// console.log(Object.groupBy(splitPoints, p => hash(p.node)));
|
||||
let lastNode;
|
||||
let accumulatedOffset = 0;
|
||||
|
||||
for (const { node, offset } of splitPoints) {
|
||||
if (lastNode !== node) {
|
||||
accumulatedOffset = 0;
|
||||
}
|
||||
|
||||
const index = remaining.findIndex(c => find(c, n => equals(n, node)));
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error('The tree does not contain the given node');
|
||||
}
|
||||
|
||||
const [targetLeft, targetRight] = splitNode(remaining[index], node, offset);
|
||||
const [targetLeft, targetRight] = splitNode(remaining[index], node, offset - accumulatedOffset);
|
||||
|
||||
const left = remaining.slice(0, index);
|
||||
const right = remaining.slice(index + 1);
|
||||
|
@ -38,6 +42,9 @@ export const splitBy = (tree: Parent, splitPoints: SplitPoint[]): RootContent[][
|
|||
|
||||
remaining = right;
|
||||
result.push(left);
|
||||
|
||||
lastNode = node;
|
||||
accumulatedOffset += offset;
|
||||
}
|
||||
|
||||
result.push(remaining);
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
import { createEventListenerMap, DocumentEventListener, WindowEventListener } from "@solid-primitives/event-listener";
|
||||
import { Accessor, createEffect, createMemo, onMount, untrack } from "solid-js";
|
||||
import { Accessor, createEffect, createMemo, createSignal, on, onCleanup, onMount } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { isServer } from "solid-js/web";
|
||||
import { createMap } from './map';
|
||||
import { lazy, splice } from "~/utilities";
|
||||
import { createState } from "./state";
|
||||
import type { Parent, Root, Text } from 'hast';
|
||||
import findAncestor from "unist-util-ancestor";
|
||||
import { unified } from "unified";
|
||||
import rehypeParse from "rehype-parse";
|
||||
|
||||
export type SelectFunction = (range: Range) => void;
|
||||
export type MutateFunction = (setter: (ast: Root) => Root) => void;
|
||||
type Editor = [Accessor<string>, { select: SelectFunction, mutate: MutateFunction, readonly selection: Accessor<Index_Range | undefined> }];
|
||||
type Editor = { select: SelectFunction, readonly selection: Accessor<Range | undefined> };
|
||||
|
||||
interface EditorStoreType {
|
||||
isComposing: boolean;
|
||||
|
@ -20,22 +17,12 @@ interface EditorStoreType {
|
|||
selectionBounds: DOMRect;
|
||||
}
|
||||
|
||||
export interface Index_Range {
|
||||
startNode: Text;
|
||||
startOffset: number;
|
||||
endNode: Text;
|
||||
endOffset: number;
|
||||
|
||||
commonAncestor: () => Parent;
|
||||
}
|
||||
|
||||
export function createEditor(ref: Accessor<Element | undefined>, value: Accessor<string>): Editor {
|
||||
if (isServer) {
|
||||
return [value, {
|
||||
return {
|
||||
select() { },
|
||||
mutate() { },
|
||||
selection: () => undefined,
|
||||
}];
|
||||
};
|
||||
}
|
||||
|
||||
if (!("EditContext" in window)) {
|
||||
|
@ -46,8 +33,9 @@ export function createEditor(ref: Accessor<Element | undefined>, value: Accessor
|
|||
text: value(),
|
||||
});
|
||||
|
||||
const state = createState(value);
|
||||
const indexMap = createMap(() => ref()!, () => state.ast);
|
||||
const mutations = observe(ref);
|
||||
const ast = createMemo(() => parse(value()));
|
||||
const indexMap = createMap(ref, ast);
|
||||
const [store, setStore] = createStore<EditorStoreType>({
|
||||
isComposing: false,
|
||||
selection: undefined,
|
||||
|
@ -58,13 +46,30 @@ export function createEditor(ref: Accessor<Element | undefined>, value: Accessor
|
|||
selectionBounds: new DOMRect(),
|
||||
});
|
||||
|
||||
createEffect(on(mutations, () => {
|
||||
const selection = store.selection;
|
||||
|
||||
if (selection === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
queueMicrotask(() => {
|
||||
console.log(selection);
|
||||
|
||||
updateSelection(selection);
|
||||
});
|
||||
}));
|
||||
|
||||
createEventListenerMap<any>(context, {
|
||||
textupdate(e: TextUpdateEvent) {
|
||||
const { updateRangeStart: start, updateRangeEnd: end, text } = e;
|
||||
const selection = store.selection;
|
||||
|
||||
setStore('text', `${store.text.slice(0, start)}${text}${store.text.slice(end)}`);
|
||||
if (!selection) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.updateSelection(start + text.length, start + text.length);
|
||||
selection.insertNode(document.createTextNode(e.text));
|
||||
selection.collapse();
|
||||
},
|
||||
|
||||
compositionstart() {
|
||||
|
@ -88,20 +93,20 @@ export function createEditor(ref: Accessor<Element | undefined>, value: Accessor
|
|||
},
|
||||
});
|
||||
|
||||
function updateText(start: number, end: number, text: string) {
|
||||
context.updateText(start, end, text);
|
||||
|
||||
state.text = splice(state.text, start, end, text);
|
||||
|
||||
// context.updateSelection(start + text.length, start + text.length);
|
||||
}
|
||||
|
||||
function updateControlBounds() {
|
||||
context.updateControlBounds(ref()!.getBoundingClientRect());
|
||||
}
|
||||
|
||||
function updateSelection(range: Range) {
|
||||
context.updateSelection(...indexMap.toHtmlIndices(range));
|
||||
const [start, end] = indexMap.query(range);
|
||||
|
||||
console.log(start, end, range);
|
||||
|
||||
if (!start || !end) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.updateSelection(start.start + range.startOffset, end.start + range.endOffset);
|
||||
context.updateSelectionBounds(range.getBoundingClientRect());
|
||||
|
||||
setStore('selection', range);
|
||||
|
@ -158,6 +163,8 @@ export function createEditor(ref: Accessor<Element | undefined>, value: Accessor
|
|||
// keyCode === 229 is a special code that indicates an IME event.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event#keydown_events_with_ime
|
||||
if (e.keyCode === 229) {
|
||||
console.log(e);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -167,9 +174,9 @@ export function createEditor(ref: Accessor<Element | undefined>, value: Accessor
|
|||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
|
||||
updateText(start, end, ' ');
|
||||
context.updateText(start, end, ' ');
|
||||
} else if (e.key === 'Enter') {
|
||||
updateText(start, end, '</p><p> ');
|
||||
context.updateText(start, end, '</p><p> ');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -177,7 +184,8 @@ export function createEditor(ref: Accessor<Element | undefined>, value: Accessor
|
|||
onMount(() => {
|
||||
updateControlBounds();
|
||||
|
||||
updateSelection(indexMap.fromHtmlIndices(40, 60))
|
||||
// updateSelection(indexMap.fromHtmlIndices(40, 60))
|
||||
// updateSelection(indexMap.fromHtmlIndices(599, 603))
|
||||
});
|
||||
|
||||
createEffect((last?: Element) => {
|
||||
|
@ -196,70 +204,43 @@ export function createEditor(ref: Accessor<Element | undefined>, value: Accessor
|
|||
return el;
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
return {
|
||||
select(range: Range) {
|
||||
updateSelection(range);
|
||||
},
|
||||
|
||||
selection: createMemo<Range | undefined>(() => {
|
||||
return store.selection;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const observe = (node: Accessor<Node | undefined>): Accessor<readonly [Node | undefined, MutationRecord[]]> => {
|
||||
const [mutations, setMutations] = createSignal<MutationRecord[]>([]);
|
||||
|
||||
const observer = new MutationObserver(records => {
|
||||
setMutations(records);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
updateText(0, -0, value());
|
||||
});
|
||||
const n = node();
|
||||
|
||||
createEffect(() => {
|
||||
state.text;
|
||||
observer.disconnect();
|
||||
|
||||
if (document.activeElement === untrack(ref)) {
|
||||
queueMicrotask(() => {
|
||||
updateSelection(indexMap.fromHtmlIndices(context.selectionStart, context.selectionEnd));
|
||||
});
|
||||
if (n) {
|
||||
observer.observe(n, { characterData: true, subtree: true, childList: true });
|
||||
}
|
||||
});
|
||||
|
||||
return [
|
||||
createMemo(() => state.text),
|
||||
{
|
||||
select(range: Range) {
|
||||
updateSelection(range);
|
||||
},
|
||||
onCleanup(() => {
|
||||
observer.disconnect();
|
||||
});
|
||||
|
||||
mutate(setter) {
|
||||
const [start, end] = indexMap.toTextIndices(store.selection!);
|
||||
return createMemo(() => [node(), mutations()] as const);
|
||||
};
|
||||
|
||||
state.ast = setter(state.ast);
|
||||
|
||||
setTimeout(() => {
|
||||
console.log('RESTORING SELECTION')
|
||||
const range = indexMap.fromTextIndices(start, end);
|
||||
|
||||
console.log(start, end, range);
|
||||
|
||||
updateSelection(range);
|
||||
}, 100);
|
||||
},
|
||||
|
||||
selection: createMemo<Index_Range | undefined>(() => {
|
||||
const selection = store.selection;
|
||||
|
||||
if (!selection) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [start, end] = indexMap.query(selection);
|
||||
|
||||
if (!start || !end) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
startNode: start.node,
|
||||
startOffset: selection.startOffset,
|
||||
|
||||
endNode: end.node,
|
||||
endOffset: selection.endOffset,
|
||||
|
||||
commonAncestor: lazy(() => findAncestor(untrack(() => state.ast), [start.node, end.node]) as Parent),
|
||||
}
|
||||
}),
|
||||
}];
|
||||
}
|
||||
const parseProcessor = unified().use(rehypeParse)
|
||||
const parse = (text: string) => parseProcessor.parse(text);
|
||||
|
||||
const equals = (a: Range, b: Range): boolean => {
|
||||
const keys: (keyof Range)[] = ['startOffset', 'endOffset', 'commonAncestorContainer', 'startContainer', 'endContainer'];
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import { createContextProvider } from "@solid-primitives/context";
|
||||
import { Accessor, createEffect, createSignal, on, ParentProps, Setter } from "solid-js";
|
||||
import { createEditor, Index_Range, MutateFunction, SelectFunction } from "./context";
|
||||
import { Accessor, createEffect, createMemo, createSignal, on, ParentProps, Setter } from "solid-js";
|
||||
import { createEditor, SelectFunction } from "./context";
|
||||
import { createSource, Source } from "../source";
|
||||
import { getTextNodes } from "@solid-primitives/selection";
|
||||
|
||||
interface EditorContextType {
|
||||
readonly text: Accessor<string>;
|
||||
readonly selection: Accessor<Index_Range | undefined>;
|
||||
readonly selection: Accessor<Range | undefined>;
|
||||
readonly source: Source;
|
||||
select: SelectFunction;
|
||||
mutate: MutateFunction;
|
||||
}
|
||||
|
||||
interface EditorContextProps extends Record<string, unknown> {
|
||||
|
@ -20,16 +19,12 @@ interface EditorContextProps extends Record<string, unknown> {
|
|||
|
||||
const [EditorProvider, useEditor] = createContextProvider<EditorContextType, EditorContextProps>((props) => {
|
||||
const source = createSource(() => props.value);
|
||||
const [text, { select, mutate, selection }] = createEditor(props.ref, () => source.out);
|
||||
const { select, selection } = createEditor(props.ref, () => source.out);
|
||||
|
||||
createEffect(() => {
|
||||
props.oninput?.(source.in);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
source.out = text();
|
||||
});
|
||||
|
||||
createEffect(on(() => [props.ref()!, source.spellingErrors] as const, ([ref, errors]) => {
|
||||
createHighlights(ref, 'spelling-error', errors);
|
||||
}));
|
||||
|
@ -43,9 +38,8 @@ const [EditorProvider, useEditor] = createContextProvider<EditorContextType, Edi
|
|||
}));
|
||||
|
||||
return {
|
||||
text,
|
||||
text: createMemo(() => source.out),
|
||||
select,
|
||||
mutate,
|
||||
source,
|
||||
selection,
|
||||
};
|
||||
|
@ -54,7 +48,6 @@ const [EditorProvider, useEditor] = createContextProvider<EditorContextType, Edi
|
|||
selection: () => undefined,
|
||||
source: {} as Source,
|
||||
select() { },
|
||||
mutate() { },
|
||||
});
|
||||
|
||||
export { useEditor };
|
||||
|
@ -72,6 +65,12 @@ export function Editor(props: ParentProps<{ value: string, oninput?: (value: str
|
|||
function Content(props: { ref: Setter<Element | undefined> }) {
|
||||
const { text } = useEditor();
|
||||
|
||||
createEffect(() => {
|
||||
text();
|
||||
|
||||
console.error('rerendering');
|
||||
});
|
||||
|
||||
return <div ref={props.ref} innerHTML={text()} />;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
|
||||
|
||||
export type { Index_Range } from './context';
|
||||
export { createEditor as createEditContext } from './context';
|
||||
export { Editor, useEditor } from './editor';
|
||||
export { splitBy, createElement, mergeNodes } from './ast';
|
|
@ -7,159 +7,37 @@ export type IndexNode = { node: Text, dom: Node, text: { start: number, end: num
|
|||
export type IndexMap = IndexNode[];
|
||||
export type IndexRange = [IndexNode, IndexNode] | [undefined, undefined];
|
||||
|
||||
|
||||
export function createMap(root: Accessor<Element | undefined>, ast: Accessor<Root>) {
|
||||
// Observe the element so that the references to the nodes in the indices are updated if the DOM is changed
|
||||
const latestMutations = observe(root);
|
||||
|
||||
const indices = createMemo(() => {
|
||||
const [node] = latestMutations();
|
||||
const mapping = createMemo(() => {
|
||||
const node = root();
|
||||
const tree = ast();
|
||||
|
||||
if (node === undefined) {
|
||||
return [];
|
||||
return new WeakMap();
|
||||
}
|
||||
|
||||
return createIndices(node, ast());
|
||||
console.warn('recalculating map');
|
||||
|
||||
return createMapping(node, tree);
|
||||
});
|
||||
|
||||
return {
|
||||
query(range: Range): [IndexNode | undefined, IndexNode | undefined] {
|
||||
query: (range: Range) => {
|
||||
return [
|
||||
indices().find(({ dom }) => dom === range.startContainer),
|
||||
indices().find(({ dom }) => dom === range.endContainer),
|
||||
mapping().get(range.startContainer),
|
||||
mapping().get(range.endContainer),
|
||||
];
|
||||
},
|
||||
|
||||
atHtmlPosition(start: number, end: number): IndexRange {
|
||||
const startNode = indices().find(({ html }) => html.start <= start && html.end >= start);
|
||||
const endNode = indices().find(({ html }) => html.start <= end && html.end >= end);
|
||||
|
||||
if (!startNode || !endNode) {
|
||||
return [undefined, undefined];
|
||||
}
|
||||
|
||||
return [startNode, endNode];
|
||||
},
|
||||
|
||||
atTextPosition(start: number, end: number): IndexRange {
|
||||
const startNode = indices().find(({ text }) => text.start <= start && text.end >= start);
|
||||
const endNode = indices().find(({ text }) => text.start <= end && text.end >= end);
|
||||
|
||||
if (!startNode || !endNode) {
|
||||
return [undefined, undefined];
|
||||
}
|
||||
|
||||
return [startNode, endNode];
|
||||
},
|
||||
|
||||
toTextIndices(range: Range): [number, number] {
|
||||
const [startNode, endNode] = this.query(range);
|
||||
|
||||
return [
|
||||
startNode ? (startNode.text.start + range.startOffset) : -1,
|
||||
endNode ? (endNode.text.start + range.endOffset) : -1
|
||||
];
|
||||
},
|
||||
|
||||
toHtmlIndices(range: Range): [number, number] {
|
||||
const [startNode, endNode] = this.query(range);
|
||||
|
||||
return [
|
||||
startNode ? (startNode.html.start + range.startOffset) : -1,
|
||||
endNode ? (endNode.html.start + range.endOffset) : -1
|
||||
];
|
||||
},
|
||||
|
||||
fromTextIndices(start: number, end: number): Range {
|
||||
const [startNode, endNode] = this.atTextPosition(start, end);
|
||||
const range = new Range();
|
||||
|
||||
if (startNode) {
|
||||
const offset = start - startNode.text.start;
|
||||
|
||||
range.setStart(startNode.dom, offset);
|
||||
}
|
||||
|
||||
if (endNode) {
|
||||
const offset = end - endNode.text.start;
|
||||
|
||||
console.log('end offset', endNode);
|
||||
|
||||
range.setEnd(endNode.dom, offset);
|
||||
}
|
||||
|
||||
return range;
|
||||
},
|
||||
|
||||
fromHtmlIndices(start: number, end: number): Range {
|
||||
const [startNode, endNode] = this.atHtmlPosition(start, end);
|
||||
const range = new Range();
|
||||
|
||||
if (startNode) {
|
||||
const offset = start - startNode.html.start;
|
||||
|
||||
range.setStart(startNode.dom, offset);
|
||||
}
|
||||
|
||||
if (endNode) {
|
||||
const offset = end - endNode.html.start;
|
||||
|
||||
range.setEnd(endNode.dom, offset);
|
||||
}
|
||||
|
||||
return range;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const createIndices = (root: Node, ast: Root): IndexMap => {
|
||||
const createMapping = (root: Node, ast: Root): WeakMap<Node, { start: number, end: number }> => {
|
||||
const nodes = getTextNodes(root);
|
||||
const indices: IndexMap = [];
|
||||
const map = new WeakMap();
|
||||
|
||||
console.log(ast);
|
||||
|
||||
let index = 0;
|
||||
visit(ast, (n): n is Text => n.type === 'text', (node) => {
|
||||
const { position, value } = node as Text;
|
||||
const end = index + value.length;
|
||||
const dom = nodes.shift()!;
|
||||
|
||||
console.log({ value, text: dom?.textContent, dom });
|
||||
|
||||
// if (value.includes('ntains bolded text')) {
|
||||
// console.log(value, dom.textContent, { node, dom });
|
||||
// }
|
||||
|
||||
if (position) {
|
||||
indices.push({ node, dom, text: { start: index, end }, html: { start: position.start.offset!, end: position.end.offset! } });
|
||||
}
|
||||
|
||||
index = end;
|
||||
map.set(nodes.shift()!, { start: node.position!.start.offset, end: node.position!.end.offset, text: node.value })
|
||||
});
|
||||
|
||||
return indices;
|
||||
};
|
||||
|
||||
const observe = (node: Accessor<Node | undefined>): Accessor<readonly [Node | undefined, MutationRecord[]]> => {
|
||||
const [mutations, setMutations] = createSignal<MutationRecord[]>([]);
|
||||
|
||||
const observer = new MutationObserver(records => {
|
||||
setMutations(records);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const n = node();
|
||||
|
||||
observer.disconnect();
|
||||
|
||||
if (n) {
|
||||
observer.observe(n, { characterData: true, subtree: true, childList: true });
|
||||
}
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
observer.disconnect();
|
||||
});
|
||||
|
||||
return createMemo(() => [node(), mutations()] as const);
|
||||
return map;
|
||||
};
|
|
@ -1,41 +0,0 @@
|
|||
import rehypeParse from "rehype-parse";
|
||||
import rehypeStringify from "rehype-stringify";
|
||||
import { Accessor, createSignal } from "solid-js";
|
||||
import { unified } from "unified";
|
||||
import type { Root } from 'hast';
|
||||
|
||||
export interface State {
|
||||
text: string;
|
||||
ast: Root;
|
||||
}
|
||||
|
||||
export const createState = (value: Accessor<string>): State => {
|
||||
const [text, setText] = createSignal(value());
|
||||
const [ast, setAst] = createSignal(parse(value()));
|
||||
|
||||
return {
|
||||
get text() {
|
||||
return text();
|
||||
},
|
||||
|
||||
set text(next: string) {
|
||||
setText(next);
|
||||
setAst(parse(next));
|
||||
},
|
||||
|
||||
get ast() {
|
||||
return ast();
|
||||
},
|
||||
|
||||
set ast(next: Root) {
|
||||
setText(stringify(next));
|
||||
setAst(next);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const stringifyProcessor = unified().use(rehypeStringify)
|
||||
const parseProcessor = unified().use(rehypeParse)
|
||||
|
||||
const stringify = (root: Root) => stringifyProcessor.stringify(root);
|
||||
const parse = (text: string) => parseProcessor.parse(text);
|
|
@ -2,11 +2,8 @@ import { Accessor, createEffect, from, createSignal } from "solid-js";
|
|||
import { json } from "./parser";
|
||||
import { filter } from "~/utilities";
|
||||
import { isServer } from "solid-js/web";
|
||||
import { installIntoGlobal } from 'iterator-helpers-polyfill';
|
||||
import { debounce } from "@solid-primitives/scheduled";
|
||||
|
||||
installIntoGlobal();
|
||||
|
||||
interface Files extends Record<string, { handle: FileSystemFileHandle, file: File }> { }
|
||||
interface Contents extends Map<string, Map<string, string>> { }
|
||||
|
||||
|
|
|
@ -20,6 +20,11 @@
|
|||
background-color: transparent;
|
||||
}
|
||||
|
||||
& ::highlight(debug) {
|
||||
text-decoration: double underline;
|
||||
text-decoration-color: cornflowerblue;
|
||||
}
|
||||
|
||||
& ::highlight(search-results) {
|
||||
background-color: var(--secondary-900);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { createEffect, createMemo, createSignal, untrack } from "solid-js";
|
||||
import { createEffect, createMemo, createSignal, onMount, 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 { Editor, useEditor } from "~/features/editor";
|
||||
import css from './editor.module.css';
|
||||
import { assert } from "~/utilities";
|
||||
|
||||
const tempVal = `
|
||||
# Header
|
||||
|
@ -49,49 +48,39 @@ export default function Formatter(props: {}) {
|
|||
}
|
||||
|
||||
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();
|
||||
const range = window.getSelection()!.getRangeAt(0);
|
||||
// const { startContainer, startOffset, endContainer, endOffset, commonAncestorContainer } = range;
|
||||
// console.log(startContainer, startOffset, endContainer, endOffset, commonAncestorContainer);
|
||||
|
||||
if (!range) {
|
||||
if (range.startContainer.nodeType !== Node.TEXT_NODE) {
|
||||
return;
|
||||
}
|
||||
|
||||
mutate((ast) => {
|
||||
const { startNode, endNode, startOffset, endOffset, commonAncestor } = trimWhitespaceOn(range);
|
||||
if (range.endContainer.nodeType !== Node.TEXT_NODE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [left, toBold, right] = splitBy(commonAncestor(), [
|
||||
{ node: startNode, offset: startOffset },
|
||||
{ node: endNode, offset: endOffset },
|
||||
]);
|
||||
const fragment = range.extractContents();
|
||||
|
||||
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;
|
||||
});
|
||||
if (range.startContainer === range.commonAncestorContainer && range.endContainer === range.commonAncestorContainer && range.commonAncestorContainer.parentElement?.tagName === 'STRONG') {
|
||||
range.selectNode(range.commonAncestorContainer.parentElement);
|
||||
range.insertNode(fragment);
|
||||
}
|
||||
else {
|
||||
const strong = document.createElement('strong');
|
||||
strong.append(fragment);
|
||||
|
||||
range.insertNode(strong);
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
queueMicrotask(() => {
|
||||
// bold();
|
||||
});
|
||||
});
|
||||
|
||||
return <div class={css.toolbar}>
|
||||
<button onclick={bold}>bold</button>
|
||||
</div>;
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
export const assert = (assertion: boolean, message: string) => {
|
||||
if (assertion !== true) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
export const splice = (subject: string, start: number, end: number, replacement: string) => {
|
||||
return `${subject.slice(0, start)}${replacement}${Object.is(end, -0) ? '' : subject.slice(end)}`;
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue