779 lines
24 KiB
JavaScript
779 lines
24 KiB
JavaScript
/**
|
|
* Toolbar UI Module
|
|
*
|
|
* Provides a minimal, collapsible UI panel for annotation tools.
|
|
* Features:
|
|
* - Toggle annotation mode on/off
|
|
* - Tool selector: pen / eraser
|
|
* - Size and opacity sliders
|
|
* - Color picker
|
|
* - Undo, redo, clear buttons
|
|
*
|
|
* The toolbar is designed to be non-intrusive and can be collapsed
|
|
* to minimize clutter in the node editor.
|
|
*/
|
|
|
|
// ============================================================================
|
|
// Constants
|
|
// ============================================================================
|
|
|
|
const TOOLBAR_ID = 'annotation-toolbar';
|
|
const LOG_PREFIX = '[Toolbar]';
|
|
|
|
// Default colors for the color picker
|
|
const PRESET_COLORS = [
|
|
'#FF5555', // Red (default)
|
|
'#55FF55', // Green
|
|
'#5555FF', // Blue
|
|
'#FFFF55', // Yellow
|
|
'#FF55FF', // Magenta
|
|
'#55FFFF', // Cyan
|
|
'#FFFFFF', // White
|
|
'#000000' // Black
|
|
];
|
|
|
|
// ============================================================================
|
|
// State
|
|
// ============================================================================
|
|
|
|
let toolbarElement = null;
|
|
let isCollapsed = false;
|
|
let callbacks = {
|
|
onToggleMode: null,
|
|
onSetTool: null,
|
|
onSetColor: null,
|
|
onSetSize: null,
|
|
onSetOpacity: null,
|
|
onSetEraserSize: null,
|
|
onUndo: null,
|
|
onRedo: null,
|
|
onClear: null,
|
|
getState: null // Function to get current state
|
|
};
|
|
|
|
// ============================================================================
|
|
// Toolbar Creation
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Creates the toolbar UI and appends it to the container.
|
|
* @param {HTMLElement} container - Parent element for the toolbar
|
|
* @param {Object} options - Callback functions and initial state
|
|
*/
|
|
export function createToolbar(container, options = {}) {
|
|
if (toolbarElement) {
|
|
console.warn(LOG_PREFIX, 'Toolbar already exists');
|
|
return toolbarElement;
|
|
}
|
|
|
|
// Store callbacks
|
|
callbacks = { ...callbacks, ...options };
|
|
|
|
// Create main toolbar container
|
|
toolbarElement = document.createElement('div');
|
|
toolbarElement.id = TOOLBAR_ID;
|
|
toolbarElement.className = 'annotation-toolbar';
|
|
|
|
// Build toolbar HTML
|
|
toolbarElement.innerHTML = buildToolbarHTML();
|
|
|
|
// Apply styles
|
|
applyToolbarStyles();
|
|
|
|
// Append to container
|
|
container.appendChild(toolbarElement);
|
|
|
|
// Setup event listeners
|
|
setupEventListeners();
|
|
|
|
// Initial state update
|
|
updateToolbarState();
|
|
|
|
console.log(LOG_PREFIX, 'Toolbar created');
|
|
|
|
return toolbarElement;
|
|
}
|
|
|
|
/**
|
|
* Builds the toolbar HTML structure.
|
|
* @returns {string} HTML string
|
|
*/
|
|
function buildToolbarHTML() {
|
|
return `
|
|
<div class="toolbar-header">
|
|
<span class="toolbar-title">Annotations</span>
|
|
<button class="toolbar-toggle" title="Toggle toolbar (T)">
|
|
<span class="toggle-icon">▶</span>
|
|
</button>
|
|
<button class="mode-toggle" title="Toggle annotation mode (A)">
|
|
<span class="mode-indicator">OFF</span>
|
|
</button>
|
|
</div>
|
|
<div class="toolbar-content">
|
|
<div class="toolbar-section">
|
|
<div class="section-label">Tools</div>
|
|
<div class="tool-buttons">
|
|
<button class="tool-btn" data-tool="pen" title="Pen tool (P)">
|
|
<svg viewBox="0 0 24 24" width="18" height="18">
|
|
<path fill="currentColor" d="M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z"/>
|
|
</svg>
|
|
</button>
|
|
<button class="tool-btn" data-tool="eraser" title="Eraser tool (E)">
|
|
<svg viewBox="0 0 24 24" width="18" height="18">
|
|
<path fill="currentColor" d="M16.24,3.56L21.19,8.5C21.97,9.29 21.97,10.55 21.19,11.34L12,20.53C10.44,22.09 7.91,22.09 6.34,20.53L2.81,17C2.03,16.21 2.03,14.95 2.81,14.16L13.41,3.56C14.2,2.78 15.46,2.78 16.24,3.56M4.22,15.58L7.76,19.11C8.54,19.9 9.8,19.9 10.59,19.11L14.12,15.58L9.17,10.63L4.22,15.58Z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toolbar-section pen-settings">
|
|
<div class="section-label">Pen</div>
|
|
<div class="setting-row">
|
|
<label>Size</label>
|
|
<input type="range" class="pen-size-slider" min="1" max="50" value="4">
|
|
<span class="pen-size-value">4</span>
|
|
</div>
|
|
<div class="setting-row">
|
|
<label>Opacity</label>
|
|
<input type="range" class="pen-opacity-slider" min="10" max="100" value="100">
|
|
<span class="pen-opacity-value">100%</span>
|
|
</div>
|
|
<div class="setting-row color-row">
|
|
<label>Color</label>
|
|
<div class="color-picker-container">
|
|
<input type="color" class="color-input" value="#FF5555">
|
|
<div class="color-presets">
|
|
${PRESET_COLORS.map(color =>
|
|
`<button class="color-preset" data-color="${color}" style="background-color: ${color}" title="${color}"></button>`
|
|
).join('')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toolbar-section eraser-settings" style="display: none;">
|
|
<div class="section-label">Eraser</div>
|
|
<div class="setting-row">
|
|
<label>Size</label>
|
|
<input type="range" class="eraser-size-slider" min="5" max="100" value="20">
|
|
<span class="eraser-size-value">20</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toolbar-section">
|
|
<div class="section-label">Actions</div>
|
|
<div class="action-buttons">
|
|
<button class="action-btn undo-btn" title="Undo (Ctrl+Z)">
|
|
<svg viewBox="0 0 24 24" width="16" height="16">
|
|
<path fill="currentColor" d="M12.5,8C9.85,8 7.45,9 5.6,10.6L2,7V16H11L7.38,12.38C8.77,11.22 10.54,10.5 12.5,10.5C16.04,10.5 19.05,12.81 20.1,16L22.47,15.22C21.08,11.03 17.15,8 12.5,8Z"/>
|
|
</svg>
|
|
<span>Undo</span>
|
|
</button>
|
|
<button class="action-btn redo-btn" title="Redo (Ctrl+Shift+Z)">
|
|
<svg viewBox="0 0 24 24" width="16" height="16">
|
|
<path fill="currentColor" d="M18.4,10.6C16.55,9 14.15,8 11.5,8C6.85,8 2.92,11.03 1.54,15.22L3.9,16C4.95,12.81 7.95,10.5 11.5,10.5C13.45,10.5 15.23,11.22 16.62,12.38L13,16H22V7L18.4,10.6Z"/>
|
|
</svg>
|
|
<span>Redo</span>
|
|
</button>
|
|
<button class="action-btn clear-btn" title="Clear all strokes">
|
|
<svg viewBox="0 0 24 24" width="16" height="16">
|
|
<path fill="currentColor" d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"/>
|
|
</svg>
|
|
<span>Clear</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Applies CSS styles for the toolbar.
|
|
*/
|
|
function applyToolbarStyles() {
|
|
const styleId = 'annotation-toolbar-styles';
|
|
if (document.getElementById(styleId)) return;
|
|
|
|
const style = document.createElement('style');
|
|
style.id = styleId;
|
|
style.textContent = `
|
|
.annotation-toolbar {
|
|
position: absolute;
|
|
top: 100px;
|
|
right: 10px;
|
|
width: 200px;
|
|
background: rgba(30, 30, 30, 0.95);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 8px;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
font-size: 12px;
|
|
color: #fff;
|
|
z-index: 1000;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
user-select: none;
|
|
transition: width 0.2s ease;
|
|
}
|
|
|
|
.annotation-toolbar.collapsed {
|
|
width: auto;
|
|
}
|
|
|
|
.annotation-toolbar.collapsed .toolbar-content {
|
|
display: none;
|
|
}
|
|
|
|
.annotation-toolbar.collapsed .toolbar-title {
|
|
display: none;
|
|
}
|
|
|
|
.annotation-toolbar.collapsed .toggle-icon {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
.toolbar-header {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 8px;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
gap: 8px;
|
|
}
|
|
|
|
.annotation-toolbar.collapsed .toolbar-header {
|
|
border-bottom: none;
|
|
padding: 6px;
|
|
}
|
|
|
|
.toolbar-toggle {
|
|
background: none;
|
|
border: none;
|
|
color: #888;
|
|
cursor: pointer;
|
|
padding: 4px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 4px;
|
|
transition: background 0.15s;
|
|
}
|
|
|
|
.toolbar-toggle:hover {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
color: #fff;
|
|
}
|
|
|
|
.toggle-icon {
|
|
font-size: 10px;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.toolbar-title {
|
|
flex: 1;
|
|
font-weight: 600;
|
|
color: #ccc;
|
|
}
|
|
|
|
.mode-toggle {
|
|
background: rgba(255, 85, 85, 0.2);
|
|
border: 1px solid rgba(255, 85, 85, 0.5);
|
|
color: #FF5555;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.mode-toggle:hover {
|
|
background: rgba(255, 85, 85, 0.3);
|
|
}
|
|
|
|
.mode-toggle.active {
|
|
background: rgba(85, 255, 85, 0.2);
|
|
border-color: rgba(85, 255, 85, 0.5);
|
|
color: #55FF55;
|
|
}
|
|
|
|
.toolbar-content {
|
|
padding: 8px;
|
|
}
|
|
|
|
.toolbar-section {
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.toolbar-section:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.section-label {
|
|
font-size: 10px;
|
|
color: #888;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.tool-buttons {
|
|
display: flex;
|
|
gap: 4px;
|
|
}
|
|
|
|
.tool-btn {
|
|
flex: 1;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
color: #888;
|
|
padding: 8px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.tool-btn:hover {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
color: #fff;
|
|
}
|
|
|
|
.tool-btn.active {
|
|
background: rgba(255, 85, 85, 0.2);
|
|
border-color: rgba(255, 85, 85, 0.5);
|
|
color: #FF5555;
|
|
}
|
|
|
|
.tool-btn.active[data-tool="eraser"] {
|
|
background: rgba(85, 170, 255, 0.2);
|
|
border-color: rgba(85, 170, 255, 0.5);
|
|
color: #55AAFF;
|
|
}
|
|
|
|
.setting-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.setting-row:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.setting-row label {
|
|
width: 50px;
|
|
color: #888;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.setting-row input[type="range"] {
|
|
flex: 1;
|
|
height: 4px;
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 2px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.setting-row input[type="range"]::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 12px;
|
|
height: 12px;
|
|
background: #fff;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.setting-row input[type="range"]::-moz-range-thumb {
|
|
width: 12px;
|
|
height: 12px;
|
|
background: #fff;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
border: none;
|
|
}
|
|
|
|
.setting-row span {
|
|
width: 35px;
|
|
text-align: right;
|
|
color: #888;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.color-row {
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.color-picker-container {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
.color-input {
|
|
width: 100%;
|
|
height: 24px;
|
|
padding: 0;
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
background: transparent;
|
|
}
|
|
|
|
.color-input::-webkit-color-swatch-wrapper {
|
|
padding: 2px;
|
|
}
|
|
|
|
.color-input::-webkit-color-swatch {
|
|
border-radius: 2px;
|
|
border: none;
|
|
}
|
|
|
|
.color-presets {
|
|
display: flex;
|
|
gap: 4px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.color-preset {
|
|
width: 18px;
|
|
height: 18px;
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
padding: 0;
|
|
transition: transform 0.1s, border-color 0.1s;
|
|
}
|
|
|
|
.color-preset:hover {
|
|
transform: scale(1.1);
|
|
border-color: rgba(255, 255, 255, 0.5);
|
|
}
|
|
|
|
.color-preset.active {
|
|
border-color: #fff;
|
|
border-width: 2px;
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.action-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
color: #888;
|
|
padding: 6px 8px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.action-btn:hover:not(:disabled) {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
color: #fff;
|
|
}
|
|
|
|
.action-btn:disabled {
|
|
opacity: 0.4;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.action-btn.clear-btn:hover:not(:disabled) {
|
|
background: rgba(255, 85, 85, 0.2);
|
|
border-color: rgba(255, 85, 85, 0.5);
|
|
color: #FF5555;
|
|
}
|
|
|
|
/* Mode indicator in collapsed state */
|
|
.annotation-toolbar.collapsed .mode-toggle {
|
|
padding: 6px 10px;
|
|
}
|
|
`;
|
|
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Event Listeners
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Sets up all event listeners for the toolbar.
|
|
*/
|
|
function setupEventListeners() {
|
|
if (!toolbarElement) return;
|
|
|
|
// Toggle collapse
|
|
const toggleBtn = toolbarElement.querySelector('.toolbar-toggle');
|
|
toggleBtn?.addEventListener('click', toggleCollapse);
|
|
|
|
// Mode toggle
|
|
const modeToggle = toolbarElement.querySelector('.mode-toggle');
|
|
modeToggle?.addEventListener('click', () => {
|
|
callbacks.onToggleMode?.();
|
|
});
|
|
|
|
// Tool buttons
|
|
const toolButtons = toolbarElement.querySelectorAll('.tool-btn');
|
|
toolButtons.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const tool = btn.dataset.tool;
|
|
callbacks.onSetTool?.(tool);
|
|
});
|
|
});
|
|
|
|
// Pen size slider
|
|
const penSizeSlider = toolbarElement.querySelector('.pen-size-slider');
|
|
penSizeSlider?.addEventListener('input', (e) => {
|
|
const value = parseInt(e.target.value);
|
|
callbacks.onSetSize?.(value);
|
|
updateSliderDisplay('.pen-size-value', value);
|
|
});
|
|
|
|
// Pen opacity slider
|
|
const penOpacitySlider = toolbarElement.querySelector('.pen-opacity-slider');
|
|
penOpacitySlider?.addEventListener('input', (e) => {
|
|
const value = parseInt(e.target.value) / 100;
|
|
callbacks.onSetOpacity?.(value);
|
|
updateSliderDisplay('.pen-opacity-value', `${e.target.value}%`);
|
|
});
|
|
|
|
// Eraser size slider
|
|
const eraserSizeSlider = toolbarElement.querySelector('.eraser-size-slider');
|
|
eraserSizeSlider?.addEventListener('input', (e) => {
|
|
const value = parseInt(e.target.value);
|
|
callbacks.onSetEraserSize?.(value);
|
|
updateSliderDisplay('.eraser-size-value', value);
|
|
});
|
|
|
|
// Color picker
|
|
const colorInput = toolbarElement.querySelector('.color-input');
|
|
colorInput?.addEventListener('input', (e) => {
|
|
callbacks.onSetColor?.(e.target.value);
|
|
updateColorPresetSelection(e.target.value);
|
|
});
|
|
|
|
// Color presets
|
|
const colorPresets = toolbarElement.querySelectorAll('.color-preset');
|
|
colorPresets.forEach(preset => {
|
|
preset.addEventListener('click', () => {
|
|
const color = preset.dataset.color;
|
|
callbacks.onSetColor?.(color);
|
|
if (colorInput) colorInput.value = color;
|
|
updateColorPresetSelection(color);
|
|
});
|
|
});
|
|
|
|
// Action buttons
|
|
const undoBtn = toolbarElement.querySelector('.undo-btn');
|
|
undoBtn?.addEventListener('click', () => callbacks.onUndo?.());
|
|
|
|
const redoBtn = toolbarElement.querySelector('.redo-btn');
|
|
redoBtn?.addEventListener('click', () => callbacks.onRedo?.());
|
|
|
|
const clearBtn = toolbarElement.querySelector('.clear-btn');
|
|
clearBtn?.addEventListener('click', () => callbacks.onClear?.());
|
|
}
|
|
|
|
/**
|
|
* Updates a slider display value.
|
|
*/
|
|
function updateSliderDisplay(selector, value) {
|
|
const display = toolbarElement?.querySelector(selector);
|
|
if (display) display.textContent = value;
|
|
}
|
|
|
|
/**
|
|
* Updates color preset selection.
|
|
*/
|
|
function updateColorPresetSelection(color) {
|
|
const presets = toolbarElement?.querySelectorAll('.color-preset');
|
|
presets?.forEach(preset => {
|
|
preset.classList.toggle('active', preset.dataset.color.toLowerCase() === color.toLowerCase());
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// State Updates
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Updates the toolbar UI to reflect current state.
|
|
* Call this after any state change.
|
|
*/
|
|
export function updateToolbarState() {
|
|
if (!toolbarElement || !callbacks.getState) return;
|
|
|
|
const state = callbacks.getState();
|
|
|
|
// Update mode toggle
|
|
const modeToggle = toolbarElement.querySelector('.mode-toggle');
|
|
const modeIndicator = modeToggle?.querySelector('.mode-indicator');
|
|
if (modeToggle && modeIndicator) {
|
|
modeToggle.classList.toggle('active', state.isActive);
|
|
modeIndicator.textContent = state.isActive ? 'ON' : 'OFF';
|
|
}
|
|
|
|
// Update tool buttons
|
|
const toolButtons = toolbarElement.querySelectorAll('.tool-btn');
|
|
toolButtons.forEach(btn => {
|
|
btn.classList.toggle('active', btn.dataset.tool === state.currentTool);
|
|
});
|
|
|
|
// Show/hide tool-specific settings
|
|
const penSettings = toolbarElement.querySelector('.pen-settings');
|
|
const eraserSettings = toolbarElement.querySelector('.eraser-settings');
|
|
if (penSettings && eraserSettings) {
|
|
penSettings.style.display = state.currentTool === 'pen' ? 'block' : 'none';
|
|
eraserSettings.style.display = state.currentTool === 'eraser' ? 'block' : 'none';
|
|
}
|
|
|
|
// Update pen settings
|
|
const penSizeSlider = toolbarElement.querySelector('.pen-size-slider');
|
|
if (penSizeSlider && state.penSettings) {
|
|
penSizeSlider.value = state.penSettings.size;
|
|
updateSliderDisplay('.pen-size-value', state.penSettings.size);
|
|
}
|
|
|
|
const penOpacitySlider = toolbarElement.querySelector('.pen-opacity-slider');
|
|
if (penOpacitySlider && state.penSettings) {
|
|
penOpacitySlider.value = state.penSettings.opacity * 100;
|
|
updateSliderDisplay('.pen-opacity-value', `${Math.round(state.penSettings.opacity * 100)}%`);
|
|
}
|
|
|
|
const colorInput = toolbarElement.querySelector('.color-input');
|
|
if (colorInput && state.penSettings) {
|
|
colorInput.value = state.penSettings.color;
|
|
updateColorPresetSelection(state.penSettings.color);
|
|
}
|
|
|
|
// Update eraser settings
|
|
const eraserSizeSlider = toolbarElement.querySelector('.eraser-size-slider');
|
|
if (eraserSizeSlider && state.eraserSettings) {
|
|
eraserSizeSlider.value = state.eraserSettings.size;
|
|
updateSliderDisplay('.eraser-size-value', state.eraserSettings.size);
|
|
}
|
|
|
|
// Update action buttons
|
|
const undoBtn = toolbarElement.querySelector('.undo-btn');
|
|
const redoBtn = toolbarElement.querySelector('.redo-btn');
|
|
const clearBtn = toolbarElement.querySelector('.clear-btn');
|
|
|
|
if (undoBtn) undoBtn.disabled = !state.canUndo;
|
|
if (redoBtn) redoBtn.disabled = !state.canRedo;
|
|
if (clearBtn) clearBtn.disabled = state.strokeCount === 0;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Collapse/Expand
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Toggles the collapsed state of the toolbar.
|
|
*/
|
|
export function toggleCollapse() {
|
|
if (!toolbarElement) return;
|
|
|
|
isCollapsed = !isCollapsed;
|
|
toolbarElement.classList.toggle('collapsed', isCollapsed);
|
|
console.log(LOG_PREFIX, isCollapsed ? 'Collapsed' : 'Expanded');
|
|
}
|
|
|
|
/**
|
|
* Sets the collapsed state.
|
|
* @param {boolean} collapsed
|
|
*/
|
|
export function setCollapsed(collapsed) {
|
|
if (!toolbarElement) return;
|
|
|
|
isCollapsed = collapsed;
|
|
toolbarElement.classList.toggle('collapsed', isCollapsed);
|
|
}
|
|
|
|
/**
|
|
* Returns whether the toolbar is collapsed.
|
|
* @returns {boolean}
|
|
*/
|
|
export function isToolbarCollapsed() {
|
|
return isCollapsed;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Visibility
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Shows the toolbar.
|
|
*/
|
|
export function showToolbar() {
|
|
if (toolbarElement) {
|
|
toolbarElement.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hides the toolbar.
|
|
*/
|
|
export function hideToolbar() {
|
|
if (toolbarElement) {
|
|
toolbarElement.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns whether the toolbar is visible.
|
|
* @returns {boolean}
|
|
*/
|
|
export function isToolbarVisible() {
|
|
return toolbarElement?.style.display !== 'none';
|
|
}
|
|
|
|
// ============================================================================
|
|
// Cleanup
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Destroys the toolbar and cleans up resources.
|
|
*/
|
|
export function destroyToolbar() {
|
|
if (toolbarElement && toolbarElement.parentElement) {
|
|
toolbarElement.parentElement.removeChild(toolbarElement);
|
|
}
|
|
|
|
toolbarElement = null;
|
|
isCollapsed = false;
|
|
callbacks = {
|
|
onToggleMode: null,
|
|
onSetTool: null,
|
|
onSetColor: null,
|
|
onSetSize: null,
|
|
onSetOpacity: null,
|
|
onSetEraserSize: null,
|
|
onUndo: null,
|
|
onRedo: null,
|
|
onClear: null,
|
|
getState: null
|
|
};
|
|
|
|
console.log(LOG_PREFIX, 'Toolbar destroyed');
|
|
}
|
|
|
|
/**
|
|
* Returns the toolbar element.
|
|
* @returns {HTMLElement | null}
|
|
*/
|
|
export function getToolbarElement() {
|
|
return toolbarElement;
|
|
}
|