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
JavaScript
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;