import { LitElement, html, css } from 'lit'; import { TpRtbBaseExtension } from './tp-rtb-base-extension.js'; import Mention from '@tiptap/extension-mention'; import { closest } from '@tp/helpers/closest.js'; const sampleUsers = [ { id: 'user1', name: 'John Doe', username: 'johndoe', avatar: '👤' }, { id: 'user2', name: 'Jane Smith', username: 'janesmith', avatar: '👩' }, { id: 'user3', name: 'Bob Johnson', username: 'bobjohnson', avatar: '👨' }, { id: 'user4', name: 'Alice Brown', username: 'alicebrown', avatar: '👩‍💼' }, { id: 'user5', name: 'Charlie Wilson', username: 'charliewilson', avatar: '👨‍💻' }, { id: 'user6', name: 'Diana Garcia', username: 'dianagarcia', avatar: '👩‍🔬' }, { id: 'user7', name: 'Edward Davis', username: 'edwarddavis', avatar: '👨‍🎨' }, { id: 'user8', name: 'Fiona Miller', username: 'fionamiller', avatar: '👩‍🎓' }, { id: 'user9', name: 'George Taylor', username: 'georgetaylor', avatar: '👨‍⚕️' }, { id: 'user10', name: 'Helen Anderson', username: 'helenaderson', avatar: '👩‍🏫' }, ]; export class TpRtbUserMention extends TpRtbBaseExtension { static getUsers() { return sampleUsers; } static get properties() { return { ...super.properties, items: { type: Array }, selectedIndex: { type: Number }, }; } static get styles() { return [ super.styles, css` :host { display: block; background-color: #fff; border: 1px solid #ccc; border-radius: 4px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); padding: 8px; max-height: 200px; overflow-y: auto; z-index: 9999; min-width: 200px; } .item { padding: 8px; cursor: pointer; display: flex; align-items: center; gap: 8px; border-radius: 4px; } .item.is-selected { background-color: #e3f2fd; } .item:hover { background-color: #f5f5f5; } .avatar { font-size: 20px; } .user-info { display: flex; flex-direction: column; flex-grow: 1; } .name { font-weight: 500; font-size: 14px; } .username { font-size: 12px; color: #666; } ` ]; } constructor() { super(); this.items = []; this.selectedIndex = 0; this.label = 'User Mention'; } getSuggestionConfig() { return { char: '@', allowSpaces: false, startOfLine: false, items: ({ query }) => { console.log('User mention query:', query); return TpRtbUserMention.getUsers().filter(user => user.name.toLowerCase().includes(query.toLowerCase()) || user.username.toLowerCase().includes(query.toLowerCase()) ).slice(0, 10); }, render: () => { let component; let parentEditor; let selectItemHandler; return { onStart: props => { console.log('User mention onStart called', props); console.log('Props clientRect:', props.clientRect); // Find the parent editor using tp/helpers closest parentEditor = closest(this, 'tp-rich-text-box', true); if (!parentEditor) { console.error('Parent editor not found for user mention'); return; } // Find the user mention component component = parentEditor.querySelector('tp-rtb-user-mention[slot="suggestion-content"]'); if (!component) { console.error('User mention component not found.'); return; } component.items = props.items; component.selectedIndex = 0; // Pass the command function to the component component._command = props.command; // Show suggestion menu with user type if (parentEditor.showSuggestionMenu) { parentEditor.showSuggestionMenu(props.clientRect, 'user'); } else { console.error('showSuggestionMenu method not available'); } // Store the handler to remove it later selectItemHandler = (event) => { const user = event.detail; console.log('User selected:', user); // Call command directly like in TipTap Vue example if (component._command) { component._command({ id: user.id, label: user.username }); } }; component.addEventListener('select-item', selectItemHandler); }, onUpdate: (props) => { if (!component || !parentEditor) return; component.items = props.items; component.selectedIndex = 0; // Update the command function component._command = props.command; // Update suggestion menu position with user type if (parentEditor.showSuggestionMenu) { parentEditor.showSuggestionMenu(props.clientRect, 'user'); } }, onKeyDown: (props) => { if (!component) return false; return component.onKeyDown(props); }, onExit: () => { console.log('User mention onExit called'); if (component && selectItemHandler) { component.removeEventListener('select-item', selectItemHandler); component.items = []; component.selectedIndex = 0; } // Hide the suggestion menu if (parentEditor && parentEditor.hideMenu) { parentEditor.hideMenu(); } component = null; parentEditor = null; selectItemHandler = null; }, }; }, }; } _handleClick() { // User mentions are triggered by typing @ not by clicking a button // This method can be left empty or used for other purposes } render() { return html` ${this.items.length > 0 ? this.items.map( (item, index) => html`
${item.avatar}
${item.name} @${item.username}
` ) : html`
No users found
`} `; } updated(changedProperties) { if (changedProperties.has('selectedIndex')) { const selectedElement = this.shadowRoot.querySelector('.item.is-selected'); if (selectedElement) { selectedElement.scrollIntoView({ block: 'nearest', inline: 'nearest', }); } } } _selectItem(index) { const item = this.items[index]; if (item) { this.dispatchEvent(new CustomEvent('select-item', { detail: item })); } } onKeyDown({ event }) { if (event.key === 'ArrowUp') { this.selectedIndex = (this.selectedIndex + this.items.length - 1) % this.items.length; return true; } if (event.key === 'ArrowDown') { this.selectedIndex = (this.selectedIndex + 1) % this.items.length; return true; } if (event.key === 'Enter') { this._selectItem(this.selectedIndex); return true; } return false; } } customElements.define('tp-rtb-user-mention', TpRtbUserMention);