Add placeholder and hide floating menu if no actions are exposed.

This commit is contained in:
pk
2026-09-26 18:53:42 +02:00
parent 4ea22025ca
commit 8b0f798b5a
+30 -6
View File
@@ -11,7 +11,7 @@ import Document from '@tiptap/extension-document';
import Paragraph from '@tiptap/extension-paragraph';
import Text from '@tiptap/extension-text';
import HardBreak from '@tiptap/extension-hard-break';
import { UndoRedo } from '@tiptap/extensions';
import { UndoRedo, Placeholder } from '@tiptap/extensions';
import Mention from '@tiptap/extension-mention';
import { MenuPositioner } from './menu-positioner.js';
import { FormElement } from '@tp/helpers/form-element.js';
@@ -23,7 +23,6 @@ class TpRichTextBox extends FormElement(LitElement) {
:host {
display: flex;
flex-direction: column;
border: 1px solid #ccc;
position: relative;
}
@@ -92,7 +91,7 @@ class TpRichTextBox extends FormElement(LitElement) {
<!-- Selection-based menu with all controls -->
<div
class="selection-menu"
?hidden=${this.menuMode !== 'selection' && this.menuPosition !== 'static'}
?hidden=${!this._menuControlCount || (this.menuMode !== 'selection' && this.menuPosition !== 'static')}
?data-disabled=${this.menuPosition === 'static' && this.menuMode !== 'selection'}
>
<slot @slotchange=${this._handleSlotChange}></slot>
@@ -113,7 +112,9 @@ class TpRichTextBox extends FormElement(LitElement) {
extensions: { type: Array },
menuMode: { type: String, reflect: true },
activeSuggestionType: { type: String },
menuPosition: { type: String, reflect: true }
menuPosition: { type: String, reflect: true },
placeholder: { type: String },
_menuControlCount: { state: true }
};
}
@@ -122,6 +123,8 @@ class TpRichTextBox extends FormElement(LitElement) {
this.extensions = [];
this.menuMode = 'hidden'; // 'hidden', 'selection', 'suggestion'
this.activeSuggestionType = null; // 'emoji', 'user', null
this.placeholder = '';
this._menuControlCount = 0;
this.menuPosition = 'floating'; // 'floating', 'static'
this._slotObserver = new MutationObserver(this._processSlotChanges.bind(this));
}
@@ -188,16 +191,19 @@ class TpRichTextBox extends FormElement(LitElement) {
return;
}
// No formatting controls slotted — no menu, ever. Consumers without
// tp-rtb-* buttons (chat composer, agents composer) otherwise got an
// EMPTY floating box popping up on every text selection.
if (!this._menuControlCount) return;
// In floating mode, hide menu if selection is empty or a caret
if (!hasSelection) {
this.hideMenu();
console.log('Selection is empty or a caret. Hiding menu.');
return;
}
// Show selection menu for text selection
this.showSelectionMenu();
console.log('Text selected. Showing selection menu.');
}
/**
@@ -253,6 +259,7 @@ class TpRichTextBox extends FormElement(LitElement) {
}
_handleSlotChange(e) {
this._menuControlCount = e.target.assignedElements().length;
this._processChildExtensions();
}
@@ -298,6 +305,10 @@ class TpRichTextBox extends FormElement(LitElement) {
Text,
HardBreak,
UndoRedo,
// Empty-state placeholder text, rendered by the consumer's CSS
// (.tiptap p.is-editor-empty:first-child::before) from the
// data-placeholder attribute the extension decorates onto the node.
Placeholder.configure({ placeholder: this.placeholder }),
];
// Collect suggestion configurations from child components
@@ -417,6 +428,19 @@ class TpRichTextBox extends FormElement(LitElement) {
}
}
updated(changes) {
super.updated(changes);
// The placeholder is baked into the editor's extension config —
// re-create the editor (content preserved) when it changes. Consumers
// typically set it once as a static attribute.
if (changes.has('placeholder') && this.editor) {
const html = this.editor.getHTML();
this._initEditor();
if (html) this.editor.commands.setContent(html);
}
}
get value() {
if (this.editor) {
return { html: this.editor.getHTML(), json: this.editor.getJSON() };