228 lines
5.6 KiB
JavaScript
228 lines
5.6 KiB
JavaScript
/**
|
|
@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`
|
|
<div class="custom-floating-menu" hidden>
|
|
<slot @slotchange=${this._handleSlotChange}></slot>
|
|
</div>
|
|
<div id="editor"></div>
|
|
`;
|
|
}
|
|
|
|
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: '<p>Hello World!</p>',
|
|
});
|
|
|
|
// 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);
|