Add editor
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
--ignore-dir=docs
|
--ignore-dir=docs
|
||||||
|
--ignore-dir=dist
|
||||||
--ignore-dir=build
|
--ignore-dir=build
|
||||||
|
--ignore-dir=.svelte-kit
|
||||||
--ignore-file=match:yarn.lock
|
--ignore-file=match:yarn.lock
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
node_modules
|
node_modules
|
||||||
docs
|
docs
|
||||||
|
dist
|
||||||
build
|
build
|
||||||
.vscode
|
.vscode
|
||||||
|
.svelte-kit
|
||||||
|
|||||||
Generated
+2720
-10
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
|||||||
|
# @welshman/editor [](https://npmjs.com/package/@welshman/editor)
|
||||||
|
|
||||||
|
A batteries-included nostr-editor.
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"name": "@welshman/editor",
|
||||||
|
"version": "0.0.2",
|
||||||
|
"scripts": {
|
||||||
|
"pub": "npm run check && npm run build && npm publish",
|
||||||
|
"fix": "",
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build && npm run package",
|
||||||
|
"package": "svelte-kit sync && svelte-package && publint",
|
||||||
|
"prepublishOnly": "npm run package",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"!dist/**/*.test.*",
|
||||||
|
"!dist/**/*.spec.*"
|
||||||
|
],
|
||||||
|
"sideEffects": [
|
||||||
|
"**/*.css"
|
||||||
|
],
|
||||||
|
"svelte": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"svelte": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^2.9.1",
|
||||||
|
"@tiptap/extension-code": "^2.9.1",
|
||||||
|
"@tiptap/extension-code-block": "^2.9.1",
|
||||||
|
"@tiptap/extension-document": "^2.9.1",
|
||||||
|
"@tiptap/extension-dropcursor": "^2.9.1",
|
||||||
|
"@tiptap/extension-gapcursor": "^2.9.1",
|
||||||
|
"@tiptap/extension-hard-break": "^2.9.1",
|
||||||
|
"@tiptap/extension-history": "^2.9.1",
|
||||||
|
"@tiptap/extension-paragraph": "^2.9.1",
|
||||||
|
"@tiptap/extension-placeholder": "^2.9.1",
|
||||||
|
"@tiptap/extension-text": "^2.9.1",
|
||||||
|
"@tiptap/pm": "^2.9.1",
|
||||||
|
"@tiptap/suggestion": "^2.9.1",
|
||||||
|
"@welshman/lib": "^0.0.36",
|
||||||
|
"@welshman/util": "^0.0.53",
|
||||||
|
"nostr-editor": "github:cesardeazevedo/nostr-editor#d8dc03c",
|
||||||
|
"nostr-tools": "^2.8.1",
|
||||||
|
"svelte": "^4.0.0",
|
||||||
|
"svelte-tiptap": "^1.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/kit": "^2.0.0",
|
||||||
|
"@sveltejs/package": "^2.0.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||||
|
"@tiptap/core": "^2.9.1",
|
||||||
|
"@tiptap/extension-code": "^2.9.1",
|
||||||
|
"@tiptap/extension-code-block": "^2.9.1",
|
||||||
|
"@tiptap/extension-document": "^2.9.1",
|
||||||
|
"@tiptap/extension-dropcursor": "^2.9.1",
|
||||||
|
"@tiptap/extension-gapcursor": "^2.9.1",
|
||||||
|
"@tiptap/extension-hard-break": "^2.9.1",
|
||||||
|
"@tiptap/extension-history": "^2.9.1",
|
||||||
|
"@tiptap/extension-paragraph": "^2.9.1",
|
||||||
|
"@tiptap/extension-placeholder": "^2.9.1",
|
||||||
|
"@tiptap/extension-text": "^2.9.1",
|
||||||
|
"@tiptap/pm": "^2.9.1",
|
||||||
|
"@welshman/lib": "^0.0.36",
|
||||||
|
"@welshman/util": "^0.0.53",
|
||||||
|
"nostr-editor": "github:cesardeazevedo/nostr-editor#d8dc03c",
|
||||||
|
"nostr-tools": "^2.10.4",
|
||||||
|
"publint": "^0.2.0",
|
||||||
|
"svelte": "^4.0.0",
|
||||||
|
"svelte-check": "^4.0.0",
|
||||||
|
"svelte-tiptap": "^1.0.0",
|
||||||
|
"tippy.js": "^6.0.0",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"vite": "^6.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div>%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {NodeViewProps} from "@tiptap/core"
|
||||||
|
import {NodeViewWrapper} from "svelte-tiptap"
|
||||||
|
|
||||||
|
export let node: NodeViewProps["node"]
|
||||||
|
export let selected: NodeViewProps["selected"]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<NodeViewWrapper as="span">
|
||||||
|
<button class="tiptap-object {selected ? 'tiptap-active' : ''}">
|
||||||
|
{node.attrs.lnbc.slice(0, 16)}...
|
||||||
|
</button>
|
||||||
|
</NodeViewWrapper>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {fromNostrURI} from "@welshman/util"
|
||||||
|
import type {NodeViewProps} from "@tiptap/core"
|
||||||
|
import {NodeViewWrapper} from "svelte-tiptap"
|
||||||
|
|
||||||
|
export let node: NodeViewProps["node"]
|
||||||
|
export let selected: NodeViewProps["selected"]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<NodeViewWrapper as="span">
|
||||||
|
<button class="tiptap-object {selected ? 'tiptap-active' : ''}">
|
||||||
|
{fromNostrURI(node.attrs.nevent).slice(0, 16)}...
|
||||||
|
</button>
|
||||||
|
</NodeViewWrapper>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {NodeViewProps} from "@tiptap/core"
|
||||||
|
import {NodeViewWrapper} from "svelte-tiptap"
|
||||||
|
|
||||||
|
export let node: NodeViewProps["node"]
|
||||||
|
export let selected: NodeViewProps["selected"]
|
||||||
|
|
||||||
|
$: selectedClass = selected ? "tiptap-active" : ""
|
||||||
|
$: uploadingClass = node.attrs.uploading ? "tiptap-uploading" : ""
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<NodeViewWrapper as="span" class="tiptap-object {selectedClass} {uploadingClass}">
|
||||||
|
{node.attrs.file?.name || node.attrs.src}
|
||||||
|
</NodeViewWrapper>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {NodeViewProps} from "@tiptap/core"
|
||||||
|
import {NodeViewWrapper} from "svelte-tiptap"
|
||||||
|
|
||||||
|
export let node: NodeViewProps["node"]
|
||||||
|
export let selected: NodeViewProps["selected"]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<NodeViewWrapper as="span">
|
||||||
|
<button class="tiptap-object {selected ? 'tiptap-active' : ''}">
|
||||||
|
@{node.attrs.nprofile.slice(0, 16)}...
|
||||||
|
</button>
|
||||||
|
</NodeViewWrapper>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let value
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{value}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<svelte:options accessors />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {fly} from "svelte/transition"
|
||||||
|
import {throttle, clamp} from "@welshman/lib"
|
||||||
|
|
||||||
|
export let term
|
||||||
|
export let search
|
||||||
|
export let select
|
||||||
|
export let component
|
||||||
|
export let allowCreate = false
|
||||||
|
|
||||||
|
let index = 0
|
||||||
|
let element: Element
|
||||||
|
let items: string[] = []
|
||||||
|
|
||||||
|
$: populateItems(term)
|
||||||
|
|
||||||
|
const populateItems = throttle(300, term => {
|
||||||
|
items = $search(term).slice(0, 5)
|
||||||
|
})
|
||||||
|
|
||||||
|
const setIndex = (newIndex: number, block: any) => {
|
||||||
|
index = clamp([0, items.length - 1], newIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const onKeyDown = (e: any) => {
|
||||||
|
if (["Enter", "Tab"].includes(e.code)) {
|
||||||
|
const value = items[index]
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
select(value)
|
||||||
|
return true
|
||||||
|
} else if (term && allowCreate) {
|
||||||
|
select(term)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.code === "Space" && term && allowCreate) {
|
||||||
|
select(term)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.code === "ArrowUp") {
|
||||||
|
setIndex(index - 1, "start")
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.code === "ArrowDown") {
|
||||||
|
setIndex(index + 1, "start")
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if term}
|
||||||
|
<div bind:this={element} transition:fly|local={{duration: 200}} class="tiptap-suggestions">
|
||||||
|
<div class="tiptap-suggestions__content">
|
||||||
|
{#if term && allowCreate && !items.includes(term)}
|
||||||
|
<button
|
||||||
|
class="tiptap-suggestions__create"
|
||||||
|
on:mousedown|preventDefault|stopPropagation
|
||||||
|
on:click|preventDefault|stopPropagation={() => select(term)}>
|
||||||
|
Use "<svelte:component this={component} value={term} />"
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#each items as value, i (value)}
|
||||||
|
<button
|
||||||
|
class="tiptap-suggestions__item"
|
||||||
|
class:tiptap-suggestions__selected={index === i}
|
||||||
|
on:mousedown|preventDefault|stopPropagation
|
||||||
|
on:click|preventDefault|stopPropagation={() => select(value)}>
|
||||||
|
<svelte:component this={component} {value} />
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{#if items.length === 0}
|
||||||
|
<div class="tiptap-suggestions__empty">No results</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export {default as Suggestions} from "./Suggestions.svelte"
|
||||||
|
export {default as SuggestionString} from "./SuggestionString.svelte"
|
||||||
|
export {default as EditBolt11} from "./EditBolt11.svelte"
|
||||||
|
export {default as EditMedia} from "./EditMedia.svelte"
|
||||||
|
export {default as EditEvent} from "./EditEvent.svelte"
|
||||||
|
export {default as EditMention} from "./EditMention.svelte"
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import {HardBreak, type HardBreakOptions} from "@tiptap/extension-hard-break"
|
||||||
|
|
||||||
|
export interface BreakOrSubmitOptions extends HardBreakOptions {
|
||||||
|
/** Handler for when enter is pressed. */
|
||||||
|
submit: () => void
|
||||||
|
|
||||||
|
/** Whether to call `submit` on unmodified Enter */
|
||||||
|
aggressive?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BreakOrSubmit = HardBreak.extend<BreakOrSubmitOptions>({
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return {
|
||||||
|
"Shift-Enter": () => this.editor.commands.setHardBreak(),
|
||||||
|
"Mod-Enter": () => {
|
||||||
|
this.options.submit()
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import {InputRule, mergeAttributes, Node, PasteRule} from "@tiptap/core"
|
||||||
|
import type {CodeOptions} from "@tiptap/extension-code"
|
||||||
|
|
||||||
|
const inputRegex = /(?:^|\s)(`(?!\s+`)((?:[^`]+))`(?!\s+`))$/
|
||||||
|
|
||||||
|
const pasteRegex = /(?:^|\s)(`(?!\s+`)((?:[^`]+))`(?!\s+`))/g
|
||||||
|
|
||||||
|
export type CodeInlineOptions = object
|
||||||
|
|
||||||
|
export const CodeInline = Node.create<CodeOptions>({
|
||||||
|
name: "codeInline",
|
||||||
|
content: "text*",
|
||||||
|
marks: "",
|
||||||
|
group: "inline",
|
||||||
|
inline: true,
|
||||||
|
code: true,
|
||||||
|
defining: true,
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
HTMLAttributes: {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseHTML() {
|
||||||
|
return [{tag: "code"}]
|
||||||
|
},
|
||||||
|
renderHTML({HTMLAttributes}) {
|
||||||
|
return ["code", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return {
|
||||||
|
// remove code block when at start of document or code block is empty
|
||||||
|
Backspace: () => {
|
||||||
|
const {empty, $anchor, $from} = this.editor.state.selection
|
||||||
|
|
||||||
|
const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2
|
||||||
|
|
||||||
|
if (!empty || $anchor.parent.type.name !== this.name) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAtEnd) {
|
||||||
|
const {tr} = this.editor.state
|
||||||
|
tr.delete($from.start(), $from.end() + 1)
|
||||||
|
this.editor.view.dispatch(tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addInputRules() {
|
||||||
|
return [
|
||||||
|
new InputRule({
|
||||||
|
find: inputRegex,
|
||||||
|
handler: ({state, range, match}) => {
|
||||||
|
const textNode = state.schema.text(match[2])
|
||||||
|
const codeNode = this.type.create(null, textNode)
|
||||||
|
state.tr.replaceWith(range.from, range.to, codeNode).insertText(" ")
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
addPasteRules() {
|
||||||
|
return [
|
||||||
|
new PasteRule({
|
||||||
|
find: pasteRegex,
|
||||||
|
handler: ({state, range, match}) => {
|
||||||
|
const textNode = state.schema.text(match[2])
|
||||||
|
const codeNode = this.type.create(null, textNode)
|
||||||
|
state.tr.replaceWith(range.from, range.to, codeNode).insertText(" ")
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
import type {StampedEvent, SignedEvent} from "@welshman/util"
|
||||||
|
import {toNostrURI} from "@welshman/util"
|
||||||
|
import {deepMergeLeft} from "@welshman/lib"
|
||||||
|
import {SvelteNodeViewRenderer} from "svelte-tiptap"
|
||||||
|
import type {Extensions, AnyExtension} from "@tiptap/core"
|
||||||
|
import {CodeBlock} from "@tiptap/extension-code-block"
|
||||||
|
import type {CodeBlockOptions} from "@tiptap/extension-code-block"
|
||||||
|
import {Document} from "@tiptap/extension-document"
|
||||||
|
import {Dropcursor} from "@tiptap/extension-dropcursor"
|
||||||
|
import type {DropcursorOptions} from "@tiptap/extension-dropcursor"
|
||||||
|
import {Gapcursor} from "@tiptap/extension-gapcursor"
|
||||||
|
import {History} from "@tiptap/extension-history"
|
||||||
|
import type {HistoryOptions} from "@tiptap/extension-history"
|
||||||
|
import {Paragraph} from "@tiptap/extension-paragraph"
|
||||||
|
import type {ParagraphOptions} from "@tiptap/extension-paragraph"
|
||||||
|
import {Text} from "@tiptap/extension-text"
|
||||||
|
import {Placeholder} from "@tiptap/extension-placeholder"
|
||||||
|
import type {PlaceholderOptions} from "@tiptap/extension-placeholder"
|
||||||
|
import type {
|
||||||
|
NostrOptions,
|
||||||
|
FileUploadOptions,
|
||||||
|
ImageOptions,
|
||||||
|
LinkOptions,
|
||||||
|
NSecRejectOptions,
|
||||||
|
} from "nostr-editor"
|
||||||
|
import {
|
||||||
|
NostrExtension,
|
||||||
|
Bolt11Extension,
|
||||||
|
FileUploadExtension,
|
||||||
|
ImageExtension,
|
||||||
|
LinkExtension,
|
||||||
|
NAddrExtension,
|
||||||
|
NEventExtension,
|
||||||
|
NProfileExtension,
|
||||||
|
TagExtension,
|
||||||
|
VideoExtension,
|
||||||
|
NSecRejectExtension,
|
||||||
|
} from "nostr-editor"
|
||||||
|
import {WordCount} from "./WordCount.js"
|
||||||
|
import {CodeInline, type CodeInlineOptions} from "./CodeInline.js"
|
||||||
|
import {BreakOrSubmit, type BreakOrSubmitOptions} from "./BreakOrSubmit.js"
|
||||||
|
import {EditBolt11, EditMedia, EditEvent, EditMention} from "../components/index.js"
|
||||||
|
|
||||||
|
export type ChildExtensionOptions<C = any, E = any> =
|
||||||
|
| false
|
||||||
|
| {
|
||||||
|
extend?: Partial<E>
|
||||||
|
config?: Partial<C>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EmptyOptions = object
|
||||||
|
|
||||||
|
export type WelshmanExtensionOptions = {
|
||||||
|
bolt11?: false
|
||||||
|
breakOrSubmit: ChildExtensionOptions<BreakOrSubmitOptions>
|
||||||
|
codeInline?: ChildExtensionOptions<CodeInlineOptions>
|
||||||
|
codeBlock?: ChildExtensionOptions<CodeBlockOptions>
|
||||||
|
document?: false
|
||||||
|
dropcursor?: ChildExtensionOptions<DropcursorOptions>
|
||||||
|
fileUpload?: ChildExtensionOptions<FileUploadOptions>
|
||||||
|
gapcursor?: false
|
||||||
|
history?: ChildExtensionOptions<HistoryOptions>
|
||||||
|
image?: ChildExtensionOptions<ImageOptions>
|
||||||
|
link?: ChildExtensionOptions<LinkOptions>
|
||||||
|
naddr?: ChildExtensionOptions<EmptyOptions>
|
||||||
|
nevent?: ChildExtensionOptions<EmptyOptions>
|
||||||
|
nprofile?: ChildExtensionOptions<EmptyOptions>
|
||||||
|
nsecReject?: ChildExtensionOptions<NSecRejectOptions>
|
||||||
|
paragraph?: ChildExtensionOptions<ParagraphOptions>
|
||||||
|
placeholder?: ChildExtensionOptions<PlaceholderOptions>
|
||||||
|
tag?: false
|
||||||
|
text?: false
|
||||||
|
video?: false
|
||||||
|
wordCount?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WelshmanOptions extends NostrOptions {
|
||||||
|
submit?: () => void
|
||||||
|
sign?: (event: StampedEvent) => Promise<SignedEvent>
|
||||||
|
defaultUploadUrl?: string
|
||||||
|
defaultUploadType?: "nip96" | "blossom"
|
||||||
|
extensions?: WelshmanExtensionOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WelshmanExtension = NostrExtension.extend<WelshmanOptions>({
|
||||||
|
name: "welshman",
|
||||||
|
|
||||||
|
// Return an empty object or else options can't be passed
|
||||||
|
addOptions() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
|
||||||
|
addExtensions() {
|
||||||
|
const {
|
||||||
|
sign,
|
||||||
|
submit,
|
||||||
|
defaultUploadUrl = "https://nostr.build",
|
||||||
|
defaultUploadType = "nip96",
|
||||||
|
} = this.options
|
||||||
|
|
||||||
|
if (!sign) throw new Error("sign is a required argument to WelshmanExtension")
|
||||||
|
if (!submit) throw new Error("submit is a required argument to WelshmanExtension")
|
||||||
|
|
||||||
|
const extensionOptions = deepMergeLeft(this.options.extensions || {}, {
|
||||||
|
codeInline: {
|
||||||
|
extend: {
|
||||||
|
renderText: (props: any) => "`" + props.node.textContent + "`",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
codeBlock: {
|
||||||
|
extend: {
|
||||||
|
renderText: (props: any) => "```" + props.node.textContent + "```",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bolt11: {
|
||||||
|
config: {
|
||||||
|
inline: true,
|
||||||
|
group: "inline",
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
addNodeView: () => SvelteNodeViewRenderer(EditBolt11),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
config: {
|
||||||
|
inline: true,
|
||||||
|
group: "inline",
|
||||||
|
defaultUploadUrl,
|
||||||
|
defaultUploadType,
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
addNodeView: () => SvelteNodeViewRenderer(EditMedia),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
video: {
|
||||||
|
config: {
|
||||||
|
inline: true,
|
||||||
|
group: "inline",
|
||||||
|
defaultUploadUrl,
|
||||||
|
defaultUploadType,
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
addNodeView: () => SvelteNodeViewRenderer(EditMedia),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nevent: {
|
||||||
|
config: {
|
||||||
|
inline: true,
|
||||||
|
group: "inline",
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
renderText: (props: any) => toNostrURI(props.node.attrs.nevent),
|
||||||
|
addNodeView: () => SvelteNodeViewRenderer(EditEvent),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
naddr: {
|
||||||
|
config: {
|
||||||
|
inline: true,
|
||||||
|
group: "inline",
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
renderText: (props: any) => toNostrURI(props.node.attrs.naddr),
|
||||||
|
addNodeView: () => SvelteNodeViewRenderer(EditEvent),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nprofile: {
|
||||||
|
extend: {
|
||||||
|
renderText: (props: any) => toNostrURI(props.node.attrs.nprofile),
|
||||||
|
addNodeView: () => SvelteNodeViewRenderer(EditMention),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
breakOrSubmit: {
|
||||||
|
config: {
|
||||||
|
submit,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fileUpload: {
|
||||||
|
config: {
|
||||||
|
sign,
|
||||||
|
immediateUpload: true,
|
||||||
|
allowedMimeTypes: [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/gif",
|
||||||
|
"image/webp",
|
||||||
|
"video/mp4",
|
||||||
|
"video/mpeg",
|
||||||
|
"video/webm",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}) as WelshmanExtensionOptions
|
||||||
|
|
||||||
|
const extensions: Extensions = []
|
||||||
|
|
||||||
|
const addExtension = (extension: AnyExtension, options?: ChildExtensionOptions | false) => {
|
||||||
|
if (options === false) return
|
||||||
|
|
||||||
|
if (options?.extend) {
|
||||||
|
extension = extension.extend(options.extend)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.config) {
|
||||||
|
extension = extension.configure(options.config)
|
||||||
|
}
|
||||||
|
|
||||||
|
extensions.push(extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
addExtension(Document, extensionOptions.document)
|
||||||
|
addExtension(Text, extensionOptions.text)
|
||||||
|
addExtension(Paragraph, extensionOptions.paragraph)
|
||||||
|
addExtension(History, extensionOptions.history)
|
||||||
|
addExtension(CodeBlock, extensionOptions.codeBlock)
|
||||||
|
addExtension(CodeInline, extensionOptions.codeInline)
|
||||||
|
addExtension(Dropcursor, extensionOptions.dropcursor)
|
||||||
|
addExtension(FileUploadExtension, extensionOptions.fileUpload)
|
||||||
|
addExtension(Gapcursor, extensionOptions.gapcursor)
|
||||||
|
addExtension(BreakOrSubmit, extensionOptions.breakOrSubmit)
|
||||||
|
addExtension(ImageExtension, extensionOptions.image)
|
||||||
|
addExtension(LinkExtension, extensionOptions.link)
|
||||||
|
addExtension(NAddrExtension, extensionOptions.naddr)
|
||||||
|
addExtension(NEventExtension, extensionOptions.nevent)
|
||||||
|
addExtension(NProfileExtension, extensionOptions.nprofile)
|
||||||
|
addExtension(NSecRejectExtension, extensionOptions.nsecReject)
|
||||||
|
addExtension(Placeholder, extensionOptions.placeholder)
|
||||||
|
addExtension(TagExtension, extensionOptions.tag)
|
||||||
|
addExtension(VideoExtension, extensionOptions.video)
|
||||||
|
addExtension(Bolt11Extension, extensionOptions.bolt11)
|
||||||
|
addExtension(WordCount, extensionOptions.wordCount)
|
||||||
|
|
||||||
|
return extensions
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import {Extension} from "@tiptap/core"
|
||||||
|
|
||||||
|
export const WordCount = Extension.create({
|
||||||
|
name: "wordCount",
|
||||||
|
|
||||||
|
addStorage() {
|
||||||
|
return {
|
||||||
|
words: 0,
|
||||||
|
chars: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onUpdate() {
|
||||||
|
const text = this.editor.state.doc.textContent
|
||||||
|
|
||||||
|
this.storage.words = text.split(/\s+/).filter(word => word.length > 0).length
|
||||||
|
this.storage.chars = text.length
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./BreakOrSubmit.js"
|
||||||
|
export * from "./CodeInline.js"
|
||||||
|
export * from "./Welshman.js"
|
||||||
|
export * from "./WordCount.js"
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
:root {
|
||||||
|
--tiptap-object-bg: #eee;
|
||||||
|
--tiptap-object-fg: #111;
|
||||||
|
--tiptap-active-bg: #ddd;
|
||||||
|
--tiptap-active-fg: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap {
|
||||||
|
outline: none;
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap p.is-editor-empty:first-child::before {
|
||||||
|
color: #adb5bd;
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
float: left;
|
||||||
|
height: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap pre code {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0.25rem;
|
||||||
|
background-color: var(--tiptap-object-bg);
|
||||||
|
color: var(--tiptap-object-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap .tiptap-object,
|
||||||
|
.tiptap p code,
|
||||||
|
.tiptap [tag] {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
background-color: var(--tiptap-object-bg);
|
||||||
|
color: var(--tiptap-object-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap .tiptap-active {
|
||||||
|
background-color: var(--tiptap-active-bg);
|
||||||
|
color: var(--tiptap-active-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap .tiptap-uploading {
|
||||||
|
animation: tiptapFileUpload 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap-suggestions {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
max-height: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap-suggestions__content {
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0px 5px 8px 0px rgba(0, 0, 0, 0.2);
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap-suggestions__create,
|
||||||
|
.tiptap-suggestions__item {
|
||||||
|
white-space: nowrap;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0px;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow-x: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
text-align: left;
|
||||||
|
transition-duration: 100ms;
|
||||||
|
transition-property: color background-color;
|
||||||
|
background-color: var(--tiptap-object-bg);
|
||||||
|
color: var(--tiptap-object-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap-suggestions__selected,
|
||||||
|
.tiptap-suggestions__create:hover,
|
||||||
|
.tiptap-suggestions__item:hover {
|
||||||
|
background-color: var(--tiptap-active-bg);
|
||||||
|
color: var(--tiptap-active-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap-suggestions__empty {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tiptapFileUpload {
|
||||||
|
0% {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import './index.css'
|
||||||
|
|
||||||
|
export * from './components/index.js'
|
||||||
|
export * from './extensions/index.js'
|
||||||
|
export * from './plugins/index.js'
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import type {SvelteComponent, ComponentType} from "svelte"
|
||||||
|
import type {Readable} from "svelte/store"
|
||||||
|
import type {Instance} from "tippy.js"
|
||||||
|
import tippy from "tippy.js"
|
||||||
|
import {nprofileEncode} from "nostr-tools/nip19"
|
||||||
|
// @ts-ignore
|
||||||
|
import type {Editor} from "svelte-tiptap"
|
||||||
|
import {PluginKey} from "@tiptap/pm/state"
|
||||||
|
import Suggestion from "@tiptap/suggestion"
|
||||||
|
import Suggestions from "../components/Suggestions.svelte"
|
||||||
|
import SuggestionString from "../components/SuggestionString.svelte"
|
||||||
|
|
||||||
|
export type TippySuggestionOptions = {
|
||||||
|
char: string
|
||||||
|
name: string
|
||||||
|
editor: Editor
|
||||||
|
search: Readable<(term: string) => string[]>
|
||||||
|
select: (value: string, props: any) => void
|
||||||
|
allowCreate?: boolean
|
||||||
|
wrapper?: ComponentType
|
||||||
|
component?: ComponentType
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TippySuggestion = ({
|
||||||
|
char,
|
||||||
|
name,
|
||||||
|
editor,
|
||||||
|
search,
|
||||||
|
select,
|
||||||
|
allowCreate,
|
||||||
|
wrapper = Suggestions,
|
||||||
|
component = SuggestionString,
|
||||||
|
}: TippySuggestionOptions) =>
|
||||||
|
Suggestion({
|
||||||
|
char,
|
||||||
|
editor,
|
||||||
|
pluginKey: new PluginKey(`suggest-${name}`),
|
||||||
|
command: ({editor, range, props}) => {
|
||||||
|
// increase range.to by one when the next node is of type "text"
|
||||||
|
// and starts with a space character
|
||||||
|
const nodeAfter = editor.view.state.selection.$to.nodeAfter
|
||||||
|
const overrideSpace = nodeAfter?.text?.startsWith(" ")
|
||||||
|
|
||||||
|
if (overrideSpace) {
|
||||||
|
range.to += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.insertContentAt(range, [
|
||||||
|
{type: name, attrs: props},
|
||||||
|
{type: "text", text: " "},
|
||||||
|
])
|
||||||
|
.run()
|
||||||
|
|
||||||
|
window.getSelection()?.collapseToEnd()
|
||||||
|
},
|
||||||
|
allow: ({state, range}) => {
|
||||||
|
const $from = state.doc.resolve(range.from)
|
||||||
|
const type = state.schema.nodes[name]
|
||||||
|
|
||||||
|
return !!$from.parent.type.contentMatch.matchType(type)
|
||||||
|
},
|
||||||
|
render: () => {
|
||||||
|
let popover: Instance[]
|
||||||
|
let suggestions: SvelteComponent
|
||||||
|
|
||||||
|
const mapProps = (props: any) => ({
|
||||||
|
term: props.query,
|
||||||
|
search,
|
||||||
|
component,
|
||||||
|
allowCreate,
|
||||||
|
select: (value: string) => select(value, props),
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
onStart: props => {
|
||||||
|
const target = document.createElement("div")
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
popover = tippy("body", {
|
||||||
|
getReferenceClientRect: props.clientRect as any,
|
||||||
|
appendTo: document.querySelector("dialog[open]") || document.body,
|
||||||
|
content: target,
|
||||||
|
showOnCreate: true,
|
||||||
|
interactive: true,
|
||||||
|
trigger: "manual",
|
||||||
|
placement: "bottom-start",
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!props.query) popover[0].hide()
|
||||||
|
|
||||||
|
suggestions = new wrapper({target, props: mapProps(props)})
|
||||||
|
},
|
||||||
|
onUpdate: props => {
|
||||||
|
if (props.query) {
|
||||||
|
popover[0].show()
|
||||||
|
} else {
|
||||||
|
popover[0].hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
suggestions.$set(mapProps(props))
|
||||||
|
|
||||||
|
if (props.clientRect) {
|
||||||
|
popover[0].setProps({
|
||||||
|
getReferenceClientRect: props.clientRect as any,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onKeyDown: props => {
|
||||||
|
if (props.event.key === "Escape") {
|
||||||
|
popover[0].hide()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return Boolean(suggestions.onKeyDown?.(props.event))
|
||||||
|
},
|
||||||
|
onExit: () => {
|
||||||
|
popover[0].destroy()
|
||||||
|
suggestions.$destroy()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export type MentionSuggestionOptions = Partial<TippySuggestionOptions> & {
|
||||||
|
editor: Editor
|
||||||
|
search: Readable<(term: string) => string[]>
|
||||||
|
getRelays: (pubkey: string) => string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MentionSuggestion = (options: MentionSuggestionOptions) =>
|
||||||
|
TippySuggestion({
|
||||||
|
char: "@",
|
||||||
|
name: "nprofile",
|
||||||
|
select: (pubkey: string, props: any) => {
|
||||||
|
const relays = options.getRelays(pubkey)
|
||||||
|
const nprofile = nprofileEncode({pubkey, relays})
|
||||||
|
|
||||||
|
return props.command({pubkey, relays, nprofile})
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./TippySuggestion.js"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<h1>Welcome to your library project</h1>
|
||||||
|
<p>Create your package using @sveltejs/package and preview/showcase your work with SvelteKit</p>
|
||||||
|
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,10 @@
|
|||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
// Consult https://svelte.dev/docs/kit/integrations
|
||||||
|
// for more information about preprocessors
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"test/**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [sveltekit()]
|
||||||
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
for package in $(./get_packages.py); do
|
for package in $(./get_packages.py); do
|
||||||
npx onchange packages/$package -e '**/build/**' -k -- ./build_and_link.sh $package &
|
npx onchange packages/$package -e '**/build/**' -e '**/dist/**' -e '**/.svelte-kit/**' -k -- ./build_and_link.sh $package &
|
||||||
done
|
done
|
||||||
|
|
||||||
wait
|
wait
|
||||||
|
|||||||
Reference in New Issue
Block a user