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 onupdate
24 }: {
25 editor?: Editor | null;
26 contentDict: Record<string, any>;
27 key: string;
28 placeholder?: string;
29 defaultContent?: string;
30 class?: string;
31 onupdate?: (content: string) => void;
32 } = $props();
33
34 const update = async () => {
35 if (!editor) return {};
36
37 const html = editor.getHTML();
38
39 var turndownService = new TurndownService({
40 headingStyle: 'atx',
41 bulletListMarker: '-'
42 });
43 const markdown = turndownService.turndown(html);
44
45 contentDict[key] = markdown;
46
47 onupdate?.(markdown);
48 };
49
50 onMount(async () => {
51 if (!element || editor) return;
52
53 let json: Content = '';
54
55 try {
56 let html = await marked.parse(contentDict[key] ?? (defaultContent as string));
57
58 // parse to json
59 json = generateJSON(html, [
60 StarterKit.configure({
61 heading: false,
62 bulletList: false,
63 codeBlock: false
64 }),
65 Image.configure(),
66 RichTextLink.configure({
67 openOnClick: false
68 })
69 ]);
70 } catch (error) {
71 console.error(error);
72 }
73
74 let extensions: Extensions = [
75 StarterKit.configure({
76 heading: false,
77 bulletList: false,
78 codeBlock: false,
79 dropcursor: false
80 }),
81 Image.configure(),
82 Link.configure({
83 openOnClick: false
84 })
85 ];
86
87 if (placeholder) {
88 extensions.push(
89 Placeholder.configure({
90 placeholder: placeholder
91 })
92 );
93 }
94
95 editor = new Editor({
96 element: element,
97 extensions: extensions,
98 onTransaction: () => {
99 editor = editor;
100 },
101 onUpdate: () => {
102 update();
103 },
104 onDrop: () => {
105 return false;
106 },
107 content: json,
108
109 editorProps: {
110 attributes: {
111 class: 'outline-none w-full'
112 },
113 handleDOMEvents: { drop: () => false }
114 }
115 });
116 });
117
118 onDestroy(() => {
119 if (editor) {
120 editor.destroy();
121 }
122 });
123</script>
124
125<div class={['w-full cursor-text', className]} bind:this={element}></div>
126
127<style>
128 :global(.tiptap p.is-editor-empty:first-child::before) {
129 color: var(--color-base-800);
130 content: attr(data-placeholder);
131 float: left;
132 height: 0;
133 pointer-events: none;
134 }
135 :global(.dark .tiptap p.is-editor-empty:first-child::before) {
136 color: var(--color-base-200);
137 }
138</style>