UNPKG

claritykit-svelte

Version:

A comprehensive Svelte component library focused on accessibility, ADHD-optimized design, developer experience, and full SSR compatibility

269 lines (268 loc) 10.1 kB
import { Node, mergeAttributes, nodeInputRule } from '@tiptap/core'; import { PluginKey } from '@tiptap/pm/state'; import { Plugin } from '@tiptap/pm/state'; export const ThreadReferenceExtension = Node.create({ name: 'threadReference', addOptions() { return { HTMLAttributes: { class: 'thread-reference', 'data-type': 'thread-reference', }, renderLabel({ options, node }) { const messageId = node.attrs.messageId; const threadId = node.attrs.threadId; if (messageId) { return `#${threadId}/${messageId}`; } return `#${threadId}`; }, onThreadClick: (threadId, messageId) => { console.log('Thread clicked:', { threadId, messageId }); }, getThreadPreview: async (threadId, messageId) => { // This would fetch thread preview from API return { threadId, messageId, title: `Thread ${threadId}`, preview: 'This is a preview of the referenced thread...', author: { id: 'user1', name: 'John Doe', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=John', }, timestamp: new Date().toISOString(), messageCount: 5, }; }, }; }, group: 'inline', inline: true, selectable: false, atom: true, addAttributes() { return { threadId: { default: null, parseHTML: element => element.getAttribute('data-thread-id'), renderHTML: attributes => { if (!attributes.threadId) { return {}; } return { 'data-thread-id': attributes.threadId, }; }, }, messageId: { default: null, parseHTML: element => element.getAttribute('data-message-id'), renderHTML: attributes => { if (!attributes.messageId) { return {}; } return { 'data-message-id': attributes.messageId, }; }, }, label: { default: null, parseHTML: element => element.getAttribute('data-label'), renderHTML: attributes => { if (!attributes.label) { return {}; } return { 'data-label': attributes.label, }; }, }, preview: { default: null, parseHTML: element => element.getAttribute('data-preview'), renderHTML: attributes => { if (!attributes.preview) { return {}; } return { 'data-preview': attributes.preview, }; }, }, }; }, parseHTML() { return [ { tag: `span[data-type="${this.name}"]`, }, ]; }, renderHTML({ node, HTMLAttributes }) { return [ 'span', mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes, { 'data-thread-id': node.attrs.threadId, 'data-message-id': node.attrs.messageId, 'data-label': node.attrs.label, 'data-preview': node.attrs.preview, title: node.attrs.preview || `Reference to thread ${node.attrs.threadId}`, }), [ 'span', { class: 'thread-reference-icon' }, '🔗', ], [ 'span', { class: 'thread-reference-label' }, this.options.renderLabel({ options: this.options, node, }), ], ]; }, renderText({ node }) { return this.options.renderLabel({ options: this.options, node, }); }, addCommands() { return { insertThreadReference: (attributes) => ({ commands }) => { return commands.insertContent({ type: this.name, attrs: attributes, }); }, }; }, addInputRules() { return [ nodeInputRule({ find: /#([a-zA-Z0-9_-]+)(?:\/([a-zA-Z0-9_-]+))?/g, type: this.type, getAttributes: (match) => { const threadId = match[1]; const messageId = match[2]; return { threadId, messageId, label: messageId ? `${threadId}/${messageId}` : threadId, }; }, }), ]; }, addProseMirrorPlugins() { return [ new Plugin({ key: new PluginKey('threadReference'), props: { handleClick: (view, pos, event) => { const target = event.target; const threadRef = target.closest('[data-type="thread-reference"]'); if (threadRef) { event.preventDefault(); event.stopPropagation(); const threadId = threadRef.getAttribute('data-thread-id'); const messageId = threadRef.getAttribute('data-message-id'); if (threadId) { this.options.onThreadClick(threadId, messageId || undefined); } return true; } return false; }, handleDOMEvents: { mouseenter: (view, event) => { const target = event.target; const threadRef = target.closest('[data-type="thread-reference"]'); if (threadRef) { const threadId = threadRef.getAttribute('data-thread-id'); const messageId = threadRef.getAttribute('data-message-id'); if (threadId) { // Show thread preview tooltip this.showThreadPreview(threadRef, threadId, messageId || undefined); } } return false; }, mouseleave: (view, event) => { const target = event.target; const threadRef = target.closest('[data-type="thread-reference"]'); if (threadRef) { // Hide thread preview tooltip this.hideThreadPreview(); } return false; }, }, }, }), ]; }, // Helper methods for thread preview showThreadPreview(element, threadId, messageId) { // Remove existing preview this.hideThreadPreview(); // Create preview element const preview = document.createElement('div'); preview.className = 'thread-reference-preview'; preview.innerHTML = ` <div class="thread-preview-loading"> <span class="loading-spinner">⏳</span> <span>Loading thread preview...</span> </div> `; // Position preview const rect = element.getBoundingClientRect(); preview.style.position = 'fixed'; preview.style.top = `${rect.bottom + 8}px`; preview.style.left = `${rect.left}px`; preview.style.zIndex = '1000'; document.body.appendChild(preview); // Load thread preview data this.options.getThreadPreview(threadId, messageId).then(data => { if (data && document.body.contains(preview)) { preview.innerHTML = ` <div class="thread-preview-content"> <div class="thread-preview-header"> <div class="thread-preview-author"> ${data.author.avatar ? `<img src="${data.author.avatar}" alt="${data.author.name}" class="author-avatar" />` : ''} <span class="author-name">${data.author.name}</span> </div> <div class="thread-preview-meta"> <span class="thread-timestamp">${new Date(data.timestamp).toLocaleString()}</span> ${data.messageCount ? `<span class="message-count">${data.messageCount} messages</span>` : ''} </div> </div> <div class="thread-preview-title">${data.title}</div> <div class="thread-preview-text">${data.preview}</div> </div> `; } }).catch(error => { console.error('Failed to load thread preview:', error); if (document.body.contains(preview)) { preview.innerHTML = ` <div class="thread-preview-error"> <span>Failed to load thread preview</span> </div> `; } }); }, hideThreadPreview() { const existing = document.querySelector('.thread-reference-preview'); if (existing) { existing.remove(); } }, }); export default ThreadReferenceExtension;