Update docs for editor
This commit is contained in:
+143
-133
@@ -12,145 +12,155 @@ This package powers the editors of [Coracle](https://coracle.social) and [Flotil
|
|||||||
npm install @welshman/editor
|
npm install @welshman/editor
|
||||||
```
|
```
|
||||||
|
|
||||||
## WelshmanExtension
|
## Example
|
||||||
|
|
||||||
The `WelshmanExtension` is the main entry point of the package, providing a pre-configured collection of extensions optimized for Nostr content creation.
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface WelshmanOptions {
|
import {get} from "svelte/store"
|
||||||
// Required: Function to sign events
|
import type {Writable} from "svelte/store"
|
||||||
sign: (event: StampedEvent) => Promise<SignedEvent>
|
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
|
export const MentionNodeView = ({node}: NodeViewProps) => {
|
||||||
submit: () => void
|
const dom = document.createElement("span")
|
||||||
|
const display = deriveProfileDisplay(node.attrs.pubkey, removeNil([url]))
|
||||||
|
|
||||||
// File upload configuration
|
dom.classList.add("tiptap-object")
|
||||||
defaultUploadUrl?: string // Default: "https://nostr.build"
|
|
||||||
defaultUploadType?: "nip96" | "blossom" // Default: "nip96"
|
|
||||||
|
|
||||||
// Extension configuration
|
const unsubDisplay = display.subscribe($display => {
|
||||||
extensions?: WelshmanExtensionOptions
|
dom.textContent = "@" + $display
|
||||||
}
|
})
|
||||||
```
|
|
||||||
|
|
||||||
### Included Extensions
|
return {
|
||||||
|
dom,
|
||||||
The extension bundles and configures multiple TipTap and nostr-editor extensions:
|
destroy: () => {
|
||||||
|
unsubDisplay()
|
||||||
#### Core TipTap Extensions
|
},
|
||||||
- Document
|
selectNode() {
|
||||||
- Text
|
dom.classList.add("tiptap-active")
|
||||||
- Paragraph
|
},
|
||||||
- History
|
deselectNode() {
|
||||||
- CodeBlock
|
dom.classList.remove("tiptap-active")
|
||||||
- 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>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user