A first version that has some working standard features.
This commit is contained in:
256
tp-rtb-user-mention.js
Normal file
256
tp-rtb-user-mention.js
Normal file
@@ -0,0 +1,256 @@
|
||||
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);
|
Reference in New Issue
Block a user