your personal website on atproto - mirror
blento.app
1<script lang="ts">
2 import { onDestroy, onMount } from 'svelte';
3 import { Editor, type Content, type Extensions } from '@tiptap/core';
4 import StarterKit from '@tiptap/starter-kit';
5 import Image from '@tiptap/extension-image';
6 import Placeholder from '@tiptap/extension-placeholder';
7 import Link from '@tiptap/extension-link';
8 import { marked } from 'marked';
9 import { generateJSON } from '@tiptap/core';
10 import TurndownService from 'turndown';
11 import { RichTextLink } from './extensions/RichTextLink';
12 import type { Item } from '$lib/types';
13
14 let element: HTMLElement | undefined = $state();
15
16 let {
17 editor = $bindable(),
18 contentDict = $bindable(),
19 key = 'text',
20 placeholder = '',
21 defaultContent = '',
22 class: className
23 }: {
24 editor: Editor | null;
25 contentDict: Record<string, any>;
26 key: string;
27 placeholder?: string;
28 defaultContent?: string;
29 class?: string;
30 } = $props();
31
32 const update = async () => {
33 if (!editor) return {};
34
35 const html = editor.getHTML();
36
37 var turndownService = new TurndownService({
38 headingStyle: 'atx',
39 bulletListMarker: '-'
40 });
41 const markdown = turndownService.turndown(html);
42
43 contentDict[key] = markdown;
44 };
45
46 onMount(async () => {
47 if (!element || editor) return;
48
49 let json: Content = '';
50
51 try {
52 let html = await marked.parse(contentDict[key] ?? (defaultContent as string));
53
54 // parse to json
55 json = generateJSON(html, [
56 StarterKit.configure({
57 heading: false,
58 bulletList: false,
59 codeBlock: false
60 }),
61 Image.configure(),
62 RichTextLink.configure({
63 openOnClick: false
64 })
65 ]);
66 } catch (error) {
67 console.error(error);
68 }
69
70 let extensions: Extensions = [
71 StarterKit.configure({
72 heading: false,
73 bulletList: false,
74 codeBlock: false,
75 dropcursor: false
76 }),
77 Image.configure(),
78 Link.configure({
79 openOnClick: false
80 })
81 ];
82
83 if (placeholder) {
84 extensions.push(
85 Placeholder.configure({
86 placeholder: placeholder
87 })
88 );
89 }
90
91 editor = new Editor({
92 element: element,
93 extensions: extensions,
94 onTransaction: () => {
95 editor = editor;
96 },
97 onUpdate: () => {
98 update();
99 },
100 onDrop: () => {
101 return false;
102 },
103 content: json,
104
105 editorProps: {
106 attributes: {
107 class:
108 'outline-none w-full text-base-600 dark:text-base-400 prose dark:prose-invert prose-a:text-accent-500 prose-a:no-underline'
109 },
110 handleDOMEvents: { drop: () => false }
111 }
112 });
113 });
114
115 onDestroy(() => {
116 if (editor) {
117 editor.destroy();
118 }
119 });
120</script>
121
122<div class={['w-full cursor-text', className]} bind:this={element}></div>
123
124<style>
125 :global(.tiptap p.is-editor-empty:first-child::before) {
126 color: var(--color-base-800);
127 content: attr(data-placeholder);
128 float: left;
129 height: 0;
130 pointer-events: none;
131 }
132 :global(.dark .tiptap p.is-editor-empty:first-child::before) {
133 color: var(--color-base-200);
134 }
135</style>