forked from coracle/flotilla
Move editor stuff to its own folder
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
<script lang="ts">
|
||||
import cx from 'classnames'
|
||||
import {DateInput} from "date-picker-svelte"
|
||||
import {formatTimestamp} from '@welshman/app'
|
||||
import Icon from '@lib/components/Icon.svelte'
|
||||
import Button from '@lib/components/Button.svelte'
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
|
||||
export let initialValue: Date | undefined = undefined
|
||||
export let value: Date | undefined = initialValue
|
||||
@@ -21,12 +19,8 @@
|
||||
</script>
|
||||
|
||||
<Button class="relative" on:click={init}>
|
||||
<DateInput
|
||||
format="yyyy-MM-dd HH:mm"
|
||||
timePrecision="minute"
|
||||
placeholder=""
|
||||
bind:value />
|
||||
<div class="absolute top-0 h-12 right-2 flex gap-2 items-center cursor-pointer">
|
||||
<DateInput format="yyyy-MM-dd HH:mm" timePrecision="minute" placeholder="" bind:value />
|
||||
<div class="absolute right-2 top-0 flex h-12 cursor-pointer items-center gap-2">
|
||||
{#if value}
|
||||
<Button on:click={clear} class="h-5">
|
||||
<Icon icon="close-circle" />
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
login: Login,
|
||||
"login-2": Login2,
|
||||
magnifer: Magnifer,
|
||||
'map-point': MapPoint,
|
||||
"map-point": MapPoint,
|
||||
"menu-dots": MenuDots,
|
||||
"notes-minimalistic": NotesMinimalistic,
|
||||
"pallete-2": Pallete2,
|
||||
@@ -124,14 +124,14 @@
|
||||
"shop-minimalistic": ShopMinimalistic,
|
||||
"smile-circle": SmileCircle,
|
||||
settings: Settings,
|
||||
'tag-horizontal': TagHorizontal,
|
||||
"tag-horizontal": TagHorizontal,
|
||||
"ufo-3": UFO3,
|
||||
"square-share-line": SquareShareLine,
|
||||
"user-heart": UserHeart,
|
||||
"user-circle": UserCircle,
|
||||
"user-rounded": UserRounded,
|
||||
widget: Widget,
|
||||
'widget-add': WidgetAdd,
|
||||
"widget-add": WidgetAdd,
|
||||
"wifi-router-round": WiFiRouterRound,
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import type {NodeViewProps} from "@tiptap/core"
|
||||
import {NodeViewWrapper} from "svelte-tiptap"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import {clip} from "@app/toast"
|
||||
|
||||
export let node: NodeViewProps["node"]
|
||||
export let selected: NodeViewProps["selected"]
|
||||
|
||||
const copy = () => clip(node.attrs.lnbc)
|
||||
</script>
|
||||
|
||||
<NodeViewWrapper class="inline">
|
||||
<Button on:click={copy} class={cx("link-content", {"link-content-selected": selected})}>
|
||||
<Icon icon="bolt" size={3} class="inline-block translate-y-px" />
|
||||
{node.attrs.lnbc.slice(0, 16)}...
|
||||
</Button>
|
||||
</NodeViewWrapper>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import type {NodeViewProps} from "@tiptap/core"
|
||||
import {NodeViewWrapper} from "svelte-tiptap"
|
||||
import {ellipsize} from "@welshman/lib"
|
||||
import {type TrustedEvent, fromNostrURI, Address} from "@welshman/util"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import {deriveEvent} from "@app/state"
|
||||
|
||||
export let node: NodeViewProps["node"]
|
||||
|
||||
const displayEvent = (e: TrustedEvent) =>
|
||||
e?.content.length > 1
|
||||
? ellipsize(e.content, 50)
|
||||
: fromNostrURI(nevent || naddr).slice(0, 16) + "..."
|
||||
|
||||
$: ({identifier, pubkey, kind, id, relays = [], nevent, naddr} = node.attrs)
|
||||
$: event = deriveEvent(id || new Address(kind, pubkey, identifier).toString(), relays)
|
||||
</script>
|
||||
|
||||
<NodeViewWrapper class="inline">
|
||||
<Link external href="https://njump.me/{node.attrs.nevent}" class="link-content">
|
||||
{displayEvent($event)}
|
||||
</Link>
|
||||
</NodeViewWrapper>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import type {NodeViewProps} from "@tiptap/core"
|
||||
import {NodeViewWrapper} from "svelte-tiptap"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
|
||||
export let node: NodeViewProps["node"]
|
||||
export let selected: NodeViewProps["selected"]
|
||||
</script>
|
||||
|
||||
<NodeViewWrapper class={cx("link-content inline", {"link-content-selected": selected})}>
|
||||
<Icon icon="paperclip" size={3} class="inline-block translate-y-px" />
|
||||
{node.attrs.file.name}
|
||||
</NodeViewWrapper>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import type {NodeViewProps} from "@tiptap/core"
|
||||
import {NodeViewWrapper} from "svelte-tiptap"
|
||||
import {displayUrl} from "@welshman/lib"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
|
||||
export let node: NodeViewProps["node"]
|
||||
export let selected: NodeViewProps["selected"]
|
||||
</script>
|
||||
|
||||
<NodeViewWrapper class="inline-block">
|
||||
<Link
|
||||
external
|
||||
href={node.attrs.url}
|
||||
class={cx("link-content", {"link-content-selected": selected})}>
|
||||
<Icon icon="link-round" size={3} class="inline-block" />
|
||||
{displayUrl(node.attrs.url)}
|
||||
</Link>
|
||||
</NodeViewWrapper>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import type {NodeViewProps} from "@tiptap/core"
|
||||
import {NodeViewWrapper} from "svelte-tiptap"
|
||||
import {displayProfile} from "@welshman/util"
|
||||
import {deriveProfile} from "@welshman/app"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
|
||||
export let node: NodeViewProps["node"]
|
||||
export let selected: NodeViewProps["selected"]
|
||||
|
||||
$: profile = deriveProfile(node.attrs.pubkey, node.attrs.relays)
|
||||
</script>
|
||||
|
||||
<NodeViewWrapper class="inline">
|
||||
<Link
|
||||
external
|
||||
href="https://njump.me/{node.attrs.nprofile}"
|
||||
class={cx("link-content", {"link-content-selected": selected})}>
|
||||
@{displayProfile($profile)}
|
||||
</Link>
|
||||
</NodeViewWrapper>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import type {NodeViewProps} from "@tiptap/core"
|
||||
import {NodeViewWrapper} from "svelte-tiptap"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
|
||||
export let node: NodeViewProps["node"]
|
||||
</script>
|
||||
|
||||
<NodeViewWrapper class="link-content inline">
|
||||
<Icon icon="paperclip" size={3} class="inline-block translate-y-px" />
|
||||
{node.attrs.file.name}
|
||||
</NodeViewWrapper>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import {deriveProfileDisplay} from "@welshman/app"
|
||||
|
||||
export let value
|
||||
|
||||
const display = deriveProfileDisplay(value)
|
||||
</script>
|
||||
|
||||
@{$display}
|
||||
@@ -0,0 +1,96 @@
|
||||
<svelte:options accessors />
|
||||
|
||||
<script lang="ts">
|
||||
import {throttle} from "throttle-debounce"
|
||||
import {slide} from "svelte/transition"
|
||||
import {clamp} from "@welshman/lib"
|
||||
import {theme} from "@app/theme"
|
||||
|
||||
export let term
|
||||
export let search
|
||||
export let select
|
||||
export let component
|
||||
export let loading = false
|
||||
export let allowCreate = false
|
||||
|
||||
let index = 0
|
||||
let element: Element
|
||||
let items: string[] = []
|
||||
|
||||
$: populateItems(term)
|
||||
|
||||
const populateItems = throttle(300, term => {
|
||||
items = $search.searchValues(term).slice(0, 30)
|
||||
})
|
||||
|
||||
const setIndex = (newIndex: number, block: any) => {
|
||||
index = clamp([0, items.length - 1], newIndex)
|
||||
element.querySelector(`button:nth-child(${index})`)?.scrollIntoView({block})
|
||||
}
|
||||
|
||||
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 items.length > 0 || (term && allowCreate)}
|
||||
<div
|
||||
data-theme={$theme}
|
||||
bind:this={element}
|
||||
transition:slide|local={{duration: 100}}
|
||||
class="mt-2 max-h-[350px] overflow-y-auto overflow-x-hidden shadow-xl">
|
||||
{#if term && allowCreate}
|
||||
<button
|
||||
class="white-space-nowrap block w-full min-w-0 cursor-pointer overflow-x-hidden text-ellipsis px-4 py-2 text-left transition-colors hover:bg-primary hover:text-primary-content"
|
||||
on:mousedown|preventDefault
|
||||
on:click|preventDefault={() => select(term)}>
|
||||
Use "<svelte:component this={component} value={term} />"
|
||||
</button>
|
||||
{/if}
|
||||
{#each items as value, i (value)}
|
||||
<button
|
||||
class="white-space-nowrap block w-full min-w-0 cursor-pointer overflow-x-hidden text-ellipsis px-4 py-2 text-left transition-colors hover:bg-primary hover:text-primary-content"
|
||||
class:bg-primary={index === i}
|
||||
class:text-primary-content={index === i}
|
||||
on:mousedown|preventDefault
|
||||
on:click|preventDefault={() => select(value)}>
|
||||
<svelte:component this={component} {value} />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{#if loading}
|
||||
<div transition:slide|local class="bg-tinted-700 flex gap-2 px-4 py-2 text-neutral-200">
|
||||
<div>
|
||||
<i class="fa fa-circle-notch fa-spin" />
|
||||
</div>
|
||||
Loading more options...
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -68,7 +68,7 @@ export const createSuggestions = (options: SuggestionsOptions) =>
|
||||
|
||||
popover = tippy("body", {
|
||||
getReferenceClientRect: props.clientRect as any,
|
||||
appendTo: document.querySelector('dialog[open]') || document.body,
|
||||
appendTo: document.querySelector("dialog[open]") || document.body,
|
||||
content: target,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
@@ -0,0 +1,170 @@
|
||||
import type {Writable} from "svelte/store"
|
||||
import {nprofileEncode} from "nostr-tools/nip19"
|
||||
import {SvelteNodeViewRenderer} from "svelte-tiptap"
|
||||
import Code from "@tiptap/extension-code"
|
||||
import CodeBlock from "@tiptap/extension-code-block"
|
||||
import Document from "@tiptap/extension-document"
|
||||
import Dropcursor from "@tiptap/extension-dropcursor"
|
||||
import Gapcursor from "@tiptap/extension-gapcursor"
|
||||
import History from "@tiptap/extension-history"
|
||||
import Paragraph from "@tiptap/extension-paragraph"
|
||||
import Text from "@tiptap/extension-text"
|
||||
import HardBreakExtension from "@tiptap/extension-hard-break"
|
||||
import {
|
||||
Bolt11Extension,
|
||||
NProfileExtension,
|
||||
NEventExtension,
|
||||
NAddrExtension,
|
||||
ImageExtension,
|
||||
VideoExtension,
|
||||
FileUploadExtension,
|
||||
} from "nostr-editor"
|
||||
import type {StampedEvent} from "@welshman/util"
|
||||
import {signer, profileSearch} from "@welshman/app"
|
||||
import {createSuggestions} from "./Suggestions"
|
||||
import {LinkExtension} from "./LinkExtension"
|
||||
import EditMention from "./EditMention.svelte"
|
||||
import EditEvent from "./EditEvent.svelte"
|
||||
import EditImage from "./EditImage.svelte"
|
||||
import EditBolt11 from "./EditBolt11.svelte"
|
||||
import EditVideo from "./EditVideo.svelte"
|
||||
import EditLink from "./EditLink.svelte"
|
||||
import Suggestions from "./Suggestions.svelte"
|
||||
import SuggestionProfile from "./SuggestionProfile.svelte"
|
||||
import {uploadFiles, asInline} from "./util"
|
||||
|
||||
export {
|
||||
createSuggestions,
|
||||
LinkExtension,
|
||||
EditMention,
|
||||
EditEvent,
|
||||
EditImage,
|
||||
EditBolt11,
|
||||
EditVideo,
|
||||
EditLink,
|
||||
Suggestions,
|
||||
SuggestionProfile,
|
||||
}
|
||||
export * from "./util"
|
||||
|
||||
type EditorOptions = {
|
||||
submit: () => void
|
||||
loading: Writable<boolean>
|
||||
getPubkeyHints: (pubkey: string) => string[]
|
||||
submitOnEnter?: boolean
|
||||
}
|
||||
|
||||
export const getModifiedHardBreakExtension = () =>
|
||||
HardBreakExtension.extend({
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
"Shift-Enter": () => this.editor.commands.setHardBreak(),
|
||||
"Mod-Enter": () => this.editor.commands.setHardBreak(),
|
||||
Enter: () => {
|
||||
if (this.editor.getText().trim()) {
|
||||
uploadFiles(this.editor)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const getEditorOptions = ({
|
||||
submit,
|
||||
loading,
|
||||
getPubkeyHints,
|
||||
submitOnEnter,
|
||||
}: EditorOptions) => {
|
||||
return {
|
||||
content: "",
|
||||
autofocus: true,
|
||||
extensions: [
|
||||
Code,
|
||||
CodeBlock,
|
||||
Document,
|
||||
Dropcursor,
|
||||
Gapcursor,
|
||||
History,
|
||||
Paragraph,
|
||||
Text,
|
||||
submitOnEnter ? getModifiedHardBreakExtension() : HardBreakExtension,
|
||||
LinkExtension.extend({
|
||||
addNodeView: () => SvelteNodeViewRenderer(EditLink),
|
||||
}),
|
||||
Bolt11Extension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditBolt11)})),
|
||||
NProfileExtension.extend({
|
||||
addNodeView: () => SvelteNodeViewRenderer(EditMention),
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
createSuggestions({
|
||||
char: "@",
|
||||
name: "nprofile",
|
||||
editor: this.editor,
|
||||
search: profileSearch,
|
||||
select: (pubkey: string, props: any) => {
|
||||
const relays = getPubkeyHints(pubkey)
|
||||
const nprofile = nprofileEncode({pubkey, relays})
|
||||
|
||||
return props.command({pubkey, nprofile, relays})
|
||||
},
|
||||
suggestionComponent: SuggestionProfile,
|
||||
suggestionsComponent: Suggestions,
|
||||
}),
|
||||
]
|
||||
},
|
||||
}),
|
||||
NEventExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditEvent)})),
|
||||
NAddrExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditEvent)})),
|
||||
ImageExtension.extend(
|
||||
asInline({addNodeView: () => SvelteNodeViewRenderer(EditImage)}),
|
||||
).configure({defaultUploadUrl: "https://nostr.build", defaultUploadType: "nip96"}),
|
||||
VideoExtension.extend(
|
||||
asInline({addNodeView: () => SvelteNodeViewRenderer(EditVideo)}),
|
||||
).configure({defaultUploadUrl: "https://nostr.build", defaultUploadType: "nip96"}),
|
||||
FileUploadExtension.configure({
|
||||
immediateUpload: false,
|
||||
sign: (event: StampedEvent) => {
|
||||
loading.set(true)
|
||||
|
||||
return signer.get()!.sign(event)
|
||||
},
|
||||
onComplete: () => {
|
||||
loading.set(false)
|
||||
submit()
|
||||
},
|
||||
}),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
type ViewOptions = {
|
||||
content: string
|
||||
}
|
||||
|
||||
export const getViewOptions = ({content}: ViewOptions) => ({
|
||||
content,
|
||||
editable: false,
|
||||
shouldRerenderOnTransaction: false,
|
||||
extensions: [
|
||||
Code,
|
||||
CodeBlock,
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
LinkExtension.extend({
|
||||
addNodeView: () => SvelteNodeViewRenderer(EditLink),
|
||||
}),
|
||||
Bolt11Extension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditBolt11)})),
|
||||
NProfileExtension.extend({
|
||||
addNodeView: () => SvelteNodeViewRenderer(EditMention),
|
||||
}),
|
||||
NEventExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditEvent)})),
|
||||
NAddrExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditEvent)})),
|
||||
ImageExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditImage)})),
|
||||
VideoExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(EditVideo)})),
|
||||
],
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
import type {JSONContent, PasteRuleMatch} from "@tiptap/core"
|
||||
import {Editor} from "@tiptap/core"
|
||||
import {choice} from "@welshman/lib"
|
||||
|
||||
export const asInline = (extend: Record<string, any>) => ({
|
||||
inline: true,
|
||||
group: "inline",
|
||||
...extend,
|
||||
})
|
||||
|
||||
export const createPasteRuleMatch = <T extends Record<string, unknown>>(
|
||||
match: RegExpMatchArray,
|
||||
data: T,
|
||||
): PasteRuleMatch => ({index: match.index!, replaceWith: match[2], text: match[0], match, data})
|
||||
|
||||
export const findNodes = (type: string, json: JSONContent) => {
|
||||
const results: JSONContent[] = []
|
||||
|
||||
for (const node of json.content || []) {
|
||||
if (node.type === type) {
|
||||
results.push(node)
|
||||
}
|
||||
|
||||
for (const result of findNodes(type, node)) {
|
||||
results.push(result)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
export const getEditorTags = (editor: Editor) => {
|
||||
const json = editor.getJSON()
|
||||
|
||||
const withAttrs = (f: any) => (attrs: any) => f(attrs as Record<string, any>)
|
||||
|
||||
const mentionTags = findNodes("nprofile", json).map(
|
||||
withAttrs(({pubkey, relays}: any) => ["p", pubkey, choice(relays), ""]),
|
||||
)
|
||||
|
||||
const imetaTags = findNodes("image", json).map(
|
||||
withAttrs(({src, sha256}: any) => ["imeta", `url ${src}`, `x ${sha256}`, `ox ${sha256}`]),
|
||||
)
|
||||
|
||||
return [...mentionTags, ...imetaTags]
|
||||
}
|
||||
|
||||
export const addFile = (editor: Editor) => editor.chain().selectFiles().run()
|
||||
|
||||
export const uploadFiles = (editor: Editor) => editor.chain().uploadFiles().run()
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "@lib/tiptap/util"
|
||||
export {createSuggestions} from "@lib/tiptap/Suggestions"
|
||||
export {LinkExtension} from "@lib/tiptap/LinkExtension"
|
||||
@@ -1,28 +0,0 @@
|
||||
import type {JSONContent, PasteRuleMatch} from "@tiptap/core"
|
||||
|
||||
export const asInline = (extend: Record<string, any>) => ({
|
||||
inline: true,
|
||||
group: "inline",
|
||||
...extend,
|
||||
})
|
||||
|
||||
export const createPasteRuleMatch = <T extends Record<string, unknown>>(
|
||||
match: RegExpMatchArray,
|
||||
data: T,
|
||||
): PasteRuleMatch => ({index: match.index!, replaceWith: match[2], text: match[0], match, data})
|
||||
|
||||
export const findNodes = (type: string, json: JSONContent) => {
|
||||
const results: JSONContent[] = []
|
||||
|
||||
for (const node of json.content || []) {
|
||||
if (node.type === type) {
|
||||
results.push(node)
|
||||
}
|
||||
|
||||
for (const result of findNodes(type, node)) {
|
||||
results.push(result)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
Reference in New Issue
Block a user