Files
tp-rich-text-box/tp-rtb-user-mention.js

256 lines
7.7 KiB
JavaScript

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`
<div
class="item ${index === this.selectedIndex ? 'is-selected' : ''}"
@click="${() => this._selectItem(index)}"
>
<span class="avatar">${item.avatar}</span>
<div class="user-info">
<span class="name">${item.name}</span>
<span class="username">@${item.username}</span>
</div>
</div>
`
)
: html`<div class="item">No users found</div>`}
`;
}
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);