Update docs for editor

This commit is contained in:
Jon Staab
2025-06-10 13:43:45 -07:00
parent a6db067a8b
commit 26739f9271
+143 -133
View File
@@ -12,145 +12,155 @@ This package powers the editors of [Coracle](https://coracle.social) and [Flotil
npm install @welshman/editor
```
## WelshmanExtension
The `WelshmanExtension` is the main entry point of the package, providing a pre-configured collection of extensions optimized for Nostr content creation.
### Configuration
## Example
```typescript
interface WelshmanOptions {
// Required: Function to sign events
sign: (event: StampedEvent) => Promise<SignedEvent>
import {get} from "svelte/store"
import type {Writable} from "svelte/store"
import type {NodeViewProps} from "@tiptap/core"
import {Router} from "@welshman/router"
import {removeNil} from "@welshman/lib"
import type {FileAttributes} from "@welshman/editor"
import {Editor, MentionSuggestion, WelshmanExtension} from "@welshman/editor"
import {profileSearch, deriveProfileDisplay} from "@welshman/app"
// Required: Handler for submit action
submit: () => void
export const MentionNodeView = ({node}: NodeViewProps) => {
const dom = document.createElement("span")
const display = deriveProfileDisplay(node.attrs.pubkey, removeNil([url]))
// File upload configuration
defaultUploadUrl?: string // Default: "https://nostr.build"
defaultUploadType?: "nip96" | "blossom" // Default: "nip96"
dom.classList.add("tiptap-object")
// Extension configuration
extensions?: WelshmanExtensionOptions
}
```
const unsubDisplay = display.subscribe($display => {
dom.textContent = "@" + $display
})
### Included Extensions
The extension bundles and configures multiple TipTap and nostr-editor extensions:
#### Core TipTap Extensions
- Document
- Text
- Paragraph
- History
- CodeBlock
- CodeInline
- Dropcursor
- Gapcursor
- Placeholder
#### Nostr-specific Extensions
- NostrExtension (base)
- Bolt11Extension (Lightning invoices)
- FileUploadExtension
- ImageExtension
- LinkExtension
- NAddrExtension (Nostr addresses)
- NEventExtension (Nostr events)
- NProfileExtension (Nostr profiles)
- TagExtension
- VideoExtension
- NSecRejectExtension
#### Custom Extensions
- BreakOrSubmit (Enter key handling)
- WordCount
### Usage
```typescript
import { Editor } from '@tiptap/core'
import { WelshmanExtension } from '@welshman/editor'
const editor = new Editor({
extensions: [
WelshmanExtension.configure({
// Required: Event signing function
sign: async (event) => {
return signEvent(event)
},
// Required: Submit handler
submit: () => {
handleSubmit(editor.getText())
},
// Optional: Custom upload configuration
defaultUploadUrl: "https://nostr.build",
defaultUploadType: "nip96",
// Optional: Extension configuration
extensions: {
// Disable specific extensions
wordCount: false,
tag: false,
// Configure extensions
placeholder: {
config: {
placeholder: 'What\'s on your mind?'
}
},
// Extend existing extensions
codeBlock: {
extend: {
renderText: (props) => '```' + props.node.textContent + '```'
}
},
fileUpload: {
config: {
immediateUpload: true,
allowedMimeTypes: [
"image/jpeg",
"image/png",
"video/mp4"
]
}
}
}
})
]
})
```
### Extension Configuration
Each extension can be configured using the `WelshmanExtensionOptions`:
```typescript
type WelshmanExtensionOptions = {
[ExtensionName: string]: {
// Disable the extension
false |
// Configure the extension
{
// Extension-specific configuration
config?: Partial<ExtensionConfig>
// Extend the extension's functionality
extend?: Partial<ExtensionAPI>
}
return {
dom,
destroy: () => {
unsubDisplay()
},
selectNode() {
dom.classList.add("tiptap-active")
},
deselectNode() {
dom.classList.remove("tiptap-active")
},
}
}
export const makeEditor = async ({
content = "",
submit,
uploading,
charCount,
wordCount,
}: {
content?: string
submit: () => void
uploading?: Writable<boolean>
charCount?: Writable<number>
wordCount?: Writable<number>
}) => {
return new Editor({
content, // Initial content, either a string or editor JSON
autofocus: true,
element: document.createElement("div"),
extensions: [
WelshmanExtension.configure({
submit,
extensions: {
placeholder: {
config: {
placeholder: "What's up?",
},
},
breakOrSubmit: {
config: {
aggressive: true, // If this is a chat-type interface
},
},
fileUpload: {
config: {
upload: async (attrs: FileAttributes) => {
const server = "https://cdn.satellite.earth"
try {
let {uploaded, url, ...task} = await uploadFile(server, attrs.file)
if (!uploaded) {
return {error: "Server refused to process the file"}
}
// Always append file extension if missing
if (new URL(url).pathname.split(".").length === 1) {
url += "." + attrs.file.type.split("/")[1]
}
const result = {...task, url, tags: []}
return {result}
} catch (e) {
return {error: e.toString()}
}
},
onDrop() {
uploading?.set(true)
},
onComplete() {
uploading?.set(false)
},
onUploadError(currentEditor, task) {
currentEditor.commands.removeFailedUploads()
alert("Failed to upload file")
uploading?.set(false)
},
},
},
nprofile: {
extend: {
addNodeView: () => MentionNodeView,
addProseMirrorPlugins() {
return [
MentionSuggestion({
editor: (this as any).editor,
search: (term: string) => get(profileSearch).searchValues(term),
getRelays: (pubkey: string) => Router.get().FromPubkeys([pubkey]).getUrls(),
createSuggestion: (value: string) => {
const target = document.createElement("div")
target.textContent = value
return target
},
}),
]
},
},
},
},
}),
],
onUpdate({editor}) {
wordCount?.set(editor.storage.wordCount.words)
charCount?.set(editor.storage.wordCount.chars)
},
})
}
// Create an editor
const editor = makeEditor({
submit: async () => {
const ed = await editor
const content = ed.getText({blockSeparator: "\n"}).trim()
const tags = ed.storage.nostr.getEditorTags()
const event = makeEvent(NOTE, {content, tags})
await publish({event, relays: [/* ... */]})
ed.chain().clearContent().run()
},
})
// This is how you trigger file uploading
const uploadFiles = () => editor.then(ed => ed.chain().selectFiles().run())
```
### Custom Components
The extension includes Svelte components for rendering various Nostr entities in the editor:
- EditBolt11: Lightning invoice
- EditMedia: Image and video
- EditEvent: Nostr event
- EditMention: Nostr profile mention