diff --git a/app/package-lock.json b/app/package-lock.json index 8fec273..2da2aa5 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -11,6 +11,7 @@ "@exampledev/new.css": "^1.1.3", "@fastify/http-proxy": "^8.2.2", "@lexical/history": "^0.4.1", + "@lexical/link": "^0.4.1", "@lexical/rich-text": "^0.4.1", "@tsconfig/node18-strictest-esm": "^1.0.1", "esbuild": "^0.15.7", @@ -138,6 +139,17 @@ "lexical": "0.4.1" } }, + "node_modules/@lexical/link": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.4.1.tgz", + "integrity": "sha512-566lQymmuBe3Y7UDyaaTs+VDlElbu1WhnjT9lVDk0BXag7MA8tv/f60XptWnTK1pv/Dobm/CyLmyLae55OuflQ==", + "dependencies": { + "@lexical/utils": "0.4.1" + }, + "peerDependencies": { + "lexical": "0.4.1" + } + }, "node_modules/@lexical/list": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.4.1.tgz", @@ -1199,6 +1211,14 @@ "@lexical/selection": "0.4.1" } }, + "@lexical/link": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.4.1.tgz", + "integrity": "sha512-566lQymmuBe3Y7UDyaaTs+VDlElbu1WhnjT9lVDk0BXag7MA8tv/f60XptWnTK1pv/Dobm/CyLmyLae55OuflQ==", + "requires": { + "@lexical/utils": "0.4.1" + } + }, "@lexical/list": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.4.1.tgz", diff --git a/app/package.json b/app/package.json index 702e862..6ff0bb3 100644 --- a/app/package.json +++ b/app/package.json @@ -14,6 +14,7 @@ "@exampledev/new.css": "^1.1.3", "@fastify/http-proxy": "^8.2.2", "@lexical/history": "^0.4.1", + "@lexical/link": "^0.4.1", "@lexical/rich-text": "^0.4.1", "@tsconfig/node18-strictest-esm": "^1.0.1", "esbuild": "^0.15.7", diff --git a/app/views/components/editor.tsx b/app/views/components/editor.tsx index a0c8ddc..2e18d2d 100644 --- a/app/views/components/editor.tsx +++ b/app/views/components/editor.tsx @@ -11,11 +11,13 @@ import { HeadingNode, registerRichText, } from "@lexical/rich-text"; +import { $createAutoLinkNode, AutoLinkNode } from "@lexical/link"; import { onCleanup, onMount } from "solid-js"; import type Pages from "../../protocol/pages"; import "./editor.css"; -const editor = createEditor({ nodes: [HeadingNode] }); +const urlMatcher = /https?:\/\/[^\s]+/; +const editor = createEditor({ nodes: [HeadingNode, AutoLinkNode] }); function ref(el: HTMLElement) { editor.setRootElement(el); @@ -29,12 +31,26 @@ export default (props: { }) => { const initialEditorState = () => { const root = $getRoot(); - for (const [i, text] of props.text.split("\n").entries()) { - const line = i === 0 ? $createHeadingNode("h2") : $createParagraphNode(); - const indent = text.match(/^\s*/)?.[0]?.length ?? 0; - line.setIndent(indent); - line.append($createTextNode(text.slice(indent))); - root.append(line); + const [title, ...lines] = props.text.split("\n"); + const titleNode = $createHeadingNode("h2"); + titleNode.append($createTextNode(title)); + root.append(titleNode); + for (const line of lines) { + const lineNode = $createParagraphNode(); + const indent = line.match(/^\s*/)?.[0]?.length ?? 0; + lineNode.setIndent(indent); + let text = line.slice(indent); + let match: RegExpMatchArray | null = null; + while ((match = text.match(urlMatcher))) { + const offset = text.slice(0, match.index!); + const input = match[0]!; + const link = $createAutoLinkNode(input); + link.append($createTextNode(match[0])); + lineNode.append($createTextNode(offset), link); + text = text.slice(offset.length + input.length); + } + lineNode.append($createTextNode(text)); + root.append(lineNode); } }; onCleanup(registerRichText(editor, initialEditorState)); @@ -64,5 +80,17 @@ export default (props: { ) ); onMount(() => editor.focus()); - return
; + return ( +
{ + const el = e.target.parentElement; + if (el instanceof HTMLAnchorElement) { + window.open(el.href, "_blank", "noreferrer"); + } + }} + class="editor" + contenteditable + /> + ); };