/** @license Copyright (c) 2025 trading_peter This program is available under Apache License Version 2.0 */ import { LitElement, html, css } from 'lit'; import { Editor } from '@tiptap/core'; 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'; class TpRichTextBox extends LitElement { static get styles() { return [ css` :host { display: flex; flex-direction: column; border: 1px solid #ccc; min-height: 100px; position: relative; } #editor { padding: 8px; flex-grow: 1; } .custom-floating-menu { display: flex; padding: 4px; background-color: #fff; border: 1px solid #ccc; border-radius: 4px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); position: absolute; top: 0; left: 0; z-index: 100; } .custom-floating-menu[hidden] { display: none; } ` ]; } constructor() { super(); this.extensions = []; this._slotObserver = new MutationObserver(this._processSlotChanges.bind(this)); } render() { return html`
`; } static get properties() { return { editor: { type: Object }, extensions: { type: Array } }; } firstUpdated() { // Get initial extensions from slot this._processChildExtensions(); // Initialize the editor with collected extensions this._initEditor(); // Observe future slot changes this._slotObserver.observe(this, { childList: true, subtree: false }); } _initEditor() { if (this.editor) { this.editor.destroy(); } this.editor = new Editor({ element: this.shadowRoot.querySelector('#editor'), extensions: this.extensions, content: 'Hello World!
', }); // Notify child extensions that the editor is ready const children = Array.from(this.children); children.forEach(child => { if (child._editorReady && typeof child._editorReady === 'function') { child._editorReady(this.editor); } }); this.editor.on('selectionUpdate', this._handleSelectionUpdate.bind(this)); this.editor.on('blur', this._handleSelectionUpdate.bind(this)); } _handleSelectionUpdate() { const { editor } = this; const menu = this.shadowRoot.querySelector('.custom-floating-menu'); if (!editor || !menu) { console.log('Editor or menu element not found.'); return; } const { empty, from, to } = editor.state.selection; if (empty || from === to) { menu.hidden = true; console.log('Selection is empty or a caret. Hiding menu.'); return; } menu.hidden = false; requestAnimationFrame(() => { this._positionMenu(menu, editor); console.log('Text selected. Showing menu.'); }); } _positionMenu(menuElement, editorInstance) { const { view } = editorInstance; const { selection } = editorInstance.state; const { from, to } = selection; // Get the coordinates of the selection const start = view.coordsAtPos(from); const end = view.coordsAtPos(to); // Calculate the center of the selection let left = (start.left + end.left) / 2; console.log('Menu offsetHeight:', menuElement.offsetHeight); let top = start.top - menuElement.offsetHeight - 10; // 10px above selection // If the calculated top is negative, position the menu below the selection if (top < 0) { top = end.bottom + 10; // 10px below selection } // Adjust left position if it goes out of viewport on the right const viewportWidth = window.innerWidth; const menuWidth = menuElement.offsetWidth; if (left + menuWidth > viewportWidth) { left = viewportWidth - menuWidth - 10; // 10px padding from right edge } // Ensure left is not negative if (left < 0) { left = 10; // 10px padding from left edge } // Position the menu menuElement.style.left = `${left}px`; menuElement.style.top = `${top}px`; console.log(`Menu positioned at: left=${left}px, top=${top}px`); } _handleSlotChange(e) { this._processChildExtensions(); } _processSlotChanges(mutations) { let needsUpdate = false; mutations.forEach(mutation => { if (mutation.type === 'childList') { needsUpdate = true; } }); if (needsUpdate) { this._processChildExtensions(); } } _processChildExtensions() { this.extensions = [ Document, Paragraph, Text, HardBreak ]; // Get all extension components const children = Array.from(this.children); children.forEach(child => { // If the child has a getExtension method, it's an extension component if (child.getExtension && typeof child.getExtension === 'function') { const extension = child.getExtension(); if (extension) { this.extensions.push(extension); } } }); // Re-initialize the editor with new extensions if it already exists if (this.editor) { this._initEditor(); } } disconnectedCallback() { super.disconnectedCallback(); if (this._slotObserver) { this._slotObserver.disconnect(); } if (this.editor) { this.editor.destroy(); } } } window.customElements.define('tp-rich-text-box', TpRichTextBox);