-
Notifications
You must be signed in to change notification settings - Fork 46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature Request:Add a context menu #252
Comments
interface ContextMenuItem {
label: string;
onClick: () => void;
disabled?: boolean; // Optional: Disabled state for menu items
}
class ContextMenu {
private menu: HTMLDivElement;
constructor() {
this.menu = document.createElement('div');
this.menu.id = 'context-menu';
this.menu.style.cssText = `
position: fixed;
background-color: white;
border: 1px solid #ccc;
padding: 5px;
display: none;
z-index: 1000; // Ensure it's on top
box-shadow: 2px 2px 5px rgba(0,0,0,0.2); // Add a subtle shadow
`; // Use cssText for more efficient styling
document.body.appendChild(this.menu);
// Close on outside click
document.addEventListener('click', this.handleDocumentClick);
// Prevent menu from closing when clicking inside
this.menu.addEventListener('click', (event) => event.stopPropagation());
}
private handleDocumentClick = (event: MouseEvent) => { // Explicit type
if (!this.menu.contains(event.target as Node)) {
this.hide();
}
};
show(x: number, y: number, items: ContextMenuItem[]) {
this.menu.innerHTML = ''; // Clear previous items
items.forEach(item => {
const menuItem = document.createElement('div');
menuItem.textContent = item.label;
menuItem.style.cssText = `
padding: 3px 5px;
cursor: pointer;
${item.disabled ? 'color: #aaa; cursor: default;' : ''} // Style disabled items
`;
if (!item.disabled) { // Only add listener if not disabled
menuItem.addEventListener('click', item.onClick);
} else {
menuItem.style.cursor = 'default';
}
this.menu.appendChild(menuItem);
});
this.menu.style.left = x + 'px';
this.menu.style.top = y + 'px';
this.menu.style.display = 'block';
}
hide() {
this.menu.style.display = 'none';
}
destroy() {
document.removeEventListener('click', this.handleDocumentClick);
this.menu.remove(); // Important: Remove from DOM to prevent memory leaks
}
}
// Example usage:
const contextMenu = new ContextMenu();
document.addEventListener('contextmenu', (event) => {
event.preventDefault();
const selectedText = window.getSelection()?.toString() || '';
const hasSelection = selectedText.length > 0;
const menuItems: ContextMenuItem[] = [
{
label: 'Copy',
onClick: () => {
navigator.clipboard.writeText(selectedText).catch(console.error);
contextMenu.hide();
},
disabled: !hasSelection
},
{
label: 'Paste',
onClick: async () => {
try {
const text = await navigator.clipboard.readText();
const focusedElement = document.activeElement as HTMLInputElement | HTMLTextAreaElement;
if (focusedElement) {
focusedElement.value += text;
} else {
alert("No focusable element found to paste into.");
}
} catch (error) {
console.error("Paste failed:", error);
alert("Error on paste. Check console."); // Provide user feedback
}
contextMenu.hide();
}
},
{
label: 'Cut',
onClick: () => {
navigator.clipboard.writeText(selectedText).then(() => {
if (window.getSelection) {
const sel = window.getSelection();
if (sel.rangeCount) {
sel.deleteFromDocument();
}
}
}).catch(console.error);
contextMenu.hide();
},
disabled: !hasSelection
}
];
contextMenu.show(event.clientX, event.clientY, menuItems);
});
// Important: When you're done with the context menu (e.g., on page unload), call destroy:
window.addEventListener('beforeunload', () => {
contextMenu.destroy();
}); |
interface ContextMenuItem {
label: string;
onClick: () => void;
disabled?: boolean;
}
class ContextMenu {
private menu: HTMLDivElement;
constructor() {
this.menu = document.createElement('div');
this.menu.id = 'context-menu';
this.menu.style.cssText = `
position: fixed;
background-color: white;
border: 1px solid #ccc;
padding: 5px;
display: none;
z-index: 1000;
box-shadow: 2px 2px 5px rgba(0,0,0,0.2);
`;
document.body.appendChild(this.menu);
document.addEventListener('click', this.handleDocumentClick);
this.menu.addEventListener('click', (event) => event.stopPropagation());
}
private handleDocumentClick = (event: MouseEvent) => {
if (!this.menu.contains(event.target as Node)) {
this.hide();
}
};
show(x: number, y: number, items: ContextMenuItem[]) {
this.menu.innerHTML = '';
items.forEach(item => {
const menuItem = document.createElement('div');
menuItem.textContent = item.label;
menuItem.style.cssText = `
padding: 3px 5px;
cursor: pointer;
${item.disabled ? 'color: #aaa; cursor: default;' : ''}
`;
if (!item.disabled) {
menuItem.addEventListener('click', item.onClick);
}
this.menu.appendChild(menuItem);
});
this.menu.style.left = x + 'px';
this.menu.style.top = y + 'px';
this.menu.style.display = 'block';
}
hide() {
this.menu.style.display = 'none';
}
destroy() {
document.removeEventListener('click', this.handleDocumentClick);
this.menu.remove();
}
}
const contextMenu = new ContextMenu();
document.addEventListener('contextmenu', (event) => {
event.preventDefault();
const selectedText = window.getSelection()?.toString() || '';
const hasSelection = selectedText.length > 0;
const menuItems: ContextMenuItem[] = [
{
label: 'Copy',
onClick: () => {
navigator.clipboard.writeText(selectedText).catch(console.error);
contextMenu.hide();
},
disabled: !hasSelection
},
{
label: 'Paste',
onClick: async () => {
try {
const text = await navigator.clipboard.readText();
const focusedElement = document.activeElement as HTMLInputElement | HTMLTextAreaElement;
if (focusedElement) {
focusedElement.value += text;
} else {
alert("No focusable element found to paste into.");
}
} catch (error) {
console.error("Paste failed:", error);
alert("Error on paste. Check console.");
}
contextMenu.hide();
}
},
{
label: 'Cut',
onClick: () => {
navigator.clipboard.writeText(selectedText).then(() => {
if (window.getSelection) {
const sel = window.getSelection();
if (sel.rangeCount) {
sel.deleteFromDocument();
}
}
}).catch(console.error);
contextMenu.hide();
},
disabled: !hasSelection
},
{
label: 'Save',
onClick: () => {
if (hasSelection) {
const blob = new Blob([selectedText], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'selected_text.txt'; // Dateiname
link.click();
URL.revokeObjectURL(url); // Speicher freigeben
}
contextMenu.hide();
},
disabled: !hasSelection
}
];
contextMenu.show(event.clientX, event.clientY, menuItems);
});
window.addEventListener('beforeunload', () => {
contextMenu.destroy();
}); |
I'm not entirely sure why we need dedicated buttons for cut, copy & paste. |
Oh, the context menu is only needed for the packaged version. If I install the application e.g. in Arch Linux or Windows and right click, there is no context menu. |
Oh, I see. Thanks for the clarification. Given the complexity of this change I think we would be better off using an existing library to handle this, such as That being said, since keyboard shortcuts do appear to work as expected (and since we also have a bunch of dedicated Copy buttons already) we'll probably punt on this improvement for later. PS: Another workaround you can try in the meantime, while the packaged app is running, you can visit |
I tried launching Hollama as an application (I'm using Hollama with Arch Linux), and when I open it in the browser, none of my sessions or knowledge is displayed. I have to set everything up again. |
Hey, that's a good application, but it should have a Context Menu with the following functions to make the app more user-friendly for many users.
The required features are:
This Context Menu would greatly enhance the usability of the application for many users.
The text was updated successfully, but these errors were encountered: