-
- <a href="https://...">Link</a> <b>bold</b>
-
+
or paste or drop
@@ -287,7 +286,7 @@
{/if}
-
+ /> -->
+
+
{#if isUploading}
diff --git a/src/lib/components/Tiptap.svelte b/src/lib/components/Tiptap.svelte
new file mode 100644
index 0000000..3e0615f
--- /dev/null
+++ b/src/lib/components/Tiptap.svelte
@@ -0,0 +1,91 @@
+
+
+{#if editor}
+
+
+
+
+
+{/if}
+
+
From a063531e44fa8d7e28400c553f2994a5f4ef339a Mon Sep 17 00:00:00 2001
From: Travis Spomer <16262858+TravisSpomer@users.noreply.github.com>
Date: Sun, 15 Dec 2024 19:15:40 -0800
Subject: [PATCH 2/9] Getting closer
---
src/lib/components/Editor.svelte | 131 +++++++++++++++++++++++------
src/lib/components/PostView.svelte | 4 +-
src/lib/components/Tiptap.svelte | 91 --------------------
3 files changed, 109 insertions(+), 117 deletions(-)
delete mode 100644 src/lib/components/Tiptap.svelte
diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte
index 44e090b..ab20242 100644
--- a/src/lib/components/Editor.svelte
+++ b/src/lib/components/Editor.svelte
@@ -3,11 +3,16 @@
import { onMount, onDestroy, createEventDispatcher } from "svelte"
import { fly } from "svelte/transition"
import { throttle } from "@travisspomer/tidbits"
+ import { Editor } from "@tiptap/core"
+ import BulletList from "@tiptap/extension-bullet-list"
+ import Link from "@tiptap/extension-link"
+ import Placeholder from "@tiptap/extension-placeholder"
+ import StarterKit from "@tiptap/starter-kit"
+ import Typography from "@tiptap/extension-typography"
import { navigating } from "$app/stores"
import { uploadImage } from "$lib/sdk"
import Button from "./Button.svelte"
import FocusWithin from "./FocusWithin.svelte"
- import Tiptap from "./Tiptap.svelte"
import Upload from "./Upload.svelte"
import Wait from "./Wait.svelte"
@@ -32,7 +37,8 @@
export let sitewideUniqueID: string | undefined = undefined
const dispatch = createEventDispatcher()
- let textarea: HTMLTextAreaElement
+ let element: HTMLDivElement
+ let editor: Editor
let upload: Upload
let isUploading: boolean = false
@@ -62,36 +68,86 @@
throttledSaveDraft()
}
+ onMount(() =>
+ {
+ editor = new Editor({
+ element: element,
+ extensions: [
+ StarterKit.configure({
+ // We include the bulleted list extension manually later so we can configure it.
+ bulletList: false,
+ }),
+ BulletList.extend({
+ addKeyboardShortcuts()
+ {
+ return {
+ "Mod-.": () => this.editor.chain().focus().toggleBulletList().run(),
+ }
+ },
+ }),
+ Link.extend({
+ inclusive: false,
+ }).configure({
+ openOnClick: false,
+ autolink: true,
+ defaultProtocol: "https",
+ protocols: ["http", "https"] }),
+ Placeholder.configure({
+ placeholder: placeholder,
+ }),
+ Typography,
+ ],
+ content: value,
+ onTransaction: () =>
+ {
+ // Force re-render so editor.isActive works
+ // https://tiptap.dev/docs/editor/getting-started/install/svelte
+ editor = editor
+ // TODO: Throttle these so we aren't converting the entire post to HTML on every keystroke, but getHtml() still returns the latest correct value ***
+ // (But we always want to update value whenever it changes to or from an empty string!)
+ value = getHtml()
+ onChange()
+ },
+ })
+ })
+
/**
- Returns a rudimentary HTML version of the text, converting newlines to BR tags. The rest is performed by the server.
+ Returns the editor contents as HTML.
If you use this value to perform an action that affects the server, you probably want to call discardDraft() after this.
*/
export function getHtml(): string
{
- // TODO: Figure out how to do this in a more Svelte-y way. How do Svelte contentEditable wrappers work?
- // When you change this, also change PostView's onStartEdit, which is the other half of this hack.
- return value.replaceAll(/\r\n|\r|\n/g, " ")
+ if (!editor)
+ {
+ console.warn("Tried to get Editor's contents but there was no contentEditable element")
+ return ""
+ }
+ const html = editor.getHTML()
+ if (html === "") return ""
+ return html
}
/** Focuses the editor. */
- export function focus(options?: Parameters[0]): void
+ export function focus(options?: Parameters[0]): void
{
- textarea.focus(options)
+ const scrollIntoView = !(options && options.preventScroll)
+ if (scrollIntoView) element.scrollIntoView()
+ editor.commands.focus(undefined, { scrollIntoView: false })
}
/** Inserts a string at the current cursor position. Will not replace a selection if present. */
- export function insertText(text: string): void
+ export function insertText(html: string): void
{
- const pos = Math.min(textarea.selectionStart, textarea.selectionEnd)
- textarea.setRangeText(text, pos, pos, "end")
- value = textarea.value
+ editor.commands.insertContent(html)
+ // TODO: Make this code work ***
+ // const currentSelection = editor.state.selection
+ // editor.chain().setTextSelection({ from: currentSelection.from, to: currentSelection.to }).insertContent(html)
}
/** Replaces the currently selected text if there is any, or otherwise inserts a string at the current cursor position. */
- export function replaceSelection(text: string): void
+ export function replaceSelection(html: string): void
{
- textarea.setRangeText(text)
- value = textarea.value
+ editor.commands.insertContent(html)
}
/** Discards the current draft and clears the text. */
@@ -207,6 +263,18 @@
}
}
+ onDestroy(() =>
+ {
+ if (editor) editor.destroy()
+ })
+
+ function onLink()
+ {
+ // TODO: Allow linking to websites other than MessageSend.aspx ***
+
+ editor.chain().focus().setLink({ href: "/MessageSend.aspx?name=csmolinsky" }).run()
+ }
+
-
diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte
index 262810d..7214d15 100644
--- a/src/lib/components/Editor.svelte
+++ b/src/lib/components/Editor.svelte
@@ -156,6 +156,8 @@
export function discardDraft(): void
{
value = ""
+ // HACK: This component doesn't respond to changes in value, so update the content directly.
+ editor.commands.clearContent()
saveDraft()
}
@@ -273,6 +275,7 @@
function onLink()
{
// TODO: Allow linking to websites other than MessageSend.aspx ***
+ // (and then unhide the link button)
editor.chain().focus().setLink({ href: "/MessageSend.aspx?name=csmolinsky" }).run()
}
@@ -286,6 +289,25 @@
position: relative;
}
+ .editor
+ {
+ /* Based on CSS from PostView.svelte */
+ :global(img), :global(svg)
+ {
+ max-width: 100%;
+ max-height: 80vh;
+ width: auto;
+ height: auto;
+ object-fit: scale-down;
+ }
+
+ /* Temporary selection visuals */
+ :global(img.ProseMirror-selectednode)
+ {
+ outline: 3px solid var(--accent-glow);
+ }
+ }
+
.curtain
{
border: 3px solid var(--accent);
@@ -341,22 +363,21 @@
{#if !collapsible || value || isFocused}