your personal website on atproto - mirror blento.app
at signup 138 lines 2.9 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 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>