your personal website on atproto - mirror blento.app
at mail-icon 130 lines 2.7 kB view raw
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>