A first version that has some working standard features.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,3 +14,4 @@ lerna-debug.log*
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
demo/dist
|
||||
|
17440
demo/dist/the-app.js
vendored
17440
demo/dist/the-app.js
vendored
File diff suppressed because one or more lines are too long
@@ -11,6 +11,12 @@ import '../../tp-rtb-strike.js';
|
||||
import '../../tp-rtb-underline.js';
|
||||
import '../../tp-rtb-code.js';
|
||||
import '../../tp-rtb-clear-format.js';
|
||||
import '../../tp-rtb-link.js';
|
||||
import '../../tp-rtb-undo.js';
|
||||
import '../../tp-rtb-redo.js';
|
||||
import '../../tp-rtb-emoji-suggestion.js';
|
||||
import '../../tp-rtb-user-mention.js';
|
||||
|
||||
import { LitElement, html, css } from 'lit';
|
||||
|
||||
class TheApp extends LitElement {
|
||||
@@ -24,6 +30,13 @@ class TheApp extends LitElement {
|
||||
inset: 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
[slot="editor-content"] {
|
||||
padding: 8px;
|
||||
flex-grow: 1;
|
||||
border: 1px solid #ccc;
|
||||
min-height: 100px;
|
||||
}
|
||||
`
|
||||
];
|
||||
}
|
||||
@@ -31,23 +44,71 @@ class TheApp extends LitElement {
|
||||
render() {
|
||||
return html`
|
||||
<tp-rich-text-box>
|
||||
<div slot="content" id="editor-content-element"></div>
|
||||
<!-- Selection Menu Controls -->
|
||||
<tp-rtb-bold></tp-rtb-bold>
|
||||
<tp-rtb-italic></tp-rtb-italic>
|
||||
<tp-rtb-strike></tp-rtb-strike>
|
||||
<tp-rtb-underline></tp-rtb-underline>
|
||||
<tp-rtb-code></tp-rtb-code>
|
||||
<tp-rtb-clear-format></tp-rtb-clear-format>
|
||||
<tp-rtb-link></tp-rtb-link>
|
||||
<tp-rtb-undo></tp-rtb-undo>
|
||||
<tp-rtb-redo></tp-rtb-redo>
|
||||
<!-- Suggestion Menu Content -->
|
||||
<tp-rtb-emoji-suggestion slot="suggestion-content"></tp-rtb-emoji-suggestion>
|
||||
<tp-rtb-user-mention slot="suggestion-content"></tp-rtb-user-mention>
|
||||
</tp-rich-text-box>
|
||||
<div style="margin-top: 20px;">
|
||||
<button @click="${this._saveContent}">Save JSON</button>
|
||||
<button @click="${this._loadContent}">Load JSON</button>
|
||||
</div>
|
||||
<div style="margin-top: 10px; font-size: 14px; color: #666;">
|
||||
<p><strong>Instructions:</strong></p>
|
||||
<ul>
|
||||
<li>Select text to see formatting controls</li>
|
||||
<li>Type ":" for emoji suggestions (e.g., ":grinning")</li>
|
||||
<li>Type "@" for user mentions (e.g., "@john")</li>
|
||||
</ul>
|
||||
</div>
|
||||
<textarea id="jsonContent" style="width: 100%; height: 200px; margin-top: 10px;"></textarea>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_editor: { type: Object },
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._editor = null;
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this._editor = this.shadowRoot.querySelector('tp-rich-text-box');
|
||||
}
|
||||
|
||||
_saveContent() {
|
||||
if (this._editor) {
|
||||
const jsonContent = this._editor.value;
|
||||
this.shadowRoot.querySelector('#jsonContent').value = JSON.stringify(jsonContent, null, 2);
|
||||
console.log('Content saved:', jsonContent);
|
||||
}
|
||||
}
|
||||
|
||||
_loadContent() {
|
||||
if (this._editor) {
|
||||
try {
|
||||
const jsonContent = JSON.parse(this.shadowRoot.querySelector('#jsonContent').value);
|
||||
this._editor.value = jsonContent;
|
||||
console.log('Content loaded:', jsonContent);
|
||||
} catch (e) {
|
||||
console.error('Invalid JSON:', e);
|
||||
alert('Invalid JSON content. Please check the textarea.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
236
menu-positioner.js
Normal file
236
menu-positioner.js
Normal file
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* Utility for positioning floating menus relative to editor selections or cursor positions
|
||||
*/
|
||||
export class MenuPositioner {
|
||||
/**
|
||||
* Position a menu element relative to a text selection or cursor position
|
||||
* @param {HTMLElement} menuElement - The menu element to position
|
||||
* @param {Editor} editorInstance - The TipTap editor instance
|
||||
* @param {Object|null} clientRect - Optional custom client rect for positioning
|
||||
*/
|
||||
static positionMenu(menuElement, editorInstance, clientRect = null) {
|
||||
const { view } = editorInstance;
|
||||
const { selection } = editorInstance.state;
|
||||
|
||||
let start, end;
|
||||
if (clientRect) {
|
||||
// Ensure clientRect has all required properties
|
||||
start = {
|
||||
left: clientRect.left || 0,
|
||||
right: clientRect.right || clientRect.left || 0,
|
||||
top: clientRect.top || 0,
|
||||
bottom: clientRect.bottom || clientRect.top || 0,
|
||||
};
|
||||
end = start;
|
||||
} else {
|
||||
try {
|
||||
start = view.coordsAtPos(selection.from);
|
||||
end = view.coordsAtPos(selection.to);
|
||||
} catch (error) {
|
||||
console.warn('Failed to get coordinates from editor selection:', error);
|
||||
// Fallback to a default position
|
||||
start = { left: 10, top: 10, right: 20, bottom: 20 };
|
||||
end = start;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate coordinates
|
||||
if (!start || typeof start.left !== 'number' || typeof start.top !== 'number') {
|
||||
console.warn('Invalid start coordinates, using fallback:', start);
|
||||
start = { left: 10, top: 10, right: 20, bottom: 20 };
|
||||
}
|
||||
if (!end || typeof end.left !== 'number' || typeof end.top !== 'number') {
|
||||
console.warn('Invalid end coordinates, using start coordinates:', end);
|
||||
end = start;
|
||||
}
|
||||
|
||||
// Calculate the center of the selection
|
||||
let left = (start.left + end.left) / 2;
|
||||
|
||||
// Get menu dimensions - ensure menu is visible to get accurate measurements
|
||||
const wasHidden = menuElement.style.display === 'none';
|
||||
if (wasHidden) {
|
||||
menuElement.style.visibility = 'hidden';
|
||||
menuElement.style.display = 'block';
|
||||
}
|
||||
|
||||
const menuWidth = menuElement.offsetWidth || 200; // fallback width
|
||||
const menuHeight = menuElement.offsetHeight || 100; // fallback height
|
||||
|
||||
if (wasHidden) {
|
||||
menuElement.style.display = 'none';
|
||||
menuElement.style.visibility = '';
|
||||
}
|
||||
|
||||
// Start by trying to position above the selection
|
||||
let top = start.top - menuHeight - 10; // 10px above selection
|
||||
let placement = 'above';
|
||||
|
||||
// If the calculated top is negative or too close to viewport top, position below
|
||||
if (top < 10) {
|
||||
top = (end.bottom || end.top || start.top) + 10; // 10px below selection
|
||||
placement = 'below';
|
||||
}
|
||||
|
||||
// Adjust horizontal position if it goes out of viewport
|
||||
const viewportWidth = window.innerWidth;
|
||||
|
||||
// Check if menu would overflow on the right
|
||||
if (left + menuWidth / 2 > viewportWidth - 10) {
|
||||
left = viewportWidth - menuWidth - 10; // Align to right edge with padding
|
||||
}
|
||||
// Check if menu would overflow on the left
|
||||
else if (left - menuWidth / 2 < 10) {
|
||||
left = 10; // Align to left edge with padding
|
||||
}
|
||||
// Center the menu horizontally on the selection
|
||||
else {
|
||||
left = left - menuWidth / 2;
|
||||
}
|
||||
|
||||
// Final validation
|
||||
if (isNaN(left) || isNaN(top)) {
|
||||
console.warn('NaN detected in basic positioning, using fallback values:', { left, top });
|
||||
left = isNaN(left) ? 10 : left;
|
||||
top = isNaN(top) ? 10 : top;
|
||||
}
|
||||
|
||||
// Apply positioning
|
||||
menuElement.style.left = `${left}px`;
|
||||
menuElement.style.top = `${top}px`;
|
||||
|
||||
// Add a data attribute to indicate placement for potential styling
|
||||
menuElement.setAttribute('data-placement', placement);
|
||||
|
||||
console.log(`Menu positioned ${placement} selection at: left=${left}px, top=${top}px`);
|
||||
|
||||
return { left, top, placement };
|
||||
}
|
||||
|
||||
/**
|
||||
* Position a menu element with enhanced viewport awareness
|
||||
* Includes additional checks for viewport boundaries and scroll position
|
||||
* @param {HTMLElement} menuElement - The menu element to position
|
||||
* @param {Editor} editorInstance - The TipTap editor instance
|
||||
* @param {Object|null} clientRect - Optional custom client rect for positioning
|
||||
*/
|
||||
static positionMenuAdvanced(menuElement, editorInstance, clientRect = null) {
|
||||
const { view } = editorInstance;
|
||||
const { selection } = editorInstance.state;
|
||||
|
||||
console.log('positionMenuAdvanced called with:', { clientRect, selection: selection.from + '-' + selection.to });
|
||||
|
||||
let start, end;
|
||||
if (clientRect) {
|
||||
console.log('Using clientRect:', clientRect);
|
||||
// Ensure clientRect has all required properties
|
||||
start = {
|
||||
left: clientRect.left || 0,
|
||||
right: clientRect.right || clientRect.left || 0,
|
||||
top: clientRect.top || 0,
|
||||
bottom: clientRect.bottom || clientRect.top || 0,
|
||||
};
|
||||
end = start;
|
||||
} else {
|
||||
try {
|
||||
start = view.coordsAtPos(selection.from);
|
||||
end = view.coordsAtPos(selection.to);
|
||||
console.log('Using editor coordinates:', { start, end });
|
||||
} catch (error) {
|
||||
console.warn('Failed to get coordinates from editor selection, falling back to basic positioning:', error);
|
||||
return MenuPositioner.positionMenu(menuElement, editorInstance, clientRect);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate coordinates - be more strict about what we consider valid
|
||||
if (!start || typeof start.left !== 'number' || isNaN(start.left) ||
|
||||
typeof start.top !== 'number' || isNaN(start.top) ||
|
||||
start.left === 0 && start.top === 0) {
|
||||
console.warn('Invalid or zero coordinates detected, using editor selection instead:', { start, end });
|
||||
|
||||
// Try to get coordinates from current editor selection
|
||||
try {
|
||||
start = view.coordsAtPos(selection.from);
|
||||
end = view.coordsAtPos(selection.to);
|
||||
console.log('Fallback to editor coordinates worked:', { start, end });
|
||||
} catch (error) {
|
||||
console.warn('Editor coordinates also failed, using basic positioning');
|
||||
return MenuPositioner.positionMenu(menuElement, editorInstance, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Get viewport dimensions and scroll position
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const scrollY = window.scrollY;
|
||||
|
||||
// Get menu dimensions - ensure menu is visible to get accurate measurements
|
||||
const wasHidden = menuElement.style.display === 'none';
|
||||
if (wasHidden) {
|
||||
menuElement.style.visibility = 'hidden';
|
||||
menuElement.style.display = 'block';
|
||||
}
|
||||
|
||||
const menuWidth = menuElement.offsetWidth || 200; // fallback width
|
||||
const menuHeight = menuElement.offsetHeight || 100; // fallback height
|
||||
|
||||
if (wasHidden) {
|
||||
menuElement.style.display = 'none';
|
||||
menuElement.style.visibility = '';
|
||||
}
|
||||
|
||||
// Calculate initial center position
|
||||
let left = (start.left + end.left) / 2;
|
||||
let top, placement;
|
||||
|
||||
// Determine vertical placement based on available space
|
||||
const spaceAbove = start.top - scrollY;
|
||||
const spaceBelow = viewportHeight - (end.bottom - scrollY);
|
||||
|
||||
if (spaceAbove >= menuHeight + 20 && spaceAbove > spaceBelow) {
|
||||
// Position above if there's enough space and it's the better option
|
||||
top = start.top - menuHeight - 10;
|
||||
placement = 'above';
|
||||
} else if (spaceBelow >= menuHeight + 20) {
|
||||
// Position below if there's enough space
|
||||
top = end.bottom + 10;
|
||||
placement = 'below';
|
||||
} else {
|
||||
// Choose the side with more space
|
||||
if (spaceAbove > spaceBelow) {
|
||||
top = Math.max(scrollY + 10, start.top - menuHeight - 10);
|
||||
placement = 'above-constrained';
|
||||
} else {
|
||||
top = Math.min(scrollY + viewportHeight - menuHeight - 10, end.bottom + 10);
|
||||
placement = 'below-constrained';
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust horizontal position
|
||||
const idealLeft = left - menuWidth / 2;
|
||||
|
||||
if (idealLeft < 10) {
|
||||
left = 10;
|
||||
} else if (idealLeft + menuWidth > viewportWidth - 10) {
|
||||
left = viewportWidth - menuWidth - 10;
|
||||
} else {
|
||||
left = idealLeft;
|
||||
}
|
||||
|
||||
// Final validation
|
||||
if (isNaN(left) || isNaN(top)) {
|
||||
console.warn('NaN detected in positioning calculation, using fallback values:', { left, top, start, end });
|
||||
left = isNaN(left) ? 10 : left;
|
||||
top = isNaN(top) ? 10 : top;
|
||||
}
|
||||
|
||||
// Apply positioning
|
||||
menuElement.style.left = `${left}px`;
|
||||
menuElement.style.top = `${top}px`;
|
||||
menuElement.setAttribute('data-placement', placement);
|
||||
|
||||
console.log(`Menu positioned ${placement} at: left=${left}px, top=${top}px (viewport: ${viewportWidth}x${viewportHeight}, scroll: ${scrollY})`);
|
||||
|
||||
return { left, top, placement };
|
||||
}
|
||||
}
|
595
package-lock.json
generated
595
package-lock.json
generated
@@ -9,19 +9,53 @@
|
||||
"version": "0.0.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tiptap/core": "^2.12.0",
|
||||
"@tiptap/extension-bold": "^2.12.0",
|
||||
"@tiptap/extension-code": "^2.24.2",
|
||||
"@tiptap/extension-document": "^2.24.2",
|
||||
"@tiptap/extension-hard-break": "^2.24.2",
|
||||
"@tiptap/extension-italic": "^2.0.0-beta.220",
|
||||
"@tiptap/extension-paragraph": "^2.0.0-beta.220",
|
||||
"@tiptap/extension-strike": "^2.0.0-beta.220",
|
||||
"@tiptap/extension-text": "^2.0.0-beta.220",
|
||||
"@tiptap/extension-underline": "^2.0.0-beta.220",
|
||||
"@floating-ui/dom": "^1.6.0",
|
||||
"@tiptap/core": "^3.0.0",
|
||||
"@tiptap/extension-bold": "^3.0.0",
|
||||
"@tiptap/extension-code": "^3.0.0",
|
||||
"@tiptap/extension-document": "^3.0.0",
|
||||
"@tiptap/extension-hard-break": "^3.0.0",
|
||||
"@tiptap/extension-italic": "^3.0.0",
|
||||
"@tiptap/extension-link": "^3.0.0",
|
||||
"@tiptap/extension-mention": "^3.0.0",
|
||||
"@tiptap/extension-paragraph": "^3.0.0",
|
||||
"@tiptap/extension-strike": "^3.0.0",
|
||||
"@tiptap/extension-text": "^3.0.0",
|
||||
"@tiptap/extension-underline": "^3.0.0",
|
||||
"@tiptap/extensions": "^3.0.0",
|
||||
"@tp/helpers": "^2.7.1",
|
||||
"@tp/tp-button": "^1.3.2",
|
||||
"@tp/tp-dialog": "^1.4.0",
|
||||
"@tp/tp-form": "^1.2.0",
|
||||
"@tp/tp-input": "^1.1.2",
|
||||
"lit": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
|
||||
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
|
||||
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.3",
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lit-labs/ssr-dom-shim": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz",
|
||||
@@ -45,139 +79,185 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@tiptap/core": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.12.0.tgz",
|
||||
"integrity": "sha512-3qX8oGVKFFZzQ0vit+ZolR6AJIATBzmEmjAA0llFhWk4vf3v64p1YcXcJsOBsr5scizJu5L6RYWEFatFwqckRg==",
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.6.2.tgz",
|
||||
"integrity": "sha512-XKZYrCVFsyQGF6dXQR73YR222l/76wkKfZ+2/4LCrem5qtcOarmv5pYxjUBG8mRuBPskTTBImSFTeQltJIUNCg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/pm": "^2.7.0"
|
||||
"@tiptap/pm": "^3.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-bold": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.12.0.tgz",
|
||||
"integrity": "sha512-lAUtoLDLRc5ofD2I9MFY6MQ7d1qBLLqS1rvpwaPjOaoQb/GPVnaHj9qXYG0SY9K3erMtto48bMFpAcscjZHzZQ==",
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.6.2.tgz",
|
||||
"integrity": "sha512-Q9KO8CCPCAXYqHzIw8b/ookVmrfqfCg2cyh9h9Hvw6nhO4LOOnJMcGVmWsrpFItbwCGMafI5iY9SbSj7RpCyuw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.7.0"
|
||||
"@tiptap/core": "^3.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-code": {
|
||||
"version": "2.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.24.2.tgz",
|
||||
"integrity": "sha512-dK1jOm0Xe0h8SUXVUJPj3AxWb1N4zeBkdPZFoz+iUHacpymMinH1CuukN9UpwmSi0YPfrIMKkCaw5WOEzjV8RA==",
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.6.2.tgz",
|
||||
"integrity": "sha512-U6jilbcpCxtLZAgJrTapXzzVJTXnS78kJITFSOLyGCTyGSm6PXatQ4hnaxVGmNet66GySONGjhwAVZ8+l94Rwg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.7.0"
|
||||
"@tiptap/core": "^3.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-document": {
|
||||
"version": "2.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.24.2.tgz",
|
||||
"integrity": "sha512-w3q1JaWZlwK8aHmF4lrFqalLssNkZoS3rjL/iS0v69q/fTI9t0WmCx5Jx427eUlNITZ5XoCL8zguKmnSPbFovg==",
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.6.2.tgz",
|
||||
"integrity": "sha512-4qg3KWL3aO1M7hfDpZR6/vSo7Cfqr3McyGUfqb/BXqYDW1DwT8jJkDTcHrGU7WUKRlWgoyPyzM8pZiGlP0uQHg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.7.0"
|
||||
"@tiptap/core": "^3.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-hard-break": {
|
||||
"version": "2.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.24.2.tgz",
|
||||
"integrity": "sha512-6TB9GBUTp3DIOptQubEVvL6BVKhxfLzAJwWYXjw0EkZHrK8TQPB3QIjLV/uZy29Ruji2k97ytxuxfrGoQXoXtA==",
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.6.2.tgz",
|
||||
"integrity": "sha512-ncuPBHhGY58QjluJvEH6vXotaa1QZ/vphXBGAr55kiATZwMIEHgwh2Hgc6AiFTcw057gabGn6jNFDfRB+HjbmA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.7.0"
|
||||
"@tiptap/core": "^3.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-italic": {
|
||||
"version": "2.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.24.2.tgz",
|
||||
"integrity": "sha512-gW9c0zJh4f9D2uZl13rhV8FFt7UgISLiRp4e+DynpKUkhjftDHmruii5Qw6fz9W5cf/vQcyMwCN3lO7Efqnyng==",
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.6.2.tgz",
|
||||
"integrity": "sha512-46zYKqM3o9w1A2G9hWr0ERGbJpqIncoH45XIfLdAI6ZldZVVf+NeXMGwjOPf4+03cZ5/emk3MRTnVp9vF4ToIg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.7.0"
|
||||
"@tiptap/core": "^3.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-link": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.6.2.tgz",
|
||||
"integrity": "sha512-3yiRDWa187h30e6iUOJeejZLsbzbJthLfBwTeJGx7pHh7RngsEW82npBRuqLoI3udhJGTkXbzwAFZ9qOGOjl1Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"linkifyjs": "^4.3.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.6.2",
|
||||
"@tiptap/pm": "^3.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-mention": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-mention/-/extension-mention-3.6.2.tgz",
|
||||
"integrity": "sha512-GrD+CB+kYWSAG1GzrWHoqPqtrE16vfNV2QnAwMb1tdmJIBBF971OVx6qjdvjxXdbccwnHMeCIl+oj7ydrwtQLw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.6.2",
|
||||
"@tiptap/pm": "^3.6.2",
|
||||
"@tiptap/suggestion": "^3.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-paragraph": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.12.0.tgz",
|
||||
"integrity": "sha512-QNK5cgewCunWFxpLlbvvoO1rrLgEtNKxiY79fctP9toV+e59R+1i1Q9lXC1O5mOfDgVxCb6uFDMsqmKhFjpPog==",
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.6.2.tgz",
|
||||
"integrity": "sha512-jeJWj2xKib3392iHQEcB7wYZ30dUgXuwqpCTwtN9eANor+Zvv6CpDKBs1R2al6BYFbIJCgKeTulqxce0yoC80g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.7.0"
|
||||
"@tiptap/core": "^3.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-strike": {
|
||||
"version": "2.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.24.2.tgz",
|
||||
"integrity": "sha512-7JJ+IOTOoXlAqXDiUY9A+oRx01vRClvKuQzIDQoDtvd4Ut9rkZ+9L+Iv7AE/HzGkOOAVvfvLzYcyHiHDOpArDA==",
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.6.2.tgz",
|
||||
"integrity": "sha512-976u5WaioIN/0xCjl/UIEypmzACzxgVz6OGgfIsYyreMUiPjhhgzXb0A/2Po5p3nZpKcaMcxifOdhqdw+lDpIQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.7.0"
|
||||
"@tiptap/core": "^3.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-text": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.12.0.tgz",
|
||||
"integrity": "sha512-0ytN9V1tZYTXdiYDQg4FB2SQ56JAJC9r/65snefb9ztl+gZzDrIvih7CflHs1ic9PgyjexfMLeH+VzuMccNyZw==",
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.6.2.tgz",
|
||||
"integrity": "sha512-fFSUEv1H3lM92yr6jZdELk0gog8rPTK5hTf08kP8RsY8pA80Br1ADVenejrMV4UNTmT1JWTXGBGhMqfQFHUvAQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.7.0"
|
||||
"@tiptap/core": "^3.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-underline": {
|
||||
"version": "2.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-2.24.2.tgz",
|
||||
"integrity": "sha512-vzsGRGsHkoV43tnJKjb4aLzVYtJ531Puxjf3qToGP5kRqyuSl2FyCARTZUHgVhMmD7Yu6oXsrXvTR3pNaDuIrA==",
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.6.2.tgz",
|
||||
"integrity": "sha512-IrG6vjxTMI2EeyhZCtx0sNTEu83PsAvzIh4vxmG1fUi/RYokks+sFbgGMuq0jtO96iVNEszlpAC/vaqfxFJwew==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.7.0"
|
||||
"@tiptap/core": "^3.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extensions": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.6.2.tgz",
|
||||
"integrity": "sha512-tg7/DgaI6SpkeawryapUtNoBxsJUMJl3+nSjTfTvsaNXed+BHzLPsvmPbzlF9ScrAbVEx8nj6CCkneECYIQ4CQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.6.2",
|
||||
"@tiptap/pm": "^3.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/pm": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.12.0.tgz",
|
||||
"integrity": "sha512-TNzVwpeNzFfHAcYTOKqX9iU4fRxliyoZrCnERR+RRzeg7gWrXrCLubQt1WEx0sojMAfznshSL3M5HGsYjEbYwA==",
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.6.2.tgz",
|
||||
"integrity": "sha512-g+NXjqjbj6NfHOMl22uNWVYIu8oCq7RFfbnpohPMsSKJLaHYE8mJR++7T6P5R9FoqhIFdwizg1jTpwRU5CHqXQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -191,20 +271,417 @@
|
||||
"prosemirror-keymap": "^1.2.2",
|
||||
"prosemirror-markdown": "^1.13.1",
|
||||
"prosemirror-menu": "^1.2.4",
|
||||
"prosemirror-model": "^1.23.0",
|
||||
"prosemirror-model": "^1.24.1",
|
||||
"prosemirror-schema-basic": "^1.2.3",
|
||||
"prosemirror-schema-list": "^1.4.1",
|
||||
"prosemirror-schema-list": "^1.5.0",
|
||||
"prosemirror-state": "^1.4.3",
|
||||
"prosemirror-tables": "^1.6.4",
|
||||
"prosemirror-trailing-node": "^3.0.0",
|
||||
"prosemirror-transform": "^1.10.2",
|
||||
"prosemirror-view": "^1.37.0"
|
||||
"prosemirror-view": "^1.38.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/suggestion": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-3.6.2.tgz",
|
||||
"integrity": "sha512-tkqHAiRvxo5tnRF/Y6l/VOg7Hno9X6lBjqajEXWPfPjlW/6tdC08Te9Gr7+mhfd168yPDjVhLyKMI209ypnwbw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.6.2",
|
||||
"@tiptap/pm": "^3.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/helpers": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Fhelpers/-/2.7.1/helpers-2.7.1.tgz",
|
||||
"integrity": "sha512-/6VvEPfDI8qz7BO3ziEA9we2mlU2lmK5KwjqlbUknUA5Jw0NUCv2IscAJ59i2J/9c17SFZfCuWXfXmhCvV4PhA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@tp/tp-button": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Ftp-button/-/1.3.2/tp-button-1.3.2.tgz",
|
||||
"integrity": "sha512-TcOu46nD8pSTL0Sll/rdn83AFfFmtSHkyFp+qS90Cz4hvJ2iNRuFKKKgT6JC4QPIp0eYPAH1D2y9bARIzDr36w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tp/helpers": "^1.0.0",
|
||||
"@tp/tp-icon": "^1.0.0",
|
||||
"@tp/tp-spinner": "^1.0.0",
|
||||
"lit": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-button/node_modules/@lit/reactive-element": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz",
|
||||
"integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-button/node_modules/@tp/helpers": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Fhelpers/-/1.3.0/helpers-1.3.0.tgz",
|
||||
"integrity": "sha512-mOAVP45kkEYXwonaOd5jkFQLX1nbeKtl8YX8FpL2ytON0cOSsh6TUAbCEcMU5xqgyD6L1ZEZNvxCjhOKOKdGyA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@tp/tp-button/node_modules/lit": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz",
|
||||
"integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^1.6.0",
|
||||
"lit-element": "^3.3.0",
|
||||
"lit-html": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-button/node_modules/lit-element": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz",
|
||||
"integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.1.0",
|
||||
"@lit/reactive-element": "^1.3.0",
|
||||
"lit-html": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-button/node_modules/lit-html": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz",
|
||||
"integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-dialog": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Ftp-dialog/-/1.4.0/tp-dialog-1.4.0.tgz",
|
||||
"integrity": "sha512-jwAXkoPHZx11PdmfD9smIN4tM3Bf0N8YViaNQLZbamNNwCKSeJSHZErrKp7TeNsSKP7Qlu6rnhZeQtgk7h5Raw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tp/helpers": "^1.0.1",
|
||||
"@tp/tp-icon": "^1.0.0",
|
||||
"lit": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-dialog/node_modules/@lit/reactive-element": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz",
|
||||
"integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-dialog/node_modules/@tp/helpers": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Fhelpers/-/1.3.0/helpers-1.3.0.tgz",
|
||||
"integrity": "sha512-mOAVP45kkEYXwonaOd5jkFQLX1nbeKtl8YX8FpL2ytON0cOSsh6TUAbCEcMU5xqgyD6L1ZEZNvxCjhOKOKdGyA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@tp/tp-dialog/node_modules/lit": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz",
|
||||
"integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^1.6.0",
|
||||
"lit-element": "^3.3.0",
|
||||
"lit-html": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-dialog/node_modules/lit-element": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz",
|
||||
"integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.1.0",
|
||||
"@lit/reactive-element": "^1.3.0",
|
||||
"lit-html": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-dialog/node_modules/lit-html": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz",
|
||||
"integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-form": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Ftp-form/-/1.2.0/tp-form-1.2.0.tgz",
|
||||
"integrity": "sha512-MhN0olIBoWfFLYPHiEC2M8xrhVxy+zyzRofMwBY/GZ3yScbOLaz/dNzY88LV/7DUlmqVdvdlBoSu1f79hsAEqw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tp/helpers": "^2.3.1",
|
||||
"lit": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-form/node_modules/@lit/reactive-element": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz",
|
||||
"integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-form/node_modules/lit": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz",
|
||||
"integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^1.6.0",
|
||||
"lit-element": "^3.3.0",
|
||||
"lit-html": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-form/node_modules/lit-element": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz",
|
||||
"integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.1.0",
|
||||
"@lit/reactive-element": "^1.3.0",
|
||||
"lit-html": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-form/node_modules/lit-html": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz",
|
||||
"integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-icon": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Ftp-icon/-/1.0.1/tp-icon-1.0.1.tgz",
|
||||
"integrity": "sha512-rBbQoXZ5t35F7yIbPAEGAlDscZhxLZ5/o229kyiBBrXvCrc+aVOsetSwF1jPeBSmb57h2PfinIvQhtMARwWHoA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tp/tp-tooltip": "^1.0.0",
|
||||
"lit": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-icon/node_modules/@lit/reactive-element": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz",
|
||||
"integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-icon/node_modules/lit": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz",
|
||||
"integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^1.6.0",
|
||||
"lit-element": "^3.3.0",
|
||||
"lit-html": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-icon/node_modules/lit-element": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz",
|
||||
"integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.1.0",
|
||||
"@lit/reactive-element": "^1.3.0",
|
||||
"lit-html": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-icon/node_modules/lit-html": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz",
|
||||
"integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-input": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Ftp-input/-/1.1.2/tp-input-1.1.2.tgz",
|
||||
"integrity": "sha512-SAhhLZF5nxul/epgiXay0Q2uGpzXNHhugf7EvPPezOG1WveOpXJPd80AWxhi+Z1LL6kHPRXC3kKUpz6+cA5dQA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tp/helpers": "^1.0.0",
|
||||
"lit": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-input/node_modules/@lit/reactive-element": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz",
|
||||
"integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-input/node_modules/@tp/helpers": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Fhelpers/-/1.3.0/helpers-1.3.0.tgz",
|
||||
"integrity": "sha512-mOAVP45kkEYXwonaOd5jkFQLX1nbeKtl8YX8FpL2ytON0cOSsh6TUAbCEcMU5xqgyD6L1ZEZNvxCjhOKOKdGyA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@tp/tp-input/node_modules/lit": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz",
|
||||
"integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^1.6.0",
|
||||
"lit-element": "^3.3.0",
|
||||
"lit-html": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-input/node_modules/lit-element": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz",
|
||||
"integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.1.0",
|
||||
"@lit/reactive-element": "^1.3.0",
|
||||
"lit-html": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-input/node_modules/lit-html": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz",
|
||||
"integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-spinner": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Ftp-spinner/-/1.0.0/tp-spinner-1.0.0.tgz",
|
||||
"integrity": "sha512-/OcQNTxeTQ4u3YYiWZ4GO6CjKbG2N0Oy/En8ryt0E5ggdR+GxW0Z6w/6fHl9qMnChIcZ3sY5XzBmazSP8ISU1Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"lit": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-spinner/node_modules/@lit/reactive-element": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz",
|
||||
"integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-spinner/node_modules/lit": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz",
|
||||
"integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^1.6.0",
|
||||
"lit-element": "^3.3.0",
|
||||
"lit-html": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-spinner/node_modules/lit-element": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz",
|
||||
"integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.1.0",
|
||||
"@lit/reactive-element": "^1.3.0",
|
||||
"lit-html": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-spinner/node_modules/lit-html": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz",
|
||||
"integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-tooltip": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Ftp-tooltip/-/1.0.0/tp-tooltip-1.0.0.tgz",
|
||||
"integrity": "sha512-wal/DPJH73rz9RbHg66ZciZUyjqfeTKMSImEVWczwjXGoPTG9n5FL5+tPyikpgFr5KDhDKlW8/Q0niBbGnc5KA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tp/helpers": "^1.0.0",
|
||||
"lit": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-tooltip/node_modules/@lit/reactive-element": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz",
|
||||
"integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-tooltip/node_modules/@tp/helpers": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Fhelpers/-/1.3.0/helpers-1.3.0.tgz",
|
||||
"integrity": "sha512-mOAVP45kkEYXwonaOd5jkFQLX1nbeKtl8YX8FpL2ytON0cOSsh6TUAbCEcMU5xqgyD6L1ZEZNvxCjhOKOKdGyA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@tp/tp-tooltip/node_modules/lit": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz",
|
||||
"integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^1.6.0",
|
||||
"lit-element": "^3.3.0",
|
||||
"lit-html": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-tooltip/node_modules/lit-element": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz",
|
||||
"integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.1.0",
|
||||
"@lit/reactive-element": "^1.3.0",
|
||||
"lit-html": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tp/tp-tooltip/node_modules/lit-html": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz",
|
||||
"integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
@@ -286,6 +763,12 @@
|
||||
"uc.micro": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/linkifyjs": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz",
|
||||
"integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lit": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-3.3.0.tgz",
|
||||
|
31
package.json
31
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@tp/tp-rich-text-box",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"main": "tp-rich-text-box.js",
|
||||
"scripts": {
|
||||
@@ -13,16 +13,25 @@
|
||||
"author": "trading_peter",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tiptap/core": "^2.12.0",
|
||||
"@tiptap/extension-bold": "^2.12.0",
|
||||
"@tiptap/extension-code": "^2.24.2",
|
||||
"@tiptap/extension-document": "^2.24.2",
|
||||
"@tiptap/extension-hard-break": "^2.24.2",
|
||||
"@tiptap/extension-italic": "^2.0.0-beta.220",
|
||||
"@tiptap/extension-paragraph": "^2.0.0-beta.220",
|
||||
"@tiptap/extension-strike": "^2.0.0-beta.220",
|
||||
"@tiptap/extension-text": "^2.0.0-beta.220",
|
||||
"@tiptap/extension-underline": "^2.0.0-beta.220",
|
||||
"@floating-ui/dom": "^1.6.0",
|
||||
"@tiptap/core": "^3.0.0",
|
||||
"@tiptap/extension-bold": "^3.0.0",
|
||||
"@tiptap/extension-code": "^3.0.0",
|
||||
"@tiptap/extension-document": "^3.0.0",
|
||||
"@tiptap/extension-hard-break": "^3.0.0",
|
||||
"@tiptap/extension-italic": "^3.0.0",
|
||||
"@tiptap/extension-link": "^3.0.0",
|
||||
"@tiptap/extension-mention": "^3.0.0",
|
||||
"@tiptap/extension-paragraph": "^3.0.0",
|
||||
"@tiptap/extension-strike": "^3.0.0",
|
||||
"@tiptap/extension-text": "^3.0.0",
|
||||
"@tiptap/extension-underline": "^3.0.0",
|
||||
"@tiptap/extensions": "^3.0.0",
|
||||
"@tp/helpers": "^2.7.1",
|
||||
"@tp/tp-button": "^1.3.2",
|
||||
"@tp/tp-dialog": "^1.4.0",
|
||||
"@tp/tp-form": "^1.2.0",
|
||||
"@tp/tp-input": "^1.1.2",
|
||||
"lit": "^3.0.0"
|
||||
}
|
||||
}
|
||||
|
@@ -10,8 +10,15 @@ 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 Mention from '@tiptap/extension-mention';
|
||||
import { MenuPositioner } from './menu-positioner.js';
|
||||
import './tp-rtb-emoji-suggestion.js';
|
||||
|
||||
class TpRichTextBox extends LitElement {
|
||||
|
||||
import { FormElement } from '@tp/helpers/form-element.js';
|
||||
|
||||
class TpRichTextBox extends FormElement(LitElement) {
|
||||
static get styles() {
|
||||
return [
|
||||
css`
|
||||
@@ -19,15 +26,9 @@ class TpRichTextBox extends LitElement {
|
||||
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;
|
||||
@@ -44,6 +45,27 @@ class TpRichTextBox extends LitElement {
|
||||
.custom-floating-menu[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.suggestion-menu {
|
||||
display: block;
|
||||
padding: 0;
|
||||
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: 101;
|
||||
}
|
||||
|
||||
.suggestion-menu[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
slot[name="content"]::slotted(*) {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
`
|
||||
];
|
||||
}
|
||||
@@ -51,22 +73,33 @@ class TpRichTextBox extends LitElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.extensions = [];
|
||||
this.menuMode = 'hidden'; // 'hidden', 'selection', 'suggestion'
|
||||
this.activeSuggestionType = null; // 'emoji', 'user', null
|
||||
this._slotObserver = new MutationObserver(this._processSlotChanges.bind(this));
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="custom-floating-menu" hidden>
|
||||
<!-- Selection-based floating menu with all controls -->
|
||||
<div class="custom-floating-menu" ?hidden=${this.menuMode !== 'selection'}>
|
||||
<slot @slotchange=${this._handleSlotChange}></slot>
|
||||
</div>
|
||||
<div id="editor"></div>
|
||||
|
||||
<!-- Suggestion-based menu for emoji/mention triggers -->
|
||||
<div class="suggestion-menu" ?hidden=${this.menuMode !== 'suggestion'}>
|
||||
<slot name="suggestion-content" @slotchange=${this._handleSuggestionSlotChange}></slot>
|
||||
</div>
|
||||
|
||||
<slot name="content"></slot>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
editor: { type: Object },
|
||||
extensions: { type: Array }
|
||||
extensions: { type: Array },
|
||||
menuMode: { type: String, reflect: true },
|
||||
activeSuggestionType: { type: String }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -90,7 +123,7 @@ class TpRichTextBox extends LitElement {
|
||||
}
|
||||
|
||||
this.editor = new Editor({
|
||||
element: this.shadowRoot.querySelector('#editor'),
|
||||
element: this.querySelector('[slot="content"]'),
|
||||
extensions: this.extensions,
|
||||
content: '<p>Hello World!</p>',
|
||||
});
|
||||
@@ -109,69 +142,88 @@ class TpRichTextBox extends LitElement {
|
||||
|
||||
_handleSelectionUpdate() {
|
||||
const { editor } = this;
|
||||
const menu = this.shadowRoot.querySelector('.custom-floating-menu');
|
||||
|
||||
if (!editor || !menu) {
|
||||
console.log('Editor or menu element not found.');
|
||||
|
||||
if (!editor) {
|
||||
console.log('Editor not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const { empty, from, to } = editor.state.selection;
|
||||
|
||||
// Hide menu if selection is empty or a caret
|
||||
if (empty || from === to) {
|
||||
menu.hidden = true;
|
||||
this.hideMenu();
|
||||
console.log('Selection is empty or a caret. Hiding menu.');
|
||||
return;
|
||||
}
|
||||
|
||||
menu.hidden = false;
|
||||
// Show selection menu for text selection
|
||||
this.showSelectionMenu();
|
||||
console.log('Text selected. Showing selection menu.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the selection-based floating menu
|
||||
*/
|
||||
showSelectionMenu() {
|
||||
this.menuMode = 'selection';
|
||||
requestAnimationFrame(() => {
|
||||
this._positionMenu(menu, editor);
|
||||
console.log('Text selected. Showing menu.');
|
||||
const menu = this.shadowRoot.querySelector('.custom-floating-menu');
|
||||
if (menu) {
|
||||
MenuPositioner.positionMenu(menu, this.editor);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_positionMenu(menuElement, editorInstance) {
|
||||
const { view } = editorInstance;
|
||||
const { selection } = editorInstance.state;
|
||||
const { from, to } = selection;
|
||||
/**
|
||||
* Show the suggestion-based menu (for emoji/mention triggers)
|
||||
*/
|
||||
showSuggestionMenu(clientRect = null, suggestionType = null) {
|
||||
this.menuMode = 'suggestion';
|
||||
this.activeSuggestionType = suggestionType;
|
||||
|
||||
// Update component visibility
|
||||
this._updateSuggestionComponentVisibility();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const menu = this.shadowRoot.querySelector('.suggestion-menu');
|
||||
if (menu) {
|
||||
MenuPositioner.positionMenuAdvanced(menu, this.editor, clientRect);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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`);
|
||||
/**
|
||||
* Hide all menus
|
||||
*/
|
||||
hideMenu() {
|
||||
this.menuMode = 'hidden';
|
||||
this.activeSuggestionType = null;
|
||||
// Reset all suggestion component visibility
|
||||
this._updateSuggestionComponentVisibility();
|
||||
}
|
||||
|
||||
_handleSlotChange(e) {
|
||||
this._processChildExtensions();
|
||||
}
|
||||
|
||||
_handleSuggestionSlotChange(e) {
|
||||
// Update visibility of suggestion components based on activeSuggestionType
|
||||
this._updateSuggestionComponentVisibility();
|
||||
}
|
||||
|
||||
_updateSuggestionComponentVisibility() {
|
||||
const emojiComponent = this.querySelector('tp-rtb-emoji-suggestion[slot="suggestion-content"]');
|
||||
const userComponent = this.querySelector('tp-rtb-user-mention[slot="suggestion-content"]');
|
||||
|
||||
if (emojiComponent) {
|
||||
emojiComponent.style.display = this.activeSuggestionType === 'emoji' ? 'block' : 'none';
|
||||
}
|
||||
if (userComponent) {
|
||||
userComponent.style.display = this.activeSuggestionType === 'user' ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
_processSlotChanges(mutations) {
|
||||
let needsUpdate = false;
|
||||
|
||||
@@ -191,15 +243,24 @@ class TpRichTextBox extends LitElement {
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
HardBreak
|
||||
HardBreak,
|
||||
UndoRedo,
|
||||
];
|
||||
|
||||
// Get all extension components
|
||||
// Collect suggestion configurations from child components
|
||||
const suggestions = [];
|
||||
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') {
|
||||
// If the child has a getSuggestionConfig method, it provides suggestion configuration
|
||||
if (child.getSuggestionConfig && typeof child.getSuggestionConfig === 'function') {
|
||||
const suggestionConfig = child.getSuggestionConfig();
|
||||
if (suggestionConfig) {
|
||||
suggestions.push(suggestionConfig);
|
||||
}
|
||||
}
|
||||
// If the child has a getExtension method but not getSuggestionConfig, it's a regular extension
|
||||
else if (child.getExtension && typeof child.getExtension === 'function') {
|
||||
const extension = child.getExtension();
|
||||
if (extension) {
|
||||
this.extensions.push(extension);
|
||||
@@ -207,6 +268,36 @@ class TpRichTextBox extends LitElement {
|
||||
}
|
||||
});
|
||||
|
||||
// If we have suggestions, add a single Mention extension with all suggestions
|
||||
if (suggestions.length > 0) {
|
||||
this.extensions.push(
|
||||
Mention.extend({
|
||||
renderText({ options, node }) {
|
||||
// Return just the label without the trigger character
|
||||
return node.attrs.label || node.attrs.id;
|
||||
},
|
||||
renderHTML({ options, node }) {
|
||||
return [
|
||||
'span',
|
||||
{
|
||||
class: 'mention',
|
||||
'data-type': 'mention',
|
||||
'data-id': node.attrs.id,
|
||||
'data-label': node.attrs.label,
|
||||
contenteditable: 'false'
|
||||
},
|
||||
node.attrs.label || node.attrs.id
|
||||
];
|
||||
},
|
||||
}).configure({
|
||||
HTMLAttributes: {
|
||||
class: 'mention',
|
||||
},
|
||||
suggestions: suggestions
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Re-initialize the editor with new extensions if it already exists
|
||||
if (this.editor) {
|
||||
this._initEditor();
|
||||
@@ -222,6 +313,34 @@ class TpRichTextBox extends LitElement {
|
||||
this.editor.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (this.editor) {
|
||||
this.editor.chain().focus().undo().run();
|
||||
}
|
||||
}
|
||||
|
||||
redo() {
|
||||
if (this.editor) {
|
||||
this.editor.chain().focus().redo().run();
|
||||
}
|
||||
}
|
||||
|
||||
get value() {
|
||||
if (this.editor) {
|
||||
return this.editor.getJSON();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
set value(jsonContent) {
|
||||
this.updateComplete.then(() => {
|
||||
if (this.editor) {
|
||||
this.editor.commands.setContent(jsonContent);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('tp-rich-text-box', TpRichTextBox);
|
||||
|
462
tp-rtb-emoji-suggestion.js
Normal file
462
tp-rtb-emoji-suggestion.js
Normal file
@@ -0,0 +1,462 @@
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { TpRtbBaseExtension } from './tp-rtb-base-extension.js';
|
||||
import Mention from '@tiptap/extension-mention';
|
||||
import { closest } from '@tp/helpers/closest.js';
|
||||
|
||||
const emojis = [
|
||||
{ emoji: '😀', name: 'grinning face' },
|
||||
{ emoji: '😃', name: 'grinning face with big eyes' },
|
||||
{ emoji: '😄', name: 'grinning face with smiling eyes' },
|
||||
{ emoji: '😁', name: 'beaming face with smiling eyes' },
|
||||
{ emoji: '😆', name: 'grinning squinting face' },
|
||||
{ emoji: '😅', name: 'grinning face with sweat' },
|
||||
{ emoji: '🤣', name: 'rolling on the floor laughing' },
|
||||
{ emoji: '😂', name: 'face with tears of joy' },
|
||||
{ emoji: '🙂', name: 'slightly smiling face' },
|
||||
{ emoji: '🙃', name: 'upside-down face' },
|
||||
{ emoji: '😉', name: 'winking face' },
|
||||
{ emoji: '😊', name: 'smiling face with smiling eyes' },
|
||||
{ emoji: '😇', name: 'smiling face with halo' },
|
||||
{ emoji: '🥰', name: 'smiling face with hearts' },
|
||||
{ emoji: '😍', name: 'smiling face with heart-eyes' },
|
||||
{ emoji: '🤩', name: 'star-struck' },
|
||||
{ emoji: '😘', name: 'face blowing a kiss' },
|
||||
{ emoji: '😗', name: 'kissing face' },
|
||||
{ emoji: '😚', name: 'kissing face with closed eyes' },
|
||||
{ emoji: '😙', name: 'kissing face with smiling eyes' },
|
||||
{ emoji: '😋', name: 'savoring food' },
|
||||
{ emoji: '😛', name: 'face with tongue' },
|
||||
{ emoji: '😜', name: 'winking face with tongue' },
|
||||
{ emoji: '🤪', name: 'zany face' },
|
||||
{ emoji: '😝', name: 'squinting face with tongue' },
|
||||
{ emoji: '🤑', name: 'money-mouth face' },
|
||||
{ emoji: '🤗', name: 'hugging face' },
|
||||
{ emoji: '🤭', name: 'hand over mouth' },
|
||||
{ emoji: '🤫', name: 'shushing face' },
|
||||
{ emoji: '🤔', name: 'thinking face' },
|
||||
{ emoji: '🤐', name: 'zipper-mouth face' },
|
||||
{ emoji: '🤨', name: 'face with raised eyebrow' },
|
||||
{ emoji: '😐', name: 'neutral face' },
|
||||
{ emoji: '😑', name: 'expressionless face' },
|
||||
{ emoji: '😶', name: 'face without mouth' },
|
||||
{ emoji: '😏', name: 'smirking face' },
|
||||
{ emoji: '😒', name: 'unamused face' },
|
||||
{ emoji: '🙄', name: 'face with rolling eyes' },
|
||||
{ emoji: '😬', name: 'grimacing face' },
|
||||
{ emoji: '🤥', name: 'lying face' },
|
||||
{ emoji: '😌', name: 'relieved face' },
|
||||
{ emoji: '😔', name: 'pensive face' },
|
||||
{ emoji: '😪', name: 'sleepy face' },
|
||||
{ emoji: '🤤', name: 'drooling face' },
|
||||
{ emoji: '😴', name: 'sleeping face' },
|
||||
{ emoji: '😷', name: 'face with medical mask' },
|
||||
{ emoji: '🤒', name: 'face with thermometer' },
|
||||
{ emoji: '🤕', name: 'face with head-bandage' },
|
||||
{ emoji: '🤢', name: 'nauseated face' },
|
||||
{ emoji: '🤮', name: 'face vomiting' },
|
||||
{ emoji: '🤧', name: 'sneezing face' },
|
||||
{ emoji: '🥵', name: 'hot face' },
|
||||
{ emoji: '🥶', name: 'cold face' },
|
||||
{ emoji: '🥴', name: 'woozy face' },
|
||||
{ emoji: '😵', name: 'dizzy face' },
|
||||
{ emoji: '🤯', name: 'exploding head' },
|
||||
{ emoji: '🤠', name: 'cowboy hat face' },
|
||||
{ emoji: '🥳', name: 'partying face' },
|
||||
{ emoji: '😎', name: 'smiling face with sunglasses' },
|
||||
{ emoji: '🤓', name: 'nerd face' },
|
||||
{ emoji: '🧐', name: 'face with monocle' },
|
||||
{ emoji: '😕', name: 'confused face' },
|
||||
{ emoji: '😟', name: 'worried face' },
|
||||
{ emoji: '🙁', name: 'slightly frowning face' },
|
||||
{ emoji: '😮', name: 'face with open mouth' },
|
||||
{ emoji: '😯', name: 'hushed face' },
|
||||
{ emoji: '😲', name: 'astonished face' },
|
||||
{ emoji: '😳', name: 'flushed face' },
|
||||
{ emoji: '🥺', name: 'pleading face' },
|
||||
{ emoji: '😦', name: 'frowning face with open mouth' },
|
||||
{ emoji: '😧', name: 'anguished face' },
|
||||
{ emoji: '😨', name: 'fearful face' },
|
||||
{ emoji: '😰', name: 'anxious face with sweat' },
|
||||
{ emoji: '😥', name: 'sad but relieved face' },
|
||||
{ emoji: '😢', name: 'crying face' },
|
||||
{ emoji: '😭', name: 'loudly crying face' },
|
||||
{ emoji: '😱', name: 'face screaming in fear' },
|
||||
{ emoji: '😖', name: 'confounded face' },
|
||||
{ emoji: '😣', name: 'persevering face' },
|
||||
{ emoji: '😩', name: 'weary face' },
|
||||
{ emoji: '😫', name: 'tired face' },
|
||||
{ emoji: '😤', name: 'face with steam from nose' },
|
||||
{ emoji: '😡', name: 'pouting face' },
|
||||
{ emoji: '😠', name: 'angry face' },
|
||||
{ emoji: '🤬', name: 'face with symbols on mouth' },
|
||||
{ emoji: '😈', name: 'smiling face with horns' },
|
||||
{ emoji: '👿', name: 'angry face with horns' },
|
||||
{ emoji: '💀', name: 'skull' },
|
||||
{ emoji: '☠️', name: 'skull and crossbones' },
|
||||
{ emoji: '💩', name: 'pile of poo' },
|
||||
{ emoji: '🤡', name: 'clown face' },
|
||||
{ emoji: '👹', name: 'ogre' },
|
||||
{ emoji: '👺', name: 'goblin' },
|
||||
{ emoji: '👻', name: 'ghost' },
|
||||
{ emoji: '👽', name: 'alien' },
|
||||
{ emoji: '👾', name: 'alien monster' },
|
||||
{ emoji: '🤖', name: 'robot face' },
|
||||
{ emoji: '🎃', name: 'jack-o-lantern' },
|
||||
{ emoji: '😺', name: 'grinning cat' },
|
||||
{ emoji: '😸', name: 'grinning cat with smiling eyes' },
|
||||
{ emoji: '😹', name: 'cat with tears of joy' },
|
||||
{ emoji: '😻', name: 'smiling cat with heart-eyes' },
|
||||
{ emoji: '😼', name: 'wry cat' },
|
||||
{ emoji: '😽', name: 'kissing cat' },
|
||||
{ emoji: '🙀', name: 'weary cat' },
|
||||
{ emoji: '😿', name: 'crying cat' },
|
||||
{ emoji: '😾', name: 'pouting cat' },
|
||||
{ emoji: '👋', name: 'waving hand' },
|
||||
{ emoji: '🤚', name: 'raised back of hand' },
|
||||
{ emoji: '🖐️', name: 'hand with fingers splayed' },
|
||||
{ emoji: '✋', name: 'raised hand' },
|
||||
{ emoji: '🖖', name: 'vulcan salute' },
|
||||
{ emoji: '👌', name: 'ok hand' },
|
||||
{ emoji: '🤏', name: 'pinching hand' },
|
||||
{ emoji: '✌️', name: 'victory hand' },
|
||||
{ emoji: '🤞', name: 'crossed fingers' },
|
||||
{ emoji: '🤟', name: 'love-you gesture' },
|
||||
{ emoji: '🤘', name: 'sign of the horns' },
|
||||
{ emoji: '🤙', name: 'call me hand' },
|
||||
{ emoji: '👈', name: 'backhand index pointing left' },
|
||||
{ emoji: '👉', name: 'backhand index pointing right' },
|
||||
{ emoji: '👆', name: 'backhand index pointing up' },
|
||||
{ emoji: '🖕', name: 'middle finger' },
|
||||
{ emoji: '👇', name: 'backhand index pointing down' },
|
||||
{ emoji: '☝️', name: 'index pointing up' },
|
||||
{ emoji: '👍', name: 'thumbs up' },
|
||||
{ emoji: '👎', name: 'thumbs down' },
|
||||
{ emoji: '✊', name: 'raised fist' },
|
||||
{ emoji: '👊', name: 'oncoming fist' },
|
||||
{ emoji: '🤛', name: 'left-facing fist' },
|
||||
{ emoji: '🤜', name: 'right-facing fist' },
|
||||
{ emoji: '👏', name: 'clapping hands' },
|
||||
{ emoji: '🙌', name: 'raising hands' },
|
||||
{ emoji: '👐', name: 'open hands' },
|
||||
{ emoji: '🤲', name: 'palms up together' },
|
||||
{ emoji: '🤝', name: 'handshake' },
|
||||
{ emoji: '🙏', name: 'folded hands' },
|
||||
{ emoji: '✍️', name: 'writing hand' },
|
||||
{ emoji: '💅', name: 'nail polish' },
|
||||
{ emoji: '🤳', name: 'selfie' },
|
||||
{ emoji: '💪', name: 'flexed biceps' },
|
||||
{ emoji: '🦵', name: 'leg' },
|
||||
{ emoji: '🦶', name: 'foot' },
|
||||
{ emoji: '👂', name: 'ear' },
|
||||
{ emoji: '👃', name: 'nose' },
|
||||
{ emoji: '🧠', name: 'brain' },
|
||||
{ emoji: '🦷', name: 'tooth' },
|
||||
{ emoji: '🦴', name: 'bone' },
|
||||
{ emoji: '👀', name: 'eyes' },
|
||||
{ emoji: '👁️', name: 'eye' },
|
||||
{ emoji: '👅', name: 'tongue' },
|
||||
{ emoji: '👄', name: 'mouth' },
|
||||
{ emoji: '👶', name: 'baby' },
|
||||
{ emoji: '🧒', name: 'child' },
|
||||
{ emoji: '👦', name: 'boy' },
|
||||
{ emoji: '👧', name: 'girl' },
|
||||
{ emoji: '🧑', name: 'person' },
|
||||
{ emoji: '👨', name: 'man' },
|
||||
{ emoji: '👩', name: 'woman' },
|
||||
{ emoji: '🧓', name: 'older person' },
|
||||
{ emoji: '👴', name: 'old man' },
|
||||
{ emoji: '👵', name: 'old woman' },
|
||||
{ emoji: '👨⚕️', name: 'man health worker' },
|
||||
{ emoji: '👩⚕️', name: 'woman health worker' },
|
||||
{ emoji: '👨🎓', name: 'man student' },
|
||||
{ emoji: '👩🎓', name: 'woman student' },
|
||||
{ emoji: '👨🏫', name: 'man teacher' },
|
||||
{ emoji: '👩🏫', name: 'woman teacher' },
|
||||
{ emoji: '👨⚖️', name: 'man judge' },
|
||||
{ emoji: '👩⚖️', name: 'woman judge' },
|
||||
{ emoji: '👨🌾', name: 'man farmer' },
|
||||
{ emoji: '👩🌾', name: 'woman farmer' },
|
||||
{ emoji: '👨🍳', name: 'man cook' },
|
||||
{ emoji: '👩🍳', name: 'woman cook' },
|
||||
{ emoji: '👨🔧', name: 'man mechanic' },
|
||||
{ emoji: '👩🔧', name: 'woman mechanic' },
|
||||
{ emoji: '👨🏭', name: 'man factory worker' },
|
||||
{ emoji: '👩🏭', name: 'woman factory worker' },
|
||||
{ emoji: '👨 사무실', name: 'man office worker' },
|
||||
{ emoji: '👩 사무실', name: 'woman office worker' },
|
||||
{ emoji: '👨🔬', name: 'man scientist' },
|
||||
{ emoji: '👩🔬', name: 'woman scientist' },
|
||||
{ emoji: '👨💻', name: 'man technologist' },
|
||||
{ emoji: '👩💻', name: 'woman technologist' },
|
||||
{ emoji: '👨🎤', name: 'man singer' },
|
||||
{ emoji: '👩🎤', name: 'woman singer' },
|
||||
{ emoji: '👨🎨', name: 'man artist' },
|
||||
{ emoji: '👩🎨', name: 'woman artist' },
|
||||
{ emoji: '👨✈️', name: 'man pilot' },
|
||||
{ emoji: '👩✈️', name: 'woman pilot' },
|
||||
{ emoji: '👨🚀', name: 'man astronaut' },
|
||||
{ emoji: '👩🚀', name: 'woman astronaut' },
|
||||
{ emoji: '👨🚒', name: 'man firefighter' },
|
||||
{ emoji: '👩🚒', name: 'woman firefighter' },
|
||||
{ emoji: '👮', name: 'police officer' },
|
||||
{ emoji: '👮♂️', name: 'man police officer' },
|
||||
{ emoji: '👮♀️', name: 'woman police officer' },
|
||||
{ emoji: '🕵️', name: 'detective' },
|
||||
{ emoji: '🕵️♂️', name: 'man detective' },
|
||||
{ emoji: '🕵️♀️', name: 'woman detective' },
|
||||
{ emoji: '💂', name: 'guard' },
|
||||
{ emoji: '💂♂️', name: 'man guard' },
|
||||
{ emoji: '💂♀️', name: 'woman guard' },
|
||||
{ emoji: '👷', name: 'construction worker' },
|
||||
{ emoji: '👷♂️', name: 'man construction worker' },
|
||||
{ emoji: '👷♀️', name: 'woman construction worker' },
|
||||
{ emoji: '🤴', name: 'prince' },
|
||||
{ emoji: '👸', name: 'princess' },
|
||||
{ emoji: '👳', name: 'person wearing turban' },
|
||||
{ emoji: '👳♂️', name: 'man wearing turban' },
|
||||
{ emoji: '👳♀️', name: 'woman wearing turban' },
|
||||
{ emoji: '👲', name: 'man with skullcap' },
|
||||
{ emoji: '🧕', name: 'woman with headscarf' },
|
||||
{ emoji: '🤵', name: 'man in tuxedo' },
|
||||
{ emoji: '👰', name: 'bride with veil' },
|
||||
{ emoji: '🤰', name: 'pregnant woman' },
|
||||
{ emoji: '🤱', name: 'breast-feeding' },
|
||||
{ emoji: '👼', name: 'baby angel' },
|
||||
{ emoji: '🎅', name: 'santa claus' },
|
||||
{ emoji: '🤶', name: 'mrs. claus' },
|
||||
{ emoji: '🦸', name: 'superhero' },
|
||||
{ emoji: '🦸♂️', name: 'man superhero' },
|
||||
{ emoji: '🦸♀️', name: 'woman superhero' },
|
||||
{ emoji: '🦹', name: 'supervillain' },
|
||||
{ emoji: '🦹♂️', name: 'man supervillain' },
|
||||
{ emoji: '🦹♀️', name: 'woman supervillain' },
|
||||
{ emoji: '🧙', name: 'mage' },
|
||||
{ emoji: '🧙♂️', name: 'man mage' },
|
||||
{ emoji: '🧙♀️', name: 'woman mage' },
|
||||
{ emoji: '🧚', name: 'fairy' },
|
||||
{ emoji: '🧚♂️', name: 'man fairy' },
|
||||
{ emoji: '🧚♀️', name: 'woman fairy' },
|
||||
{ emoji: '🧛', name: 'vampire' },
|
||||
{ emoji: '🧛♂️', name: 'man vampire' },
|
||||
{ emoji: '🧛♀️', name: 'woman vampire' },
|
||||
{ emoji: '🧜', name: 'merperson' },
|
||||
{ emoji: '🧜♂️', name: 'merman' },
|
||||
{ emoji: '🧜♀️', name: 'mermaid' },
|
||||
{ emoji: '🧝', name: 'elf' },
|
||||
{ emoji: '🧝♂️', name: 'man elf' },
|
||||
{ emoji: '🧝♀️', name: 'woman elf' },
|
||||
{ emoji: '🧞', name: 'genie' },
|
||||
{ emoji: '🧞♂️', name: 'man genie' },
|
||||
{ emoji: '🧞♀️', name: 'woman genie' },
|
||||
{ emoji: '🧟', name: 'zombie' },
|
||||
{ emoji: '🧟♂️', name: 'man zombie' },
|
||||
{ emoji: '🧟♀️', name: 'woman zombie' },
|
||||
];
|
||||
|
||||
export class TpRtbEmojiSuggestion extends TpRtbBaseExtension {
|
||||
static getEmojis() {
|
||||
return emojis;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
...super.properties,
|
||||
items: { type: Array },
|
||||
selectedIndex: { type: Number },
|
||||
};
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
super.styles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
padding: 8px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
z-index: 9999;
|
||||
}
|
||||
.item {
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.item.is-selected {
|
||||
background-color: #eee;
|
||||
}
|
||||
`
|
||||
];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.items = [];
|
||||
this.selectedIndex = 0;
|
||||
this.label = 'Emoji';
|
||||
}
|
||||
|
||||
getSuggestionConfig() {
|
||||
return {
|
||||
char: ':',
|
||||
allowSpaces: false,
|
||||
startOfLine: false,
|
||||
items: ({ query }) => {
|
||||
console.log('Emoji query:', query);
|
||||
return TpRtbEmojiSuggestion.getEmojis().filter(item => item.name.toLowerCase().startsWith(query.toLowerCase())).slice(0, 10);
|
||||
},
|
||||
render: () => {
|
||||
let component;
|
||||
let parentEditor;
|
||||
let selectItemHandler;
|
||||
|
||||
return {
|
||||
onStart: props => {
|
||||
console.log('Emoji suggestion onStart called', props);
|
||||
console.log('Props clientRect:', props.clientRect);
|
||||
// Find the parent editor using tp/helpers closest
|
||||
parentEditor = closest(this, 'tp-rich-text-box', true);
|
||||
if (!parentEditor) {
|
||||
console.error('Parent editor not found for emoji suggestion');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find specifically the emoji suggestion component
|
||||
component = parentEditor.querySelector('tp-rtb-emoji-suggestion[slot="suggestion-content"]');
|
||||
|
||||
if (!component) {
|
||||
console.error('Emoji suggestion component not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
component.items = props.items;
|
||||
component.selectedIndex = 0;
|
||||
// Pass the command function to the component
|
||||
component._command = props.command;
|
||||
|
||||
// Show suggestion menu with emoji type
|
||||
if (parentEditor.showSuggestionMenu) {
|
||||
parentEditor.showSuggestionMenu(props.clientRect, 'emoji');
|
||||
} else {
|
||||
console.error('showSuggestionMenu method not available');
|
||||
}
|
||||
|
||||
// Store the handler to remove it later
|
||||
selectItemHandler = (event) => {
|
||||
const emoji = event.detail;
|
||||
console.log('Emoji selected:', emoji);
|
||||
|
||||
// Call command directly like in TipTap Vue example
|
||||
if (component._command) {
|
||||
component._command({
|
||||
id: `emoji-${Date.now()}-${Math.random()}`,
|
||||
label: emoji.emoji
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
component.addEventListener('select-item', selectItemHandler);
|
||||
},
|
||||
onUpdate: (props) => {
|
||||
if (!component || !parentEditor) return;
|
||||
|
||||
component.items = props.items;
|
||||
component.selectedIndex = 0;
|
||||
// Update the command function
|
||||
component._command = props.command;
|
||||
|
||||
// Update suggestion menu position with emoji type
|
||||
if (parentEditor.showSuggestionMenu) {
|
||||
parentEditor.showSuggestionMenu(props.clientRect, 'emoji');
|
||||
}
|
||||
},
|
||||
onKeyDown: (props) => {
|
||||
if (!component) return false;
|
||||
return component.onKeyDown(props);
|
||||
},
|
||||
onExit: () => {
|
||||
console.log('Emoji suggestion onExit called');
|
||||
if (component && selectItemHandler) {
|
||||
component.removeEventListener('select-item', selectItemHandler);
|
||||
component.items = [];
|
||||
component.selectedIndex = 0;
|
||||
}
|
||||
|
||||
// Hide the suggestion menu
|
||||
if (parentEditor && parentEditor.hideMenu) {
|
||||
parentEditor.hideMenu();
|
||||
}
|
||||
component = null;
|
||||
parentEditor = null;
|
||||
selectItemHandler = null;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_handleClick() {
|
||||
// Emoji suggestions are triggered by typing : not by clicking a button
|
||||
// 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) {
|
||||
if (changedProperties.has('selectedIndex')) {
|
||||
const selectedElement = this.shadowRoot.querySelector('.item.is-selected');
|
||||
if (selectedElement) {
|
||||
selectedElement.scrollIntoView({
|
||||
block: 'nearest',
|
||||
inline: 'nearest',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_selectItem(index) {
|
||||
const item = this.items[index];
|
||||
if (item) {
|
||||
this.dispatchEvent(new CustomEvent('select-item', { detail: item }));
|
||||
}
|
||||
}
|
||||
|
||||
onKeyDown({ event }) {
|
||||
if (event.key === 'ArrowUp') {
|
||||
this.selectedIndex = (this.selectedIndex + this.items.length - 1) % this.items.length;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
this.selectedIndex = (this.selectedIndex + 1) % this.items.length;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
this._selectItem(this.selectedIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tp-rtb-emoji-suggestion', TpRtbEmojiSuggestion);
|
97
tp-rtb-link.js
Normal file
97
tp-rtb-link.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import '@tp/tp-dialog/tp-dialog.js';
|
||||
import '@tp/tp-form/tp-form.js';
|
||||
import '@tp/tp-input/tp-input.js';
|
||||
import '@tp/tp-button/tp-button.js';
|
||||
import { html } from 'lit';
|
||||
import { TpRtbBaseExtension } from './tp-rtb-base-extension.js';
|
||||
import Link from '@tiptap/extension-link';
|
||||
|
||||
class TpRtbLink extends TpRtbBaseExtension {
|
||||
constructor() {
|
||||
super();
|
||||
this.label = 'Link';
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
${super.render()}
|
||||
|
||||
<tp-dialog>
|
||||
<h3>Add Link</h3>
|
||||
<tp-form @submit=${this._saveLink}>
|
||||
<tp-input name="url" .value=${this.url} required errorMessage="Required">
|
||||
<input type="text">
|
||||
</tp-input>
|
||||
<tp-input name="text" .value=${this.text}>
|
||||
<input type="text">
|
||||
</tp-input>
|
||||
<div class="buttons">
|
||||
<tp-button dialog-dismiss>Cancel</tp-button>
|
||||
<tp-button submit primary>Save</tp-button>
|
||||
</div>
|
||||
</tp-form>
|
||||
</tp-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
url: { type: String },
|
||||
text: { type: String },
|
||||
};
|
||||
}
|
||||
|
||||
getExtension() {
|
||||
return Link.configure({
|
||||
autolink: true,
|
||||
linkOnPaste: true,
|
||||
openOnClick: false, // We will handle opening links manually
|
||||
});
|
||||
}
|
||||
|
||||
_handleClick() {
|
||||
if (this.parentEditor && this.parentEditor.editor) {
|
||||
const { editor } = this.parentEditor;
|
||||
const { selection } = editor.state;
|
||||
const { from, to } = selection;
|
||||
this.text = editor.state.doc.textBetween(from, to, ' ');
|
||||
this.url = editor.getAttributes('link').href || '';
|
||||
|
||||
const dialog = this.shadowRoot.querySelector('tp-dialog');
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
|
||||
_saveLink(e) {
|
||||
const { url, text } = e.detail;
|
||||
const { editor } = this.parentEditor;
|
||||
|
||||
console.log(e.detail);
|
||||
if (url) {
|
||||
console.log(url, editor);
|
||||
editor.chain().focus().setLink({ href: url }).run();
|
||||
// If text is provided and different from current selection, update it
|
||||
if (text && editor.state.selection.empty) {
|
||||
editor.chain().focus().insertContent(text).run();
|
||||
}
|
||||
} else {
|
||||
editor.chain().focus().unsetLink().run();
|
||||
}
|
||||
|
||||
this.shadowRoot.querySelector('tp-dialog').close();
|
||||
}
|
||||
|
||||
_setupEditorListeners() {
|
||||
const { editor } = this.parentEditor;
|
||||
|
||||
editor.on('selectionUpdate', () => {
|
||||
this.active = editor.isActive('link');
|
||||
});
|
||||
|
||||
editor.on('focus', () => {
|
||||
this.active = editor.isActive('link');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tp-rtb-link', TpRtbLink);
|
23
tp-rtb-redo.js
Normal file
23
tp-rtb-redo.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { html } from 'lit';
|
||||
import { TpRtbBaseExtension } from './tp-rtb-base-extension.js';
|
||||
|
||||
class TpRtbRedo extends TpRtbBaseExtension {
|
||||
constructor() {
|
||||
super();
|
||||
this.label = 'Redo';
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
${super.render()}
|
||||
`;
|
||||
}
|
||||
|
||||
_handleClick() {
|
||||
if (this.parentEditor) {
|
||||
this.parentEditor.redo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tp-rtb-redo', TpRtbRedo);
|
23
tp-rtb-undo.js
Normal file
23
tp-rtb-undo.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { html } from 'lit';
|
||||
import { TpRtbBaseExtension } from './tp-rtb-base-extension.js';
|
||||
|
||||
class TpRtbUndo extends TpRtbBaseExtension {
|
||||
constructor() {
|
||||
super();
|
||||
this.label = 'Undo';
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
${super.render()}
|
||||
`;
|
||||
}
|
||||
|
||||
_handleClick() {
|
||||
if (this.parentEditor) {
|
||||
this.parentEditor.undo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tp-rtb-undo', TpRtbUndo);
|
256
tp-rtb-user-mention.js
Normal file
256
tp-rtb-user-mention.js
Normal file
@@ -0,0 +1,256 @@
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { TpRtbBaseExtension } from './tp-rtb-base-extension.js';
|
||||
import Mention from '@tiptap/extension-mention';
|
||||
import { closest } from '@tp/helpers/closest.js';
|
||||
|
||||
const sampleUsers = [
|
||||
{ id: 'user1', name: 'John Doe', username: 'johndoe', avatar: '👤' },
|
||||
{ id: 'user2', name: 'Jane Smith', username: 'janesmith', avatar: '👩' },
|
||||
{ id: 'user3', name: 'Bob Johnson', username: 'bobjohnson', avatar: '👨' },
|
||||
{ id: 'user4', name: 'Alice Brown', username: 'alicebrown', avatar: '👩💼' },
|
||||
{ id: 'user5', name: 'Charlie Wilson', username: 'charliewilson', avatar: '👨💻' },
|
||||
{ id: 'user6', name: 'Diana Garcia', username: 'dianagarcia', avatar: '👩🔬' },
|
||||
{ id: 'user7', name: 'Edward Davis', username: 'edwarddavis', avatar: '👨🎨' },
|
||||
{ id: 'user8', name: 'Fiona Miller', username: 'fionamiller', avatar: '👩🎓' },
|
||||
{ id: 'user9', name: 'George Taylor', username: 'georgetaylor', avatar: '👨⚕️' },
|
||||
{ id: 'user10', name: 'Helen Anderson', username: 'helenaderson', avatar: '👩🏫' },
|
||||
];
|
||||
|
||||
export class TpRtbUserMention extends TpRtbBaseExtension {
|
||||
static getUsers() {
|
||||
return sampleUsers;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
...super.properties,
|
||||
items: { type: Array },
|
||||
selectedIndex: { type: Number },
|
||||
};
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
super.styles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
padding: 8px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
z-index: 9999;
|
||||
min-width: 200px;
|
||||
}
|
||||
.item {
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.item.is-selected {
|
||||
background-color: #e3f2fd;
|
||||
}
|
||||
.item:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.avatar {
|
||||
font-size: 20px;
|
||||
}
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.name {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
.username {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
`
|
||||
];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.items = [];
|
||||
this.selectedIndex = 0;
|
||||
this.label = 'User Mention';
|
||||
}
|
||||
|
||||
getSuggestionConfig() {
|
||||
return {
|
||||
char: '@',
|
||||
allowSpaces: false,
|
||||
startOfLine: false,
|
||||
items: ({ query }) => {
|
||||
console.log('User mention query:', query);
|
||||
return TpRtbUserMention.getUsers().filter(user =>
|
||||
user.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
user.username.toLowerCase().includes(query.toLowerCase())
|
||||
).slice(0, 10);
|
||||
},
|
||||
render: () => {
|
||||
let component;
|
||||
let parentEditor;
|
||||
let selectItemHandler;
|
||||
|
||||
return {
|
||||
onStart: props => {
|
||||
console.log('User mention onStart called', props);
|
||||
console.log('Props clientRect:', props.clientRect);
|
||||
// Find the parent editor using tp/helpers closest
|
||||
parentEditor = closest(this, 'tp-rich-text-box', true);
|
||||
if (!parentEditor) {
|
||||
console.error('Parent editor not found for user mention');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the user mention component
|
||||
component = parentEditor.querySelector('tp-rtb-user-mention[slot="suggestion-content"]');
|
||||
|
||||
if (!component) {
|
||||
console.error('User mention component not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
component.items = props.items;
|
||||
component.selectedIndex = 0;
|
||||
// Pass the command function to the component
|
||||
component._command = props.command;
|
||||
|
||||
// Show suggestion menu with user type
|
||||
if (parentEditor.showSuggestionMenu) {
|
||||
parentEditor.showSuggestionMenu(props.clientRect, 'user');
|
||||
} else {
|
||||
console.error('showSuggestionMenu method not available');
|
||||
}
|
||||
|
||||
// Store the handler to remove it later
|
||||
selectItemHandler = (event) => {
|
||||
const user = event.detail;
|
||||
console.log('User selected:', user);
|
||||
|
||||
// Call command directly like in TipTap Vue example
|
||||
if (component._command) {
|
||||
component._command({
|
||||
id: user.id,
|
||||
label: user.username
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
component.addEventListener('select-item', selectItemHandler);
|
||||
},
|
||||
onUpdate: (props) => {
|
||||
if (!component || !parentEditor) return;
|
||||
|
||||
component.items = props.items;
|
||||
component.selectedIndex = 0;
|
||||
// Update the command function
|
||||
component._command = props.command;
|
||||
|
||||
// Update suggestion menu position with user type
|
||||
if (parentEditor.showSuggestionMenu) {
|
||||
parentEditor.showSuggestionMenu(props.clientRect, 'user');
|
||||
}
|
||||
},
|
||||
onKeyDown: (props) => {
|
||||
if (!component) return false;
|
||||
return component.onKeyDown(props);
|
||||
},
|
||||
onExit: () => {
|
||||
console.log('User mention onExit called');
|
||||
if (component && selectItemHandler) {
|
||||
component.removeEventListener('select-item', selectItemHandler);
|
||||
component.items = [];
|
||||
component.selectedIndex = 0;
|
||||
}
|
||||
|
||||
// Hide the suggestion menu
|
||||
if (parentEditor && parentEditor.hideMenu) {
|
||||
parentEditor.hideMenu();
|
||||
}
|
||||
component = null;
|
||||
parentEditor = null;
|
||||
selectItemHandler = null;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_handleClick() {
|
||||
// User mentions are triggered by typing @ not by clicking a button
|
||||
// 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) {
|
||||
if (changedProperties.has('selectedIndex')) {
|
||||
const selectedElement = this.shadowRoot.querySelector('.item.is-selected');
|
||||
if (selectedElement) {
|
||||
selectedElement.scrollIntoView({
|
||||
block: 'nearest',
|
||||
inline: 'nearest',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_selectItem(index) {
|
||||
const item = this.items[index];
|
||||
if (item) {
|
||||
this.dispatchEvent(new CustomEvent('select-item', { detail: item }));
|
||||
}
|
||||
}
|
||||
|
||||
onKeyDown({ event }) {
|
||||
if (event.key === 'ArrowUp') {
|
||||
this.selectedIndex = (this.selectedIndex + this.items.length - 1) % this.items.length;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
this.selectedIndex = (this.selectedIndex + 1) % this.items.length;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
this._selectItem(this.selectedIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tp-rtb-user-mention', TpRtbUserMention);
|
Reference in New Issue
Block a user