Fixes
This commit is contained in:
@@ -43,7 +43,7 @@ class TheApp extends LitElement {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<tp-rich-text-box>
|
<tp-rich-text-box menu-position="static">
|
||||||
<div slot="content" id="editor-content-element"></div>
|
<div slot="content" id="editor-content-element"></div>
|
||||||
<!-- Selection Menu Controls -->
|
<!-- Selection Menu Controls -->
|
||||||
<tp-rtb-bold></tp-rtb-bold>
|
<tp-rtb-bold></tp-rtb-bold>
|
||||||
|
|||||||
@@ -29,23 +29,42 @@ class TpRichTextBox extends FormElement(LitElement) {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-floating-menu {
|
.selection-menu {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
position: absolute;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-floating-menu[hidden] {
|
:host([menu-position="static"]) .selection-menu {
|
||||||
|
position: static;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
border-radius: 0;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
box-shadow: none;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-menu[hidden] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host([menu-position="static"]) .selection-menu[hidden] {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-menu[data-disabled="true"] {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.suggestion-menu {
|
.suggestion-menu {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -53,7 +72,7 @@ class TpRichTextBox extends FormElement(LitElement) {
|
|||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
position: absolute;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 101;
|
z-index: 101;
|
||||||
@@ -72,8 +91,12 @@ class TpRichTextBox extends FormElement(LitElement) {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<!-- Selection-based floating menu with all controls -->
|
<!-- Selection-based menu with all controls -->
|
||||||
<div class="custom-floating-menu" ?hidden=${this.menuMode !== 'selection'}>
|
<div
|
||||||
|
class="selection-menu"
|
||||||
|
?hidden=${this.menuMode !== 'selection' && this.menuPosition !== 'static'}
|
||||||
|
?data-disabled=${this.menuPosition === 'static' && this.menuMode !== 'selection'}
|
||||||
|
>
|
||||||
<slot @slotchange=${this._handleSlotChange}></slot>
|
<slot @slotchange=${this._handleSlotChange}></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -92,7 +115,7 @@ class TpRichTextBox extends FormElement(LitElement) {
|
|||||||
extensions: { type: Array },
|
extensions: { type: Array },
|
||||||
menuMode: { type: String, reflect: true },
|
menuMode: { type: String, reflect: true },
|
||||||
activeSuggestionType: { type: String },
|
activeSuggestionType: { type: String },
|
||||||
asHtml: { type: Boolean }
|
menuPosition: { type: String, reflect: true }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +124,7 @@ class TpRichTextBox extends FormElement(LitElement) {
|
|||||||
this.extensions = [];
|
this.extensions = [];
|
||||||
this.menuMode = 'hidden'; // 'hidden', 'selection', 'suggestion'
|
this.menuMode = 'hidden'; // 'hidden', 'selection', 'suggestion'
|
||||||
this.activeSuggestionType = null; // 'emoji', 'user', null
|
this.activeSuggestionType = null; // 'emoji', 'user', null
|
||||||
this.asHtml = false;
|
this.menuPosition = 'floating'; // 'floating', 'static'
|
||||||
this._slotObserver = new MutationObserver(this._processSlotChanges.bind(this));
|
this._slotObserver = new MutationObserver(this._processSlotChanges.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +150,7 @@ class TpRichTextBox extends FormElement(LitElement) {
|
|||||||
this.editor = new Editor({
|
this.editor = new Editor({
|
||||||
element: this.querySelector('[slot="content"]'),
|
element: this.querySelector('[slot="content"]'),
|
||||||
extensions: this.extensions,
|
extensions: this.extensions,
|
||||||
content: '<p>Hello World!</p>',
|
content: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Notify child extensions that the editor is ready
|
// Notify child extensions that the editor is ready
|
||||||
@@ -151,9 +174,22 @@ class TpRichTextBox extends FormElement(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { empty, from, to } = editor.state.selection;
|
const { empty, from, to } = editor.state.selection;
|
||||||
|
const hasSelection = !empty && from !== to;
|
||||||
|
|
||||||
// Hide menu if selection is empty or a caret
|
// In static mode, always keep menu visible but update its state
|
||||||
if (empty || from === to) {
|
if (this.menuPosition === 'static') {
|
||||||
|
if (hasSelection) {
|
||||||
|
this.menuMode = 'selection';
|
||||||
|
} else {
|
||||||
|
this.menuMode = 'hidden'; // This will disable the menu but keep it visible
|
||||||
|
}
|
||||||
|
// Trigger a re-render to update the data-disabled attribute
|
||||||
|
this.requestUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In floating mode, hide menu if selection is empty or a caret
|
||||||
|
if (!hasSelection) {
|
||||||
this.hideMenu();
|
this.hideMenu();
|
||||||
console.log('Selection is empty or a caret. Hiding menu.');
|
console.log('Selection is empty or a caret. Hiding menu.');
|
||||||
return;
|
return;
|
||||||
@@ -165,16 +201,20 @@ class TpRichTextBox extends FormElement(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the selection-based floating menu
|
* Show the selection-based menu
|
||||||
*/
|
*/
|
||||||
showSelectionMenu() {
|
showSelectionMenu() {
|
||||||
this.menuMode = 'selection';
|
this.menuMode = 'selection';
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const menu = this.shadowRoot.querySelector('.custom-floating-menu');
|
// Only position the menu if it's floating
|
||||||
if (menu) {
|
if (this.menuPosition === 'floating') {
|
||||||
MenuPositioner.positionMenu(menu, this.editor);
|
requestAnimationFrame(() => {
|
||||||
}
|
const menu = this.shadowRoot.querySelector('.selection-menu');
|
||||||
});
|
if (menu) {
|
||||||
|
MenuPositioner.positionMenu(menu, this.editor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -199,6 +239,13 @@ class TpRichTextBox extends FormElement(LitElement) {
|
|||||||
* Hide all menus
|
* Hide all menus
|
||||||
*/
|
*/
|
||||||
hideMenu() {
|
hideMenu() {
|
||||||
|
// In static mode, don't change the menu mode (it stays visible but disabled)
|
||||||
|
if (this.menuPosition === 'static') {
|
||||||
|
this.activeSuggestionType = null;
|
||||||
|
this._updateSuggestionComponentVisibility();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.menuMode = 'hidden';
|
this.menuMode = 'hidden';
|
||||||
this.activeSuggestionType = null;
|
this.activeSuggestionType = null;
|
||||||
// Reset all suggestion component visibility
|
// Reset all suggestion component visibility
|
||||||
@@ -217,6 +264,7 @@ class TpRichTextBox extends FormElement(LitElement) {
|
|||||||
_updateSuggestionComponentVisibility() {
|
_updateSuggestionComponentVisibility() {
|
||||||
const emojiComponent = this.querySelector('tp-rtb-emoji-suggestion[slot="suggestion-content"]');
|
const emojiComponent = this.querySelector('tp-rtb-emoji-suggestion[slot="suggestion-content"]');
|
||||||
const userComponent = this.querySelector('tp-rtb-user-mention[slot="suggestion-content"]');
|
const userComponent = this.querySelector('tp-rtb-user-mention[slot="suggestion-content"]');
|
||||||
|
const commandComponent = this.querySelector('dp-rtb-command[slot="suggestion-content"]');
|
||||||
|
|
||||||
if (emojiComponent) {
|
if (emojiComponent) {
|
||||||
emojiComponent.style.display = this.activeSuggestionType === 'emoji' ? 'block' : 'none';
|
emojiComponent.style.display = this.activeSuggestionType === 'emoji' ? 'block' : 'none';
|
||||||
@@ -224,6 +272,9 @@ class TpRichTextBox extends FormElement(LitElement) {
|
|||||||
if (userComponent) {
|
if (userComponent) {
|
||||||
userComponent.style.display = this.activeSuggestionType === 'user' ? 'block' : 'none';
|
userComponent.style.display = this.activeSuggestionType === 'user' ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
if (commandComponent) {
|
||||||
|
commandComponent.style.display = this.activeSuggestionType === 'command' ? 'block' : 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_processSlotChanges(mutations) {
|
_processSlotChanges(mutations) {
|
||||||
@@ -273,29 +324,24 @@ class TpRichTextBox extends FormElement(LitElement) {
|
|||||||
// If we have suggestions, add a single Mention extension with all suggestions
|
// If we have suggestions, add a single Mention extension with all suggestions
|
||||||
if (suggestions.length > 0) {
|
if (suggestions.length > 0) {
|
||||||
this.extensions.push(
|
this.extensions.push(
|
||||||
Mention.extend({
|
Mention.configure({
|
||||||
renderText({ options, node }) {
|
suggestions: suggestions,
|
||||||
// Return just the label without the trigger character
|
renderHTML({ options, node, suggestion }) {
|
||||||
return node.attrs.label || node.attrs.id;
|
if (suggestion && suggestion.renderHTML) {
|
||||||
},
|
return suggestion.renderHTML({ options, node, suggestion });
|
||||||
renderHTML({ options, node }) {
|
}
|
||||||
|
|
||||||
|
console.warn('No renderHTML defined for mention suggestion. Using default rendering.');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'span',
|
'span',
|
||||||
{
|
{
|
||||||
class: 'mention',
|
|
||||||
'data-type': 'mention',
|
'data-type': 'mention',
|
||||||
'data-id': node.attrs.id,
|
|
||||||
'data-label': node.attrs.label,
|
|
||||||
contenteditable: 'false'
|
contenteditable: 'false'
|
||||||
},
|
},
|
||||||
node.attrs.label || node.attrs.id
|
node.attrs.label || node.attrs.id
|
||||||
];
|
];
|
||||||
},
|
}
|
||||||
}).configure({
|
|
||||||
HTMLAttributes: {
|
|
||||||
class: 'mention',
|
|
||||||
},
|
|
||||||
suggestions: suggestions
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -330,10 +376,10 @@ class TpRichTextBox extends FormElement(LitElement) {
|
|||||||
|
|
||||||
get value() {
|
get value() {
|
||||||
if (this.editor) {
|
if (this.editor) {
|
||||||
return this.asHtml ? this.editor.getHTML() : this.editor.getJSON();
|
return { html: this.editor.getHTML(), json: this.editor.getJSON() };
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.asHtml ? '' : {};
|
return { html: '', json: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
set value(content) {
|
set value(content) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { LitElement, html, css } from 'lit';
|
import { html, css } from 'lit';
|
||||||
import { TpRtbBaseExtension } from './tp-rtb-base-extension.js';
|
import { TpRtbBaseExtension } from './tp-rtb-base-extension.js';
|
||||||
import Mention from '@tiptap/extension-mention';
|
|
||||||
import { closest } from '@tp/helpers/closest.js';
|
import { closest } from '@tp/helpers/closest.js';
|
||||||
|
|
||||||
const emojis = [
|
const emojis = [
|
||||||
@@ -254,18 +253,6 @@ const emojis = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export class TpRtbEmojiSuggestion extends TpRtbBaseExtension {
|
export class TpRtbEmojiSuggestion extends TpRtbBaseExtension {
|
||||||
static getEmojis() {
|
|
||||||
return emojis;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
...super.properties,
|
|
||||||
items: { type: Array },
|
|
||||||
selectedIndex: { type: Number },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles() {
|
static get styles() {
|
||||||
return [
|
return [
|
||||||
super.styles,
|
super.styles,
|
||||||
@@ -292,6 +279,36 @@ export class TpRtbEmojiSuggestion extends TpRtbBaseExtension {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
${this.items.length > 0
|
||||||
|
? this.items.map(
|
||||||
|
(item, index) => html`
|
||||||
|
<div
|
||||||
|
class="item ${index === this.selectedIndex ? 'is-selected' : ''}"
|
||||||
|
@mousedown=${(e) => this._handleItemMouseDown(e, index)}
|
||||||
|
@click=${(e) => this._selectItem(e, index)}
|
||||||
|
>
|
||||||
|
${item.emoji} ${item.name}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
: html`<div class="item">No result</div>`}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getEmojis() {
|
||||||
|
return emojis;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
...super.properties,
|
||||||
|
items: { type: Array },
|
||||||
|
selectedIndex: { type: Number },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.items = [];
|
this.items = [];
|
||||||
@@ -299,6 +316,16 @@ export class TpRtbEmojiSuggestion extends TpRtbBaseExtension {
|
|||||||
this.label = 'Emoji';
|
this.label = 'Emoji';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.addEventListener('mousedown', this._handleContainerMouseDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this.removeEventListener('mousedown', this._handleContainerMouseDown);
|
||||||
|
}
|
||||||
|
|
||||||
getSuggestionConfig() {
|
getSuggestionConfig() {
|
||||||
return {
|
return {
|
||||||
char: ':',
|
char: ':',
|
||||||
@@ -308,6 +335,19 @@ export class TpRtbEmojiSuggestion extends TpRtbBaseExtension {
|
|||||||
console.log('Emoji query:', query);
|
console.log('Emoji query:', query);
|
||||||
return TpRtbEmojiSuggestion.getEmojis().filter(item => item.name.toLowerCase().startsWith(query.toLowerCase())).slice(0, 10);
|
return TpRtbEmojiSuggestion.getEmojis().filter(item => item.name.toLowerCase().startsWith(query.toLowerCase())).slice(0, 10);
|
||||||
},
|
},
|
||||||
|
renderHTML({ options, node, suggestion }) {
|
||||||
|
return [
|
||||||
|
'span',
|
||||||
|
{
|
||||||
|
class: 'emoji',
|
||||||
|
'data-type': 'emoji',
|
||||||
|
'data-id': node.attrs.id,
|
||||||
|
'data-char': ':',
|
||||||
|
contenteditable: 'false'
|
||||||
|
},
|
||||||
|
node.attrs.label || node.attrs.id
|
||||||
|
];
|
||||||
|
},
|
||||||
render: () => {
|
render: () => {
|
||||||
let component;
|
let component;
|
||||||
let parentEditor;
|
let parentEditor;
|
||||||
@@ -315,8 +355,6 @@ export class TpRtbEmojiSuggestion extends TpRtbBaseExtension {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
onStart: props => {
|
onStart: props => {
|
||||||
console.log('Emoji suggestion onStart called', props);
|
|
||||||
console.log('Props clientRect:', props.clientRect);
|
|
||||||
// Find the parent editor using tp/helpers closest
|
// Find the parent editor using tp/helpers closest
|
||||||
parentEditor = closest(this, 'tp-rich-text-box', true);
|
parentEditor = closest(this, 'tp-rich-text-box', true);
|
||||||
if (!parentEditor) {
|
if (!parentEditor) {
|
||||||
@@ -378,7 +416,6 @@ export class TpRtbEmojiSuggestion extends TpRtbBaseExtension {
|
|||||||
return component.onKeyDown(props);
|
return component.onKeyDown(props);
|
||||||
},
|
},
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
console.log('Emoji suggestion onExit called');
|
|
||||||
if (component && selectItemHandler) {
|
if (component && selectItemHandler) {
|
||||||
component.removeEventListener('select-item', selectItemHandler);
|
component.removeEventListener('select-item', selectItemHandler);
|
||||||
component.items = [];
|
component.items = [];
|
||||||
@@ -403,23 +440,6 @@ export class TpRtbEmojiSuggestion extends TpRtbBaseExtension {
|
|||||||
// This method can be left empty or used for other purposes
|
// 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)}"
|
|
||||||
>
|
|
||||||
${item.emoji} ${item.name}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)
|
|
||||||
: html`<div class="item">No result</div>`}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
updated(changedProperties) {
|
updated(changedProperties) {
|
||||||
if (changedProperties.has('selectedIndex')) {
|
if (changedProperties.has('selectedIndex')) {
|
||||||
const selectedElement = this.shadowRoot.querySelector('.item.is-selected');
|
const selectedElement = this.shadowRoot.querySelector('.item.is-selected');
|
||||||
@@ -432,13 +452,27 @@ export class TpRtbEmojiSuggestion extends TpRtbBaseExtension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_selectItem(index) {
|
_selectItem(e, index) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
const item = this.items[index];
|
const item = this.items[index];
|
||||||
if (item) {
|
if (item) {
|
||||||
this.dispatchEvent(new CustomEvent('select-item', { detail: item }));
|
this.dispatchEvent(new CustomEvent('select-item', { detail: item }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_handleItemMouseDown(e, index) {
|
||||||
|
// Prevent blur event from firing on the editor
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleContainerMouseDown(event) {
|
||||||
|
// Prevent blur event from firing when clicking anywhere in the suggestion popup
|
||||||
|
// This includes scrollbars, empty areas, etc.
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
onKeyDown({ event }) {
|
onKeyDown({ event }) {
|
||||||
if (event.key === 'ArrowUp') {
|
if (event.key === 'ArrowUp') {
|
||||||
this.selectedIndex = (this.selectedIndex + this.items.length - 1) % this.items.length;
|
this.selectedIndex = (this.selectedIndex + this.items.length - 1) % this.items.length;
|
||||||
@@ -451,7 +485,9 @@ export class TpRtbEmojiSuggestion extends TpRtbBaseExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
this._selectItem(this.selectedIndex);
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
this._selectItem(event, this.selectedIndex);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { LitElement, html, css } from 'lit';
|
import { html, css } from 'lit';
|
||||||
import { TpRtbBaseExtension } from './tp-rtb-base-extension.js';
|
import { TpRtbBaseExtension } from './tp-rtb-base-extension.js';
|
||||||
import Mention from '@tiptap/extension-mention';
|
|
||||||
import { closest } from '@tp/helpers/closest.js';
|
import { closest } from '@tp/helpers/closest.js';
|
||||||
|
|
||||||
const sampleUsers = [
|
const sampleUsers = [
|
||||||
@@ -17,18 +16,6 @@ const sampleUsers = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export class TpRtbUserMention extends TpRtbBaseExtension {
|
export class TpRtbUserMention extends TpRtbBaseExtension {
|
||||||
static getUsers() {
|
|
||||||
return sampleUsers;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
...super.properties,
|
|
||||||
items: { type: Array },
|
|
||||||
selectedIndex: { type: Number },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles() {
|
static get styles() {
|
||||||
return [
|
return [
|
||||||
super.styles,
|
super.styles,
|
||||||
@@ -45,6 +32,7 @@ export class TpRtbUserMention extends TpRtbBaseExtension {
|
|||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -53,24 +41,30 @@ export class TpRtbUserMention extends TpRtbBaseExtension {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item.is-selected {
|
.item.is-selected {
|
||||||
background-color: #e3f2fd;
|
background-color: #e3f2fd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item:hover {
|
.item:hover {
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-info {
|
.user-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #666;
|
color: #666;
|
||||||
@@ -79,11 +73,67 @@ export class TpRtbUserMention extends TpRtbBaseExtension {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
${this.items.length > 0
|
||||||
|
? this.items.map(
|
||||||
|
(item, index) => html`
|
||||||
|
<div
|
||||||
|
class="item ${index === this.selectedIndex ? 'is-selected' : ''}"
|
||||||
|
@mousedown=${(e) => this._handleItemMouseDown(e, index)}
|
||||||
|
@click=${(e) => this._handleItemClick(e, 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>`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
items: { type: Array },
|
||||||
|
selectedIndex: { type: Number },
|
||||||
|
getUsersCallback: { type: Function, state: true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.items = [];
|
this.items = [];
|
||||||
this.selectedIndex = 0;
|
this.selectedIndex = 0;
|
||||||
this.label = 'User Mention';
|
this.label = 'User Mention';
|
||||||
|
this.getUsersCallback = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.addEventListener('mousedown', this._handleContainerMouseDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this.removeEventListener('mousedown', this._handleContainerMouseDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getFilteredUsers(query) {
|
||||||
|
// Use callback if provided, otherwise fall back to sample users
|
||||||
|
if (this.getUsersCallback && typeof this.getUsersCallback === 'function') {
|
||||||
|
return this.getUsersCallback(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to sample users with default filtering
|
||||||
|
return sampleUsers.filter(user =>
|
||||||
|
user.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
|
user.username.toLowerCase().includes(query.toLowerCase())
|
||||||
|
).slice(0, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSuggestionConfig() {
|
getSuggestionConfig() {
|
||||||
@@ -92,11 +142,21 @@ export class TpRtbUserMention extends TpRtbBaseExtension {
|
|||||||
allowSpaces: false,
|
allowSpaces: false,
|
||||||
startOfLine: false,
|
startOfLine: false,
|
||||||
items: ({ query }) => {
|
items: ({ query }) => {
|
||||||
console.log('User mention query:', query);
|
return this._getFilteredUsers(query);
|
||||||
return TpRtbUserMention.getUsers().filter(user =>
|
},
|
||||||
user.name.toLowerCase().includes(query.toLowerCase()) ||
|
renderHTML({ options, node, suggestion }) {
|
||||||
user.username.toLowerCase().includes(query.toLowerCase())
|
return [
|
||||||
).slice(0, 10);
|
'span',
|
||||||
|
{
|
||||||
|
class: 'user-mention',
|
||||||
|
'data-type': 'mention',
|
||||||
|
'data-id': node.attrs.id,
|
||||||
|
'data-label': node.attrs.label,
|
||||||
|
'data-mention-suggestion-char': '@',
|
||||||
|
contenteditable: 'false'
|
||||||
|
},
|
||||||
|
node.attrs.label || node.attrs.id
|
||||||
|
];
|
||||||
},
|
},
|
||||||
render: () => {
|
render: () => {
|
||||||
let component;
|
let component;
|
||||||
@@ -193,27 +253,6 @@ export class TpRtbUserMention extends TpRtbBaseExtension {
|
|||||||
// This method can be left empty or used for other purposes
|
// 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) {
|
updated(changedProperties) {
|
||||||
if (changedProperties.has('selectedIndex')) {
|
if (changedProperties.has('selectedIndex')) {
|
||||||
const selectedElement = this.shadowRoot.querySelector('.item.is-selected');
|
const selectedElement = this.shadowRoot.querySelector('.item.is-selected');
|
||||||
@@ -233,6 +272,23 @@ export class TpRtbUserMention extends TpRtbBaseExtension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_handleItemClick(event, index) {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
this._selectItem(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleItemMouseDown(event, index) {
|
||||||
|
// Prevent blur event from firing on the editor
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleContainerMouseDown(event) {
|
||||||
|
// Prevent blur event from firing when clicking anywhere in the suggestion popup
|
||||||
|
// This includes scrollbars, empty areas, etc.
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
onKeyDown({ event }) {
|
onKeyDown({ event }) {
|
||||||
if (event.key === 'ArrowUp') {
|
if (event.key === 'ArrowUp') {
|
||||||
this.selectedIndex = (this.selectedIndex + this.items.length - 1) % this.items.length;
|
this.selectedIndex = (this.selectedIndex + this.items.length - 1) % this.items.length;
|
||||||
@@ -245,6 +301,8 @@ export class TpRtbUserMention extends TpRtbBaseExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
this._selectItem(this.selectedIndex);
|
this._selectItem(this.selectedIndex);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user