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