Remove svelte from editor
This commit is contained in:
Generated
+75
-2220
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
build
|
||||||
@@ -1,80 +1,48 @@
|
|||||||
{
|
{
|
||||||
"name": "@welshman/editor",
|
"name": "@welshman/editor",
|
||||||
"version": "0.0.10",
|
"version": "0.0.11-pre.1",
|
||||||
"scripts": {
|
"author": "hodlbod",
|
||||||
"pub": "npm run check && npm run build && npm publish",
|
"license": "MIT",
|
||||||
"fix": "",
|
"description": "A batteries-included nostr editor.",
|
||||||
"dev": "vite dev",
|
"publishConfig": {
|
||||||
"build": "vite build && npm run package",
|
"access": "public"
|
||||||
"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",
|
"type": "module",
|
||||||
|
"files": [
|
||||||
|
"build"
|
||||||
|
],
|
||||||
|
"types": "./build/src/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./build/src/index.d.ts",
|
||||||
"svelte": "./dist/index.js"
|
"import": "./build/src/index.js",
|
||||||
|
"require": "./build/src/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"scripts": {
|
||||||
"@tiptap/core": "^2.9.1",
|
"pub": "npm run lint && npm run build && npm publish",
|
||||||
"@tiptap/extension-code": "^2.9.1",
|
"build": "gts clean && tsc",
|
||||||
"@tiptap/extension-code-block": "^2.9.1",
|
"lint": "gts lint",
|
||||||
"@tiptap/extension-document": "^2.9.1",
|
"fix": "gts fix"
|
||||||
"@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": "^0.0.4-pre.8",
|
|
||||||
"nostr-tools": "^2.8.1",
|
|
||||||
"svelte": "^4.0.0",
|
|
||||||
"svelte-tiptap": "^1.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"dependencies": {
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@tiptap/core": "^2.11.5",
|
||||||
"@sveltejs/package": "^2.0.0",
|
"@tiptap/extension-code": "^2.11.5",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
"@tiptap/extension-code-block": "^2.11.5",
|
||||||
"@tiptap/core": "^2.9.1",
|
"@tiptap/extension-document": "^2.11.5",
|
||||||
"@tiptap/extension-code": "^2.9.1",
|
"@tiptap/extension-dropcursor": "^2.11.5",
|
||||||
"@tiptap/extension-code-block": "^2.9.1",
|
"@tiptap/extension-gapcursor": "^2.11.5",
|
||||||
"@tiptap/extension-document": "^2.9.1",
|
"@tiptap/extension-hard-break": "^2.11.5",
|
||||||
"@tiptap/extension-dropcursor": "^2.9.1",
|
"@tiptap/extension-history": "^2.11.5",
|
||||||
"@tiptap/extension-gapcursor": "^2.9.1",
|
"@tiptap/extension-paragraph": "^2.11.5",
|
||||||
"@tiptap/extension-hard-break": "^2.9.1",
|
"@tiptap/extension-placeholder": "^2.11.5",
|
||||||
"@tiptap/extension-history": "^2.9.1",
|
"@tiptap/extension-text": "^2.11.5",
|
||||||
"@tiptap/extension-paragraph": "^2.9.1",
|
"@tiptap/pm": "^2.11.5",
|
||||||
"@tiptap/extension-placeholder": "^2.9.1",
|
"@tiptap/suggestion": "^2.11.5",
|
||||||
"@tiptap/extension-text": "^2.9.1",
|
"@welshman/lib": "^0.0.39",
|
||||||
"@tiptap/pm": "^2.9.1",
|
"@welshman/util": "^0.0.60",
|
||||||
"@welshman/lib": "~0.0.36",
|
"nostr-editor": "^0.0.4-pre.12",
|
||||||
"@welshman/util": "~0.0.53",
|
|
||||||
"nostr-editor": "^0.0.4-pre.7",
|
|
||||||
"nostr-tools": "^2.10.4",
|
"nostr-tools": "^2.10.4",
|
||||||
"publint": "^0.2.0",
|
"tippy.js": "^6.3.7"
|
||||||
"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
@@ -1,13 +0,0 @@
|
|||||||
// 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 {};
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<!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>
|
|
||||||
+1
-1
@@ -17,7 +17,7 @@ export const BreakOrSubmit = HardBreak.extend<BreakOrSubmitOptions>({
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
"Enter": () => {
|
Enter: () => {
|
||||||
if (this.options.aggressive) {
|
if (this.options.aggressive) {
|
||||||
this.options.submit()
|
this.options.submit()
|
||||||
|
|
||||||
+7
-9
@@ -1,7 +1,5 @@
|
|||||||
import type {StampedEvent, SignedEvent} from "@welshman/util"
|
import type {StampedEvent, SignedEvent} from "@welshman/util"
|
||||||
import {toNostrURI} from "@welshman/util"
|
|
||||||
import {deepMergeLeft} from "@welshman/lib"
|
import {deepMergeLeft} from "@welshman/lib"
|
||||||
import {SvelteNodeViewRenderer} from "svelte-tiptap"
|
|
||||||
import type {Extensions, AnyExtension} from "@tiptap/core"
|
import type {Extensions, AnyExtension} from "@tiptap/core"
|
||||||
import {CodeBlock} from "@tiptap/extension-code-block"
|
import {CodeBlock} from "@tiptap/extension-code-block"
|
||||||
import type {CodeBlockOptions} from "@tiptap/extension-code-block"
|
import type {CodeBlockOptions} from "@tiptap/extension-code-block"
|
||||||
@@ -39,7 +37,7 @@ import {
|
|||||||
import {WordCount} from "./WordCount.js"
|
import {WordCount} from "./WordCount.js"
|
||||||
import {CodeInline, type CodeInlineOptions} from "./CodeInline.js"
|
import {CodeInline, type CodeInlineOptions} from "./CodeInline.js"
|
||||||
import {BreakOrSubmit, type BreakOrSubmitOptions} from "./BreakOrSubmit.js"
|
import {BreakOrSubmit, type BreakOrSubmitOptions} from "./BreakOrSubmit.js"
|
||||||
import {EditBolt11, EditMedia, EditEvent, EditMention} from "../components/index.js"
|
import {MentionNodeView, Bolt11NodeView, MediaNodeView, EventNodeView} from "../nodeviews/index.js"
|
||||||
|
|
||||||
export type ChildExtensionOptions<C = any, E = any> =
|
export type ChildExtensionOptions<C = any, E = any> =
|
||||||
| false
|
| false
|
||||||
@@ -116,7 +114,7 @@ export const WelshmanExtension = NostrExtension.extend<WelshmanOptions>({
|
|||||||
group: "inline",
|
group: "inline",
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
addNodeView: () => SvelteNodeViewRenderer(EditBolt11),
|
addNodeView: () => Bolt11NodeView,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
image: {
|
image: {
|
||||||
@@ -127,7 +125,7 @@ export const WelshmanExtension = NostrExtension.extend<WelshmanOptions>({
|
|||||||
defaultUploadType,
|
defaultUploadType,
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
addNodeView: () => SvelteNodeViewRenderer(EditMedia),
|
addNodeView: () => MediaNodeView,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
video: {
|
video: {
|
||||||
@@ -138,7 +136,7 @@ export const WelshmanExtension = NostrExtension.extend<WelshmanOptions>({
|
|||||||
defaultUploadType,
|
defaultUploadType,
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
addNodeView: () => SvelteNodeViewRenderer(EditMedia),
|
addNodeView: () => MediaNodeView,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
nevent: {
|
nevent: {
|
||||||
@@ -147,7 +145,7 @@ export const WelshmanExtension = NostrExtension.extend<WelshmanOptions>({
|
|||||||
group: "inline",
|
group: "inline",
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
addNodeView: () => SvelteNodeViewRenderer(EditEvent),
|
addNodeView: () => EventNodeView,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
naddr: {
|
naddr: {
|
||||||
@@ -156,12 +154,12 @@ export const WelshmanExtension = NostrExtension.extend<WelshmanOptions>({
|
|||||||
group: "inline",
|
group: "inline",
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
addNodeView: () => SvelteNodeViewRenderer(EditEvent),
|
addNodeView: () => EventNodeView,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
nprofile: {
|
nprofile: {
|
||||||
extend: {
|
extend: {
|
||||||
addNodeView: () => SvelteNodeViewRenderer(EditMention),
|
addNodeView: () => MentionNodeView,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
breakOrSubmit: {
|
breakOrSubmit: {
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import "./index.css"
|
||||||
|
|
||||||
|
export * from "./nodeviews/index.js"
|
||||||
|
export * from "./extensions/index.js"
|
||||||
|
export * from "./plugins/index.js"
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<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.bech32).slice(0, 16)}...
|
|
||||||
</button>
|
|
||||||
</NodeViewWrapper>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<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.bech32.slice(0, 16)}...
|
|
||||||
</button>
|
|
||||||
</NodeViewWrapper>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
export let value
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{value}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
<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}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
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"
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import './index.css'
|
|
||||||
|
|
||||||
export * from './components/index.js'
|
|
||||||
export * from './extensions/index.js'
|
|
||||||
export * from './plugins/index.js'
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
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 {makeNProfileAttrs} from 'nostr-editor'
|
|
||||||
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 bech32 = nprofileEncode({pubkey, relays})
|
|
||||||
|
|
||||||
return props.command(makeNProfileAttrs(bech32, {}))
|
|
||||||
},
|
|
||||||
...options,
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type {NodeViewProps} from "@tiptap/core"
|
||||||
|
|
||||||
|
export const Bolt11NodeView = ({node}: NodeViewProps) => {
|
||||||
|
const dom = document.createElement("span")
|
||||||
|
|
||||||
|
dom.classList.add("tiptap-object")
|
||||||
|
dom.innerText = `${node.attrs.lnbc.slice(0, 16)}...`
|
||||||
|
|
||||||
|
return {
|
||||||
|
dom,
|
||||||
|
selectNode() {
|
||||||
|
dom.classList.add("tiptap-active")
|
||||||
|
},
|
||||||
|
deselectNode() {
|
||||||
|
dom.classList.remove("tiptap-active")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type {NodeViewProps} from "@tiptap/core"
|
||||||
|
import {fromNostrURI} from "@welshman/util"
|
||||||
|
|
||||||
|
export const EventNodeView = ({node}: NodeViewProps) => {
|
||||||
|
const dom = document.createElement("span")
|
||||||
|
|
||||||
|
dom.classList.add("tiptap-object")
|
||||||
|
dom.innerText = `${fromNostrURI(node.attrs.bech32).slice(0, 16)}...`
|
||||||
|
|
||||||
|
return {
|
||||||
|
dom,
|
||||||
|
selectNode() {
|
||||||
|
dom.classList.add("tiptap-active")
|
||||||
|
},
|
||||||
|
deselectNode() {
|
||||||
|
dom.classList.remove("tiptap-active")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import type {NodeViewProps} from "@tiptap/core"
|
||||||
|
|
||||||
|
export const MediaNodeView = ({node}: NodeViewProps) => {
|
||||||
|
const dom = document.createElement("span")
|
||||||
|
|
||||||
|
const syncUploading = (node: NodeViewProps["node"]) => {
|
||||||
|
if (node.attrs.uploading) {
|
||||||
|
dom.classList.add("tiptap-uploading")
|
||||||
|
} else {
|
||||||
|
dom.classList.remove("tiptap-uploading")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dom.classList.add("tiptap-object")
|
||||||
|
dom.innerText = node.attrs.file?.name || node.attrs.src
|
||||||
|
|
||||||
|
syncUploading(node)
|
||||||
|
|
||||||
|
return {
|
||||||
|
dom,
|
||||||
|
update(node: NodeViewProps["node"]) {
|
||||||
|
syncUploading(node)
|
||||||
|
},
|
||||||
|
selectNode() {
|
||||||
|
dom.classList.add("tiptap-active")
|
||||||
|
},
|
||||||
|
deselectNode() {
|
||||||
|
dom.classList.remove("tiptap-active")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type {NodeViewProps} from "@tiptap/core"
|
||||||
|
|
||||||
|
export const MentionNodeView = ({node}: NodeViewProps) => {
|
||||||
|
const dom = document.createElement("span")
|
||||||
|
|
||||||
|
dom.classList.add("tiptap-object")
|
||||||
|
dom.textContent = `@${node.attrs.bech32.slice(0, 16)}...`
|
||||||
|
|
||||||
|
return {
|
||||||
|
dom,
|
||||||
|
selectNode() {
|
||||||
|
dom.classList.add("tiptap-active")
|
||||||
|
},
|
||||||
|
deselectNode() {
|
||||||
|
dom.classList.remove("tiptap-active")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./MentionNodeView.js"
|
||||||
|
export * from "./Bolt11NodeView.js"
|
||||||
|
export * from "./EventNodeView.js"
|
||||||
|
export * from "./MediaNodeView.js"
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
import type {Instance} from "tippy.js"
|
||||||
|
import tippy from "tippy.js"
|
||||||
|
import {nprofileEncode} from "nostr-tools/nip19"
|
||||||
|
import type {Editor} from "@tiptap/core"
|
||||||
|
import {makeNProfileAttrs} from "nostr-editor"
|
||||||
|
import {PluginKey} from "@tiptap/pm/state"
|
||||||
|
import Suggestion from "@tiptap/suggestion"
|
||||||
|
import {throttle, enumerate, clamp} from "@welshman/lib"
|
||||||
|
|
||||||
|
export type CreateSuggestion = (item: string) => HTMLElement
|
||||||
|
|
||||||
|
export const defaultCreateSuggestion = (item: string) => {
|
||||||
|
const span = document.createElement("span")
|
||||||
|
|
||||||
|
span.textContent = item
|
||||||
|
|
||||||
|
return span
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SuggestionsWrapperProps = {
|
||||||
|
term: string
|
||||||
|
allowCreate: boolean
|
||||||
|
select: (value: string) => void
|
||||||
|
search: (term: string) => string[]
|
||||||
|
createSuggestion: CreateSuggestion
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISuggestionsWrapperConstructor {
|
||||||
|
new (target: HTMLElement, props: SuggestionsWrapperProps): ISuggestionsWrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISuggestionsWrapper {
|
||||||
|
setProps: (props: SuggestionsWrapperProps) => void
|
||||||
|
onKeyDown: (event: Event) => boolean
|
||||||
|
destroy: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSuggestionsWrapper(
|
||||||
|
ctor: ISuggestionsWrapperConstructor,
|
||||||
|
target: HTMLElement,
|
||||||
|
props: SuggestionsWrapperProps,
|
||||||
|
): ISuggestionsWrapper {
|
||||||
|
return new ctor(target, props)
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DefaultSuggestionsWrapper implements ISuggestionsWrapper {
|
||||||
|
index = 0
|
||||||
|
items: string[] = []
|
||||||
|
target: HTMLElement
|
||||||
|
content: HTMLElement
|
||||||
|
props: SuggestionsWrapperProps
|
||||||
|
|
||||||
|
constructor(target: HTMLElement, props: SuggestionsWrapperProps) {
|
||||||
|
this.target = target
|
||||||
|
this.props = props
|
||||||
|
this.content = document.createElement("div")
|
||||||
|
this.content.classList.add("tiptap-suggestions__content")
|
||||||
|
|
||||||
|
target.appendChild(this.content)
|
||||||
|
target.classList.add("tiptap-suggestions")
|
||||||
|
|
||||||
|
this.search()
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
search = throttle(300, () => {
|
||||||
|
const {term, search} = this.props
|
||||||
|
|
||||||
|
this.items = search(term).slice(0, 5)
|
||||||
|
})
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {index} = this
|
||||||
|
const {select, term, allowCreate, createSuggestion} = this.props
|
||||||
|
|
||||||
|
this.content.innerHTML = ""
|
||||||
|
|
||||||
|
if (term && allowCreate && this.items.includes(term)) {
|
||||||
|
const button = document.createElement("button")
|
||||||
|
|
||||||
|
button.classList.add("tiptap-suggestions__create")
|
||||||
|
|
||||||
|
button.addEventListener("mousedown", (event: Event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
})
|
||||||
|
|
||||||
|
button.addEventListener("click", (event: Event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
select(term)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.content.appendChild(button)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [i, item] of enumerate(this.items)) {
|
||||||
|
const button = document.createElement("button")
|
||||||
|
|
||||||
|
button.classList.add("tiptap-suggestions__item")
|
||||||
|
|
||||||
|
if (i === index) {
|
||||||
|
button.classList.add("tiptap-suggestions__selected")
|
||||||
|
}
|
||||||
|
|
||||||
|
button.addEventListener("mousedown", (event: Event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
})
|
||||||
|
|
||||||
|
button.addEventListener("click", (event: Event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
select(item)
|
||||||
|
})
|
||||||
|
|
||||||
|
button.appendChild(createSuggestion(item))
|
||||||
|
|
||||||
|
this.content.appendChild(button)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIndex(index: number) {
|
||||||
|
this.index = clamp([0, this.items.length - 1], index)
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
setProps(props: SuggestionsWrapperProps) {
|
||||||
|
this.props = props
|
||||||
|
this.search()
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyDown(event: any) {
|
||||||
|
const {index, items} = this
|
||||||
|
const {term, select, allowCreate} = this.props
|
||||||
|
|
||||||
|
if (["Enter", "Tab"].includes(event.code)) {
|
||||||
|
const value = items[index]
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
select(value)
|
||||||
|
|
||||||
|
return true
|
||||||
|
} else if (term && allowCreate) {
|
||||||
|
select(term)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.code === "Space" && term && allowCreate) {
|
||||||
|
select(term)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.code === "ArrowUp") {
|
||||||
|
this.setIndex(index - 1)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.code === "ArrowDown") {
|
||||||
|
this.setIndex(index + 1)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.target.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TippySuggestionOptions = {
|
||||||
|
char: string
|
||||||
|
name: string
|
||||||
|
editor: Editor
|
||||||
|
search: (term: string) => string[]
|
||||||
|
select: (value: string, props: any) => void
|
||||||
|
allowCreate?: boolean
|
||||||
|
createSuggestion?: CreateSuggestion
|
||||||
|
suggestionsWrapper?: ISuggestionsWrapperConstructor
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TippySuggestion = ({
|
||||||
|
char,
|
||||||
|
name,
|
||||||
|
editor,
|
||||||
|
search,
|
||||||
|
select,
|
||||||
|
allowCreate = false,
|
||||||
|
createSuggestion = defaultCreateSuggestion,
|
||||||
|
suggestionsWrapper = DefaultSuggestionsWrapper,
|
||||||
|
}: 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 wrapper: ISuggestionsWrapper
|
||||||
|
|
||||||
|
const mapProps = (props: any) => ({
|
||||||
|
term: props.query,
|
||||||
|
search,
|
||||||
|
allowCreate,
|
||||||
|
createSuggestion,
|
||||||
|
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()
|
||||||
|
|
||||||
|
wrapper = createSuggestionsWrapper(suggestionsWrapper, target, mapProps(props))
|
||||||
|
},
|
||||||
|
onUpdate: props => {
|
||||||
|
if (props.query) {
|
||||||
|
popover[0].show()
|
||||||
|
} else {
|
||||||
|
popover[0].hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.setProps(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(wrapper.onKeyDown(props.event))
|
||||||
|
},
|
||||||
|
onExit: () => {
|
||||||
|
popover[0].destroy()
|
||||||
|
wrapper.destroy()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export type MentionSuggestionOptions = Partial<TippySuggestionOptions> & {
|
||||||
|
editor: Editor
|
||||||
|
search: (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 bech32 = nprofileEncode({pubkey, relays})
|
||||||
|
|
||||||
|
return props.command(makeNProfileAttrs(bech32, {}))
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<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.
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,10 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,17 +1,13 @@
|
|||||||
{
|
{
|
||||||
"extends": "./.svelte-kit/tsconfig.json",
|
"extends": "../../node_modules/gts/tsconfig-google.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"allowJs": true,
|
"rootDir": ".",
|
||||||
"checkJs": true,
|
"outDir": "build",
|
||||||
"esModuleInterop": true,
|
"module": "nodenext",
|
||||||
"forceConsistentCasingInFileNames": true,
|
"moduleResolution": "nodenext",
|
||||||
"resolveJsonModule": true,
|
"skipLibCheck": true,
|
||||||
"skipLibCheck": true,
|
"lib": ["esnext", "dom"]
|
||||||
"sourceMap": true,
|
},
|
||||||
"strict": true,
|
|
||||||
"module": "NodeNext",
|
|
||||||
"moduleResolution": "NodeNext"
|
|
||||||
},
|
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.ts",
|
"src/**/*.ts",
|
||||||
"test/**/*.ts"
|
"test/**/*.ts"
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
|
||||||
import { defineConfig } from 'vite';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [sveltekit()]
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user