[READ-ONLY] Mirror of https://github.com/flo-bit/ui-kit. 🦊 fox ui, svelte 5 and tailwind 4 flo-bit.dev/ui-kit/
svelte tailwindcss ui-components
0

Configure Feed

Select the types of activity you want to include in your feed.

commit

Florian (Mar 1, 2026, 2:29 AM +0100) 7e0dbc75 0dcf76c5

+228 -264
+38 -24
pnpm-lock.yaml
··· 599 599 600 600 packages/text: 601 601 dependencies: 602 + '@floating-ui/dom': 603 + specifier: ^1.7.0 604 + version: 1.7.5 602 605 '@foxui/core': 603 606 specifier: workspace:* 604 607 version: link:../core ··· 617 620 '@tiptap/extension-dropcursor': 618 621 specifier: ^3.20.0 619 622 version: 3.20.0(@tiptap/extensions@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)) 623 + '@tiptap/extension-floating-menu': 624 + specifier: ^3.20.0 625 + version: 3.20.0(@floating-ui/dom@1.7.5)(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0) 620 626 '@tiptap/extension-history': 621 627 specifier: ^3.20.0 622 628 version: 3.20.0(@tiptap/extensions@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)) ··· 644 650 '@tiptap/extension-underline': 645 651 specifier: ^3.20.0 646 652 version: 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0)) 653 + '@tiptap/markdown': 654 + specifier: ^3.20.0 655 + version: 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0) 647 656 '@tiptap/pm': 648 657 specifier: ^3.20.0 649 658 version: 3.20.0 ··· 664 673 version: link:@tiptap/pm/state 665 674 svelte-tiptap: 666 675 specifier: ^3.0.1 667 - version: 3.0.1(@floating-ui/dom@1.7.5)(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/extension-bubble-menu@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))(@tiptap/extension-floating-menu@2.27.2(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)(svelte@5.53.2) 668 - tippy.js: 669 - specifier: ^6.3.7 670 - version: 6.3.7 676 + version: 3.0.1(@floating-ui/dom@1.7.5)(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/extension-bubble-menu@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))(@tiptap/extension-floating-menu@3.20.0(@floating-ui/dom@1.7.5)(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)(svelte@5.53.2) 671 677 devDependencies: 672 678 '@eslint/compat': 673 679 specifier: ^2.0.2 ··· 1488 1494 '@polka/url@1.0.0-next.29': 1489 1495 resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==, tarball: https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz} 1490 1496 1491 - '@popperjs/core@2.11.8': 1492 - resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==, tarball: https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz} 1493 - 1494 1497 '@remirror/core-constants@3.0.0': 1495 1498 resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==, tarball: https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz} 1496 1499 ··· 1912 1915 peerDependencies: 1913 1916 '@tiptap/extensions': ^3.20.0 1914 1917 1915 - '@tiptap/extension-floating-menu@2.27.2': 1916 - resolution: {integrity: sha512-GUN6gPIGXS7ngRJOwdSmtBRBDt9Kt9CM/9pSwKebhLJ+honFoNA+Y6IpVyDvvDMdVNgBchiJLs6qA5H97gAePQ==, tarball: https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.27.2.tgz} 1918 + '@tiptap/extension-floating-menu@3.20.0': 1919 + resolution: {integrity: sha512-rYs4Bv5pVjqZ/2vvR6oe7ammZapkAwN51As/WDbemvYDjfOGRqK58qGauUjYZiDzPOEIzI2mxGwsZ4eJhPW4Ig==, tarball: https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.20.0.tgz} 1917 1920 peerDependencies: 1918 - '@tiptap/core': ^2.7.0 1919 - '@tiptap/pm': ^2.7.0 1921 + '@floating-ui/dom': ^1.0.0 1922 + '@tiptap/core': ^3.20.0 1923 + '@tiptap/pm': ^3.20.0 1920 1924 1921 1925 '@tiptap/extension-gapcursor@3.20.0': 1922 1926 resolution: {integrity: sha512-P/LasfvG9/qFq43ZAlNbAnPnXC+/RJf49buTrhtFvI9Zg0+Lbpjx1oh6oMHB19T88Y28KtrckfFZ8aTSUWDq6w==, tarball: https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.20.0.tgz} ··· 2013 2017 2014 2018 '@tiptap/extensions@3.20.0': 2015 2019 resolution: {integrity: sha512-HIsXX942w3nbxEQBlMAAR/aa6qiMBEP7CsSMxaxmTIVAmW35p6yUASw6GdV1u0o3lCZjXq2OSRMTskzIqi5uLg==, tarball: https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.20.0.tgz} 2020 + peerDependencies: 2021 + '@tiptap/core': ^3.20.0 2022 + '@tiptap/pm': ^3.20.0 2023 + 2024 + '@tiptap/markdown@3.20.0': 2025 + resolution: {integrity: sha512-3vUxs8tsVIf/KWKLWjFsTqrjuaTYJY9rawDL5sio9NwlqFWDuWpHEVJcqbQXJUrgQSh12AZoTKyfgiEqkAGI3Q==, tarball: https://registry.npmjs.org/@tiptap/markdown/-/markdown-3.20.0.tgz} 2016 2026 peerDependencies: 2017 2027 '@tiptap/core': ^3.20.0 2018 2028 '@tiptap/pm': ^3.20.0 ··· 2897 2907 resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==, tarball: https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz} 2898 2908 hasBin: true 2899 2909 2910 + marked@17.0.3: 2911 + resolution: {integrity: sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A==, tarball: https://registry.npmjs.org/marked/-/marked-17.0.3.tgz} 2912 + engines: {node: '>= 20'} 2913 + hasBin: true 2914 + 2900 2915 mdast-util-from-markdown@2.0.3: 2901 2916 resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==, tarball: https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz} 2902 2917 ··· 3678 3693 tinyglobby@0.2.15: 3679 3694 resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==, tarball: https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz} 3680 3695 engines: {node: '>=12.0.0'} 3681 - 3682 - tippy.js@6.3.7: 3683 - resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==, tarball: https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz} 3684 3696 3685 3697 tlds@1.261.0: 3686 3698 resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==, tarball: https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz} ··· 4567 4579 4568 4580 '@polka/url@1.0.0-next.29': {} 4569 4581 4570 - '@popperjs/core@2.11.8': {} 4571 - 4572 4582 '@remirror/core-constants@3.0.0': {} 4573 4583 4574 4584 '@rollup/rollup-android-arm-eabi@4.59.0': ··· 4959 4969 dependencies: 4960 4970 '@tiptap/extensions': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0) 4961 4971 4962 - '@tiptap/extension-floating-menu@2.27.2(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)': 4972 + '@tiptap/extension-floating-menu@3.20.0(@floating-ui/dom@1.7.5)(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)': 4963 4973 dependencies: 4974 + '@floating-ui/dom': 1.7.5 4964 4975 '@tiptap/core': 3.20.0(@tiptap/pm@3.20.0) 4965 4976 '@tiptap/pm': 3.20.0 4966 - tippy.js: 6.3.7 4967 4977 4968 4978 '@tiptap/extension-gapcursor@3.20.0(@tiptap/extensions@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))': 4969 4979 dependencies: ··· 5045 5055 dependencies: 5046 5056 '@tiptap/core': 3.20.0(@tiptap/pm@3.20.0) 5047 5057 '@tiptap/pm': 3.20.0 5058 + 5059 + '@tiptap/markdown@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)': 5060 + dependencies: 5061 + '@tiptap/core': 3.20.0(@tiptap/pm@3.20.0) 5062 + '@tiptap/pm': 3.20.0 5063 + marked: 17.0.3 5048 5064 5049 5065 '@tiptap/pm@3.20.0': 5050 5066 dependencies: ··· 6104 6120 punycode.js: 2.3.1 6105 6121 uc.micro: 2.1.0 6106 6122 6123 + marked@17.0.3: {} 6124 + 6107 6125 mdast-util-from-markdown@2.0.3: 6108 6126 dependencies: 6109 6127 '@types/mdast': 4.0.4 ··· 6847 6865 runed: 0.28.0(svelte@5.53.2) 6848 6866 svelte: 5.53.2 6849 6867 6850 - svelte-tiptap@3.0.1(@floating-ui/dom@1.7.5)(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/extension-bubble-menu@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))(@tiptap/extension-floating-menu@2.27.2(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)(svelte@5.53.2): 6868 + svelte-tiptap@3.0.1(@floating-ui/dom@1.7.5)(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/extension-bubble-menu@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))(@tiptap/extension-floating-menu@3.20.0(@floating-ui/dom@1.7.5)(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)(svelte@5.53.2): 6851 6869 dependencies: 6852 6870 '@floating-ui/dom': 1.7.5 6853 6871 '@tiptap/core': 3.20.0(@tiptap/pm@3.20.0) 6854 6872 '@tiptap/extension-bubble-menu': 3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0) 6855 - '@tiptap/extension-floating-menu': 2.27.2(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0) 6873 + '@tiptap/extension-floating-menu': 3.20.0(@floating-ui/dom@1.7.5)(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0) 6856 6874 '@tiptap/pm': 3.20.0 6857 6875 svelte: 5.53.2 6858 6876 ··· 6943 6961 dependencies: 6944 6962 fdir: 6.5.0(picomatch@4.0.3) 6945 6963 picomatch: 4.0.3 6946 - 6947 - tippy.js@6.3.7: 6948 - dependencies: 6949 - '@popperjs/core': 2.11.8 6950 6964 6951 6965 tlds@1.261.0: {} 6952 6966
+4 -2
packages/text/package.json
··· 68 68 "vite": "^7.3.1" 69 69 }, 70 70 "dependencies": { 71 + "@floating-ui/dom": "^1.7.0", 71 72 "@foxui/core": "workspace:*", 72 73 "@tiptap/core": "^3.20.0", 73 74 "@tiptap/extension-bubble-menu": "^3.20.0", 74 75 "@tiptap/extension-code-block-lowlight": "^3.20.0", 75 76 "@tiptap/extension-document": "^3.20.0", 76 77 "@tiptap/extension-dropcursor": "^3.20.0", 78 + "@tiptap/extension-floating-menu": "^3.20.0", 77 79 "@tiptap/extension-history": "^3.20.0", 78 80 "@tiptap/extension-image": "^3.20.0", 79 81 "@tiptap/extension-link": "^3.20.0", ··· 83 85 "@tiptap/extension-text": "^3.20.0", 84 86 "@tiptap/extension-typography": "^3.20.0", 85 87 "@tiptap/extension-underline": "^3.20.0", 88 + "@tiptap/markdown": "^3.20.0", 86 89 "@tiptap/pm": "^3.20.0", 87 90 "@tiptap/starter-kit": "^3.20.0", 88 91 "@tiptap/suggestion": "^3.20.0", 89 92 "bits-ui": "^2.16.2", 90 93 "lowlight": "^3.3.0", 91 94 "state": "link:@tiptap/pm/state", 92 - "svelte-tiptap": "^3.0.1", 93 - "tippy.js": "^6.3.7" 95 + "svelte-tiptap": "^3.0.1" 94 96 }, 95 97 "peerDependencies": { 96 98 "svelte": ">=5",
+153 -200
packages/text/src/lib/components/rich-text-editor/RichTextEditor.svelte
··· 1 1 <script lang="ts"> 2 - import { onMount, onDestroy } from 'svelte'; 3 - import { Editor, mergeAttributes, type Content } from '@tiptap/core'; 2 + import { onDestroy } from 'svelte'; 3 + import { type Editor as CoreEditor, mergeAttributes, type Content } from '@tiptap/core'; 4 + import { type Editor, createEditor, EditorContent, BubbleMenu } from 'svelte-tiptap'; 4 5 import StarterKit from '@tiptap/starter-kit'; 5 6 import Placeholder from '@tiptap/extension-placeholder'; 6 7 import Image from '@tiptap/extension-image'; 7 8 import { all, createLowlight } from 'lowlight'; 8 9 import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'; 9 - import BubbleMenu from '@tiptap/extension-bubble-menu'; 10 10 import Underline from '@tiptap/extension-underline'; 11 11 import RichTextEditorMenu from './RichTextEditorMenu.svelte'; 12 12 import type { RichTextTypes } from '.'; ··· 14 14 import Slash, { suggestion } from './slash-menu'; 15 15 import Typography from '@tiptap/extension-typography'; 16 16 import { RichTextLink } from './RichTextLink'; 17 + import { Markdown } from '@tiptap/markdown'; 17 18 18 19 import './code.css'; 19 20 import { cn } from '@foxui/core'; 20 21 import { ImageUploadNode } from './image-upload/ImageUploadNode'; 21 - import { Transaction } from '@tiptap/pm/state'; 22 + import type { Transaction } from '@tiptap/pm/state'; 23 + import type { Readable } from 'svelte/store'; 22 24 23 25 let { 24 26 content = $bindable({}), 25 27 placeholder = 'Write or press / for commands', 26 - editor = $bindable(null), 28 + editor = $bindable(), 27 29 ref = $bindable(null), 28 30 class: className, 29 31 onupdate, ··· 31 33 }: { 32 34 content?: Content; 33 35 placeholder?: string; 34 - editor?: Editor | null; 36 + editor?: Readable<Editor>; 35 37 ref?: HTMLDivElement | null; 36 38 class?: string; 37 - onupdate?: (content: Content, context: { editor: Editor; transaction: Transaction }) => void; 39 + onupdate?: (content: Content, context: { editor: CoreEditor; transaction: Transaction }) => void; 38 40 ontransaction?: () => void; 39 41 } = $props(); 40 42 41 43 const lowlight = createLowlight(all); 42 44 43 45 let hasFocus = true; 44 - 45 - let menu: HTMLElement | null = $state(null); 46 - let menuLink: HTMLElement | null = $state(null); 47 46 48 47 let selectedType: RichTextTypes = $state('paragraph'); 49 48 ··· 55 54 let isImage = $state(false); 56 55 57 56 const CustomImage = Image.extend({ 58 - // addAttributes(this) { 59 - // return { 60 - // inline: true, 61 - // allowBase64: true, 62 - // HTMLAttributes: {}, 63 - // uploadImageHandler: { default: undefined } 64 - // }; 65 - // }, 66 57 renderHTML({ HTMLAttributes }) { 67 58 const attrs = mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { 68 59 uploadImageHandler: undefined 69 60 }); 70 - const isLocal = attrs['data-local'] === 'true'; 71 - 72 - // if (isLocal) { 73 - // // For local images, wrap in a container with a label 74 - // return [ 75 - // 'div', 76 - // { class: 'image-container' }, 77 - // ['img', { ...attrs, class: `${attrs.class || ''} local-image` }], 78 - // ['span', { class: 'local-image-label' }, 'Local preview'] 79 - // ]; 80 - // } 81 - 82 - console.log('attrs', attrs); 83 - 84 - // For regular images, just return the img tag 85 61 return ['img', attrs]; 86 62 } 87 63 }); 88 64 89 - onMount(() => { 90 - if (!ref) return; 65 + // Flag to track whether a file is being dragged over the drop area 66 + let isDragOver = $state(false); 91 67 92 - let extensions = [ 68 + // Store local image files for later upload 69 + let localImages: Map<string, File> = $state(new Map()); 70 + 71 + // Track which image URLs in the editor are local previews 72 + let localImageUrls: Set<string> = $state(new Set()); 73 + 74 + // Process image file to create a local preview 75 + async function processImageFile(file: File) { 76 + if (!$editor) { 77 + console.warn('Tiptap editor not initialized'); 78 + return; 79 + } 80 + 81 + try { 82 + const localUrl = URL.createObjectURL(file); 83 + 84 + localImages.set(localUrl, file); 85 + localImageUrls.add(localUrl); 86 + 87 + $editor 88 + .chain() 89 + .focus() 90 + .setImageUploadNode({ 91 + preview: localUrl 92 + }) 93 + .run(); 94 + } catch (error) { 95 + console.error('Error creating image preview:', error); 96 + } 97 + } 98 + 99 + function switchTo(value: RichTextTypes) { 100 + $editor?.chain().focus().setParagraph().run(); 101 + 102 + if (value === 'heading-1') { 103 + $editor?.chain().focus().setNode('heading', { level: 1 }).run(); 104 + } else if (value === 'heading-2') { 105 + $editor?.chain().focus().setNode('heading', { level: 2 }).run(); 106 + } else if (value === 'heading-3') { 107 + $editor?.chain().focus().setNode('heading', { level: 3 }).run(); 108 + } else if (value === 'blockquote') { 109 + $editor?.chain().focus().setBlockquote().run(); 110 + } else if (value === 'code') { 111 + $editor?.chain().focus().setCodeBlock().run(); 112 + } else if (value === 'bullet-list') { 113 + $editor?.chain().focus().toggleBulletList().run(); 114 + } else if (value === 'ordered-list') { 115 + $editor?.chain().focus().toggleOrderedList().run(); 116 + } 117 + } 118 + 119 + editor = createEditor({ 120 + extensions: [ 93 121 StarterKit.configure({ 94 122 dropcursor: { 95 123 class: 'text-accent-500/30 rounded-2xl', ··· 102 130 }), 103 131 Placeholder.configure({ 104 132 placeholder: ({ node }) => { 105 - // only show in paragraphs 106 133 if (node.type.name === 'paragraph' || node.type.name === 'heading') { 107 134 return placeholder; 108 135 } ··· 119 146 lowlight, 120 147 defaultLanguage: 'js' 121 148 }), 122 - BubbleMenu.configure({ 123 - element: menu, 124 - shouldShow: ({ editor }) => { 125 - // dont show if image selected or no selection or is code block 126 - return ( 127 - !editor.isActive('image') && 128 - !editor.view.state.selection.empty && 129 - !editor.isActive('codeBlock') && 130 - !editor.isActive('link') && 131 - !editor.isActive('imageUpload') 132 - ); 133 - }, 134 - pluginKey: 'bubble-menu-marks' 135 - }), 136 - BubbleMenu.configure({ 137 - element: menuLink, 138 - shouldShow: ({ editor }) => { 139 - // only show if link is selected 140 - return editor.isActive('link') && !editor.view.state.selection.empty; 141 - }, 142 - pluginKey: 'bubble-menu-links' 143 - }), 144 149 Underline.configure({}), 145 150 RichTextLink.configure({ 146 151 openOnClick: false, ··· 156 161 }) 157 162 }), 158 163 Typography.configure(), 164 + Markdown.configure(), 159 165 ImageUploadNode.configure({ 160 166 upload: async (file, onProgress, abortSignal) => { 161 167 console.log('uploading image', file); 162 - // wait 2 seconds 163 168 for (let i = 0; i < 10; i++) { 164 169 await new Promise((resolve) => setTimeout(resolve, 200)); 165 170 onProgress?.({ progress: i / 10 }); ··· 168 173 return 'https://picsum.photos/200/300'; 169 174 } 170 175 }) 171 - ]; 176 + ], 177 + editorProps: { 178 + attributes: { 179 + class: 'outline-none' 180 + } 181 + }, 182 + onUpdate: (ctx) => { 183 + content = ctx.editor.getJSON(); 184 + onupdate?.(content, ctx); 185 + }, 186 + onFocus: () => { 187 + hasFocus = true; 188 + }, 189 + onBlur: () => { 190 + hasFocus = false; 191 + }, 192 + onTransaction: (ctx) => { 193 + isBold = ctx.editor.isActive('bold'); 194 + isItalic = ctx.editor.isActive('italic'); 195 + isUnderline = ctx.editor.isActive('underline'); 196 + isStrikethrough = ctx.editor.isActive('strike'); 197 + isLink = ctx.editor.isActive('link'); 198 + isImage = ctx.editor.isActive('image'); 172 199 173 - editor = new Editor({ 174 - element: ref, 175 - extensions, 176 - editorProps: { 177 - attributes: { 178 - class: 'outline-none' 179 - } 180 - }, 181 - onUpdate: (ctx) => { 182 - content = ctx.editor.getJSON(); 183 - onupdate?.(content, ctx); 184 - }, 185 - onFocus: () => { 186 - hasFocus = true; 187 - }, 188 - onBlur: () => { 189 - hasFocus = false; 190 - }, 191 - onTransaction: (ctx) => { 192 - isBold = ctx.editor.isActive('bold'); 193 - isItalic = ctx.editor.isActive('italic'); 194 - isUnderline = ctx.editor.isActive('underline'); 195 - isStrikethrough = ctx.editor.isActive('strike'); 196 - isLink = ctx.editor.isActive('link'); 197 - isImage = ctx.editor.isActive('image'); 198 - 199 - if (ctx.editor.isActive('heading', { level: 1 })) { 200 - selectedType = 'heading-1'; 201 - } else if (ctx.editor.isActive('heading', { level: 2 })) { 202 - selectedType = 'heading-2'; 203 - } else if (ctx.editor.isActive('heading', { level: 3 })) { 204 - selectedType = 'heading-3'; 205 - } else if (ctx.editor.isActive('blockquote')) { 206 - selectedType = 'blockquote'; 207 - } else if (ctx.editor.isActive('code')) { 208 - selectedType = 'code'; 209 - } else if (ctx.editor.isActive('bulletList')) { 210 - selectedType = 'bullet-list'; 211 - } else if (ctx.editor.isActive('orderedList')) { 212 - selectedType = 'ordered-list'; 213 - } else { 214 - selectedType = 'paragraph'; 215 - } 216 - ontransaction?.(); 217 - }, 218 - content 219 - }); 220 - 221 - menu?.classList.remove('hidden'); 222 - menuLink?.classList.remove('hidden'); 200 + if (ctx.editor.isActive('heading', { level: 1 })) { 201 + selectedType = 'heading-1'; 202 + } else if (ctx.editor.isActive('heading', { level: 2 })) { 203 + selectedType = 'heading-2'; 204 + } else if (ctx.editor.isActive('heading', { level: 3 })) { 205 + selectedType = 'heading-3'; 206 + } else if (ctx.editor.isActive('blockquote')) { 207 + selectedType = 'blockquote'; 208 + } else if (ctx.editor.isActive('code')) { 209 + selectedType = 'code'; 210 + } else if (ctx.editor.isActive('bulletList')) { 211 + selectedType = 'bullet-list'; 212 + } else if (ctx.editor.isActive('orderedList')) { 213 + selectedType = 'ordered-list'; 214 + } else { 215 + selectedType = 'paragraph'; 216 + } 217 + ontransaction?.(); 218 + }, 219 + content 223 220 }); 224 - 225 - // Flag to track whether a file is being dragged over the drop area 226 - let isDragOver = $state(false); 227 - 228 - // Store local image files for later upload 229 - let localImages: Map<string, File> = $state(new Map()); 230 - 231 - // Track which image URLs in the editor are local previews 232 - let localImageUrls: Set<string> = $state(new Set()); 233 - 234 - // Process image file to create a local preview 235 - async function processImageFile(file: File) { 236 - if (!editor) { 237 - console.warn('Tiptap editor not initialized'); 238 - return; 239 - } 240 - 241 - try { 242 - const localUrl = URL.createObjectURL(file); 243 - 244 - localImages.set(localUrl, file); 245 - localImageUrls.add(localUrl); 246 - 247 - //editor.commands.setImageUploadNode(); 248 - editor 249 - .chain() 250 - .focus() 251 - .setImageUploadNode({ 252 - preview: localUrl 253 - }) 254 - .run(); 255 - 256 - // wait 2 seconds 257 - // await new Promise((resolve) => setTimeout(resolve, 500)); 258 - 259 - // content = editor.getJSON(); 260 - 261 - // console.log('replacing image url in content'); 262 - // replaceImageUrlInContent(content, localUrl, 'https://picsum.photos/200/300'); 263 - // editor.commands.setContent(content); 264 - } catch (error) { 265 - console.error('Error creating image preview:', error); 266 - } 267 - } 268 221 269 222 const handlePaste = (event: ClipboardEvent) => { 270 223 const items = event.clipboardData?.items; 271 224 if (!items) return; 272 - // Check for image data in clipboard 273 225 for (const item of Array.from(items)) { 274 226 if (!item.type.startsWith('image/')) continue; 275 227 const file = item.getAsFile(); ··· 305 257 for (const localUrl of localImageUrls) { 306 258 URL.revokeObjectURL(localUrl); 307 259 } 308 - 309 - editor?.destroy(); 310 260 }); 311 261 312 262 let link = $state(''); ··· 315 265 316 266 function clickedLink() { 317 267 if (isLink) { 318 - //tiptap?.chain().focus().unsetLink().run(); 319 - // get current link 320 - link = editor?.getAttributes('link').href; 268 + link = $editor?.getAttributes('link').href; 321 269 322 270 setTimeout(() => { 323 271 linkInput?.focus(); 324 272 }, 100); 325 273 } else { 326 274 link = ''; 327 - // set link 328 - editor?.chain().focus().setLink({ href: link }).run(); 275 + $editor?.chain().focus().setLink({ href: link }).run(); 329 276 330 277 setTimeout(() => { 331 278 linkInput?.focus(); 332 279 }, 100); 333 - } 334 - } 335 - 336 - function switchTo(value: RichTextTypes) { 337 - editor?.chain().focus().setParagraph().run(); 338 - 339 - if (value === 'heading-1') { 340 - editor?.chain().focus().setNode('heading', { level: 1 }).run(); 341 - } else if (value === 'heading-2') { 342 - editor?.chain().focus().setNode('heading', { level: 2 }).run(); 343 - } else if (value === 'heading-3') { 344 - editor?.chain().focus().setNode('heading', { level: 3 }).run(); 345 - } else if (value === 'blockquote') { 346 - editor?.chain().focus().setBlockquote().run(); 347 - } else if (value === 'code') { 348 - editor?.chain().focus().setCodeBlock().run(); 349 - } else if (value === 'bullet-list') { 350 - editor?.chain().focus().toggleBulletList().run(); 351 - } else if (value === 'ordered-list') { 352 - editor?.chain().focus().toggleOrderedList().run(); 353 280 } 354 281 } 355 282 </script> ··· 362 289 ondragleave={handleDragLeave} 363 290 ondrop={handleDrop} 364 291 role="region" 365 - ></div> 292 + > 293 + {#if $editor} 294 + <EditorContent editor={$editor} /> 295 + {/if} 296 + </div> 366 297 367 - <RichTextEditorMenu 368 - bind:ref={menu} 369 - {editor} 370 - {isBold} 371 - {isItalic} 372 - {isUnderline} 373 - {isStrikethrough} 374 - {isLink} 375 - {isImage} 376 - {clickedLink} 377 - {processImageFile} 378 - {switchTo} 379 - bind:selectedType 380 - /> 381 - 382 - <RichTextEditorLinkMenu bind:ref={menuLink} {editor} bind:link bind:linkInput /> 298 + {#if $editor} 299 + <BubbleMenu 300 + editor={$editor} 301 + shouldShow={({ editor }) => { 302 + return ( 303 + !editor.isActive('image') && 304 + !editor.view.state.selection.empty && 305 + !editor.isActive('codeBlock') && 306 + !editor.isActive('link') && 307 + !editor.isActive('imageUpload') 308 + ); 309 + }} 310 + pluginKey="bubble-menu-marks" 311 + > 312 + <RichTextEditorMenu 313 + editor={$editor} 314 + {isBold} 315 + {isItalic} 316 + {isUnderline} 317 + {isStrikethrough} 318 + {isLink} 319 + {isImage} 320 + {clickedLink} 321 + {processImageFile} 322 + {switchTo} 323 + bind:selectedType 324 + /> 325 + </BubbleMenu> 326 + <BubbleMenu 327 + editor={$editor} 328 + shouldShow={({ editor }) => { 329 + return editor.isActive('link') && !editor.view.state.selection.empty; 330 + }} 331 + pluginKey="bubble-menu-links" 332 + > 333 + <RichTextEditorLinkMenu editor={$editor} bind:link bind:linkInput /> 334 + </BubbleMenu> 335 + {/if} 383 336 384 337 <style> 385 338 :global(.tiptap) {
+1 -4
packages/text/src/lib/components/rich-text-editor/RichTextEditorLinkMenu.svelte
··· 5 5 let { 6 6 editor, 7 7 link = $bindable(''), 8 - ref = $bindable(null), 9 8 linkInput = $bindable(null) 10 9 }: { 11 10 editor: Editor | null; 12 11 link: string; 13 - ref: HTMLElement | null; 14 12 linkInput: HTMLInputElement | null; 15 13 } = $props(); 16 14 ··· 20 18 </script> 21 19 22 20 <div 23 - bind:this={ref} 24 - class="menu bg-base-50 dark:bg-base-900 relative hidden w-fit rounded-2xl px-1 py-1 shadow-lg backdrop-blur-sm" 21 + class="menu bg-base-50 dark:bg-base-900 relative w-fit rounded-2xl px-1 py-1 shadow-lg backdrop-blur-sm" 25 22 > 26 23 <div class="flex items-center gap-1"> 27 24 <Input
+1 -6
packages/text/src/lib/components/rich-text-editor/RichTextEditorMenu.svelte
··· 14 14 isLink, 15 15 clickedLink, 16 16 selectedType = $bindable('paragraph'), 17 - ref = $bindable(null), 18 17 processImageFile, 19 18 switchTo 20 19 }: { ··· 27 26 isLink: boolean; 28 27 clickedLink: () => void; 29 28 selectedType: RichTextTypes; 30 - ref: HTMLElement | null; 31 29 processImageFile: (file: File, input: HTMLInputElement) => void; 32 30 switchTo: (value: RichTextTypes) => void; 33 31 } = $props(); 34 - 35 - $inspect(isBold); 36 32 37 33 function handleFileProcess(event: Event) { 38 34 const input = event.target as HTMLInputElement; ··· 47 43 </script> 48 44 49 45 <div 50 - bind:this={ref} 51 - class="bg-base-50 dark:bg-base-900 border-base-500/20 dark:border-base-700/20 relative hidden w-fit rounded-2xl border px-1 py-1 shadow-lg backdrop-blur-sm" 46 + class="bg-base-50 dark:bg-base-900 border-base-500/20 dark:border-base-700/20 relative w-fit rounded-2xl border px-1 py-1 shadow-lg backdrop-blur-sm" 52 47 > 53 48 <Select 54 49 onValueChange={(value) => {
+31 -28
packages/text/src/lib/components/rich-text-editor/slash-menu/index.ts
··· 5 5 import type { SuggestionKeyDownProps, SuggestionProps } from '@tiptap/suggestion'; 6 6 import SuggestionSelect from './SuggestionSelect.svelte'; 7 7 import { mount, unmount } from 'svelte'; 8 - import tippy, { type GetReferenceClientRect } from 'tippy.js'; 8 + import { computePosition, flip, shift, offset } from '@floating-ui/dom'; 9 9 import type { RichTextTypes } from '..'; 10 10 11 11 export default Extension.create({ ··· 139 139 140 140 render: () => { 141 141 let component: ReturnType<typeof SuggestionSelect>; 142 - let popup: ReturnType<typeof tippy>; 142 + let floatingEl: HTMLElement; 143 + 144 + function updatePosition(clientRect: (() => DOMRect | null) | null | undefined) { 145 + if (!clientRect || !floatingEl) return; 146 + const rect = clientRect(); 147 + if (!rect) return; 148 + 149 + const virtualRef = { 150 + getBoundingClientRect: () => rect 151 + }; 152 + 153 + computePosition(virtualRef as Element, floatingEl, { 154 + placement: 'bottom-start', 155 + middleware: [offset(8), flip(), shift({ padding: 8 })] 156 + }).then(({ x, y }) => { 157 + Object.assign(floatingEl.style, { 158 + left: `${x}px`, 159 + top: `${y}px` 160 + }); 161 + }); 162 + } 143 163 144 164 return { 145 165 onStart: (props: SuggestionProps) => { 146 - const element = document.createElement('div'); 166 + floatingEl = document.createElement('div'); 167 + floatingEl.style.position = 'absolute'; 168 + floatingEl.style.zIndex = '50'; 169 + document.body.appendChild(floatingEl); 147 170 148 171 component = mount(SuggestionSelect, { 149 - target: element, 172 + target: floatingEl, 150 173 props 151 174 }); 152 175 153 - if (!props.clientRect) { 154 - return; 155 - } 156 - 157 - popup = tippy('body', { 158 - getReferenceClientRect: props.clientRect as GetReferenceClientRect, 159 - appendTo: () => document.body, 160 - content: element, 161 - showOnCreate: true, 162 - interactive: true, 163 - trigger: 'manual', 164 - placement: 'bottom-start' 165 - }); 176 + updatePosition(props.clientRect); 166 177 }, 167 178 onUpdate: (props: SuggestionProps) => { 168 179 component.setItems(props.items); 169 180 component.setRange(props.range); 170 - 171 - if (!props.clientRect) { 172 - return; 173 - } 174 - 175 - popup[0].setProps({ 176 - getReferenceClientRect: props.clientRect as GetReferenceClientRect 177 - }); 181 + updatePosition(props.clientRect); 178 182 }, 179 183 onKeyDown: (props: SuggestionKeyDownProps) => { 180 184 if (props.event.key === 'Escape') { 181 - popup[0].hide(); 182 - 185 + floatingEl.style.display = 'none'; 183 186 return true; 184 187 } 185 188 186 189 return component.onKeyDown(props.event); 187 190 }, 188 191 onExit: () => { 189 - popup[0].destroy(); 190 192 unmount(component); 193 + floatingEl.remove(); 191 194 } 192 195 }; 193 196 }