From 8b0f798b5a47a5819e9ccc403e5d5764ffbcef6d Mon Sep 17 00:00:00 2001 From: pk Date: Sat, 26 Sep 2026 18:53:42 +0200 Subject: [PATCH] Add placeholder and hide floating menu if no actions are exposed. --- tp-rich-text-box.js | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/tp-rich-text-box.js b/tp-rich-text-box.js index de6b002..881016e 100644 --- a/tp-rich-text-box.js +++ b/tp-rich-text-box.js @@ -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) {
@@ -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() };