Skip to content

Instantly share code, notes, and snippets.

@vidarh
Last active November 7, 2024 19:24
// ==UserScript==
// @name Floating Panel Class
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Reusable floating panel with indicators and burger menu
// @author You
// @grant none
// ==/UserScript==
(function() {
'use strict';
window.FloatingPanel = class FloatingPanel {
constructor(options = {}) {
this.options = {
theme: options.theme || {
background: '#1a1a1a',
border: '#333333',
text: '#ffffff'
}
};
this.menuItems = [];
this.indicators = new Map();
this.isDragging = false;
this.init();
}
init() {
// Create main panel
this.panel = document.createElement('div');
this.panel.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${this.options.theme.background};
border: 1px solid ${this.options.theme.border};
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
z-index: 10000;
font-family: Arial, sans-serif;
width: fit-content;
`;
// Create header bar
this.header = document.createElement('div');
this.header.style.cssText = `
padding: 8px 12px;
background: ${this.options.theme.background};
border-bottom: 1px solid ${this.options.theme.border};
cursor: move;
display: flex;
align-items: center;
gap: 8px;
border-radius: 5px 5px 0 0;
min-height: 32px;
`;
// Create indicators container
this.indicatorsContainer = document.createElement('div');
this.indicatorsContainer.style.cssText = `
display: flex;
align-items: center;
gap: 8px;
flex-grow: 1;
`;
// Create burger menu button
this.burgerButton = document.createElement('div');
this.burgerButton.style.cssText = `
cursor: pointer;
padding: 4px;
color: ${this.options.theme.text};
font-size: 20px;
min-width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s;
user-select: none;
`;
this.burgerButton.innerHTML = '☰';
this.burgerButton.addEventListener('mouseover', () => {
this.burgerButton.style.backgroundColor = 'rgba(255,255,255,0.1)';
});
this.burgerButton.addEventListener('mouseout', () => {
this.burgerButton.style.backgroundColor = 'transparent';
});
// Create menu container
this.menuContainer = document.createElement('div');
this.menuContainer.style.cssText = `
position: absolute;
right: 0;
top: 100%;
background: ${this.options.theme.background};
border: 1px solid ${this.options.theme.border};
border-radius: 3px;
display: none;
min-width: 150px;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
z-index: 10001;
`;
// Assembly
this.header.appendChild(this.indicatorsContainer);
this.header.appendChild(this.burgerButton);
this.panel.appendChild(this.header);
this.panel.appendChild(this.menuContainer);
document.body.appendChild(this.panel);
this.setupDragging();
this.setupMenuToggle();
}
setupDragging() {
let lastX = 0;
let lastY = 0;
this.header.addEventListener('mousedown', (e) => {
if (e.target === this.header || e.target === this.indicatorsContainer) {
this.isDragging = true;
lastX = e.clientX;
lastY = e.clientY;
e.preventDefault();
}
});
document.addEventListener('mousemove', (e) => {
if (this.isDragging) {
const deltaX = lastX - e.clientX;
const deltaY = e.clientY - lastY;
const currentRight = parseInt(this.panel.style.right);
const currentTop = parseInt(this.panel.style.top);
this.panel.style.right = (currentRight + deltaX) + 'px';
this.panel.style.top = (currentTop + deltaY) + 'px';
lastX = e.clientX;
lastY = e.clientY;
}
});
document.addEventListener('mouseup', () => {
this.isDragging = false;
});
}
setupMenuToggle() {
this.burgerButton.addEventListener('click', (e) => {
e.stopPropagation();
this.menuContainer.style.display =
this.menuContainer.style.display === 'none' ? 'block' : 'none';
});
document.addEventListener('click', (e) => {
if (!this.menuContainer.contains(e.target) &&
!this.burgerButton.contains(e.target)) {
this.menuContainer.style.display = 'none';
}
});
}
addMenuItem(label, callback) {
const item = document.createElement('div');
item.style.cssText = `
padding: 8px 15px;
cursor: pointer;
color: ${this.options.theme.text};
transition: background-color 0.2s;
`;
item.textContent = label;
item.addEventListener('mouseover', () => {
item.style.backgroundColor = 'rgba(255,255,255,0.1)';
});
item.addEventListener('mouseout', () => {
item.style.backgroundColor = 'transparent';
});
item.addEventListener('click', () => {
callback();
this.menuContainer.style.display = 'none';
});
this.menuContainer.appendChild(item);
this.menuItems.push({ label, callback });
}
addIndicator(node) {
const wrapper = document.createElement('div');
wrapper.style.cssText = `
padding: 4px 8px;
border: 1px solid ${this.options.theme.border};
border-radius: 3px;
background: rgba(255,255,255,0.05);
display: flex;
align-items: center;
height: fit-content;
min-height: 24px;
color: ${this.options.theme.text};
font-size: 12px;
white-space: nowrap;
`;
if (node instanceof Text) {
const span = document.createElement('span');
span.style.color = this.options.theme.text;
span.appendChild(node);
wrapper.appendChild(span);
} else {
node.style.color = node.style.color || this.options.theme.text;
wrapper.appendChild(node);
}
this.indicatorsContainer.appendChild(wrapper);
this.indicators.set(node, wrapper);
return wrapper;
}
removeIndicator(node) {
const wrapper = this.indicators.get(node);
if (wrapper) {
wrapper.remove();
this.indicators.delete(node);
}
}
destroy() {
this.panel.remove();
this.menuItems = [];
this.indicators.clear();
}
};
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment