Skip to content

Commit 019dd32

Browse files
nisargkolheclaude
andcommitted
refactor: simplify sidebar.js with unified helpers and constants
- Create constants.js with CSS_CLASSES, SELECTORS, TIMING, MOUSE_BUTTON - Add unified getDragAfterElement() to domManager.js replacing 3 duplicates - Extract container query helpers to domManager.js (getSpaceElement, getContainers, etc.) - Move drop indicator functions to domManager.js - Add clearAllActiveStates() helper for active state management - Remove duplicate functions from sidebar.js (~96 lines reduced) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 6cdb2cd commit 019dd32

File tree

3 files changed

+408
-186
lines changed

3 files changed

+408
-186
lines changed

constants.js

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/**
2+
* Constants - Centralized CSS classes, selectors, and timing values
3+
*
4+
* Purpose: Single source of truth for hardcoded values used across the extension
5+
* Key Benefits: Eliminates magic strings, improves maintainability, enables easy refactoring
6+
*/
7+
8+
// CSS class names used throughout the application
9+
export const CSS_CLASSES = {
10+
// State classes
11+
ACTIVE: 'active',
12+
COLLAPSED: 'collapsed',
13+
HIDDEN: 'hidden',
14+
VISIBLE: 'visible',
15+
INACTIVE: 'inactive',
16+
DISABLED: 'disabled',
17+
18+
// Drag and drop classes
19+
DRAGGING: 'dragging',
20+
DRAGGING_SWITCHER: 'dragging-switcher',
21+
DRAG_OVER: 'drag-over',
22+
DRAG_OVER_PLACEHOLDER_BEFORE: 'drag-over-placeholder-before',
23+
DRAG_OVER_PLACEHOLDER_AFTER: 'drag-over-placeholder-after',
24+
25+
// Drop indicator classes
26+
DROP_INDICATOR_HORIZONTAL: 'drop-indicator-horizontal',
27+
DROP_INDICATOR_VERTICAL: 'drop-indicator-vertical',
28+
29+
// Position classes for drop indicators
30+
ABOVE: 'above',
31+
BELOW: 'below',
32+
LEFT: 'left',
33+
RIGHT: 'right',
34+
35+
// Tab classes
36+
TAB: 'tab',
37+
BOOKMARK_ONLY: 'bookmark-only',
38+
TAB_CLOSE: 'tab-close',
39+
TAB_REMOVE: 'tab-remove',
40+
TAB_FAVICON: 'tab-favicon',
41+
TAB_PLACEHOLDER: 'tab-placeholder',
42+
43+
// Pinned/Favorites classes
44+
PINNED_FAVICON: 'pinned-favicon',
45+
PINNED_PLACEHOLDER_CONTAINER: 'pinned-placeholder-container',
46+
PINNED_BACK: 'pinned-back',
47+
48+
// Folder classes
49+
FOLDER: 'folder',
50+
FOLDER_HEADER: 'folder-header',
51+
FOLDER_CONTENT: 'folder-content',
52+
FOLDER_NAME: 'folder-name',
53+
FOLDER_TITLE: 'folder-title',
54+
FOLDER_ICON: 'folder-icon',
55+
FOLDER_TOGGLE: 'folder-toggle',
56+
57+
// Space classes
58+
SPACE: 'space',
59+
60+
// UI feedback classes
61+
ERROR_POPUP: 'error-popup',
62+
SPOTLIGHT_ACTIVE: 'spotlight-active',
63+
URL_CHANGED_SLASH: 'tab-url-changed-slash',
64+
65+
// Context menu classes
66+
CONTEXT_MENU: 'context-menu',
67+
CONTEXT_MENU_ITEM: 'context-menu-item',
68+
CONTEXT_MENU_SEPARATOR: 'context-menu-separator',
69+
WITH_SUBMENU: 'with-submenu',
70+
SUBMENU: 'submenu',
71+
};
72+
73+
// DOM selectors for querying elements
74+
export const SELECTORS = {
75+
// Tab selectors
76+
TAB_BY_ID: (tabId) => `[data-tab-id="${tabId}"]`,
77+
TAB_BY_URL: (url) => `[data-url="${CSS.escape(url)}"]`,
78+
ALL_TABS: '.tab',
79+
ACTIVE_TABS: '.tab.active',
80+
BOOKMARK_ONLY_TABS: '.tab.bookmark-only',
81+
DRAGGABLE_TABS: '.tab:not(.dragging), .folder:not(.dragging)',
82+
83+
// Space selectors
84+
SPACE_BY_ID: (spaceId) => `[data-space-id="${spaceId}"]`,
85+
ALL_SPACES: '.space',
86+
87+
// Container selectors
88+
PINNED_CONTAINER: '[data-tab-type="pinned"]',
89+
TEMP_CONTAINER: '[data-tab-type="temporary"]',
90+
91+
// Pinned favicon selectors
92+
ALL_PINNED_FAVICONS: '.pinned-favicon',
93+
DRAGGABLE_PINNED_FAVICONS: '.pinned-favicon:not(.dragging)',
94+
ACTIVE_PINNED_FAVICONS: '.pinned-favicon.active',
95+
96+
// Switcher selectors
97+
DRAGGABLE_SWITCHER_BUTTONS: 'button:not(.dragging-switcher)',
98+
99+
// Folder selectors
100+
ALL_FOLDERS: '.folder',
101+
FOLDER_CONTENT: '.folder-content',
102+
103+
// Template selectors
104+
TAB_TEMPLATE: '#tabTemplate',
105+
FOLDER_TEMPLATE: '#folderTemplate',
106+
SPACE_TEMPLATE: '#spaceTemplate',
107+
108+
// UI element selectors
109+
SIDEBAR_CONTAINER: '#sidebar-container',
110+
SPACES_LIST: '#spacesList',
111+
SPACE_SWITCHER: '#spaceSwitcher',
112+
PINNED_FAVICONS: '#pinnedFavicons',
113+
ADD_SPACE_BTN: '#addSpaceBtn',
114+
NEW_TAB_BTN: '#newTabBtn',
115+
ADD_SPACE_INPUT_CONTAINER: '#addSpaceInputContainer',
116+
NEW_SPACE_NAME: '#newSpaceName',
117+
SPACE_COLOR: '#spaceColor',
118+
CREATE_SPACE_BTN: '#createSpaceBtn',
119+
CREATE_SPACE_COLOR_SWATCH: '#createSpaceColorSwatch',
120+
URL_COPY_TOAST: '#urlCopyToast',
121+
TAB_CONTEXT_MENU: '#tab-context-menu',
122+
123+
// Placeholder selectors
124+
TAB_PLACEHOLDER: '.tab-placeholder',
125+
PINNED_PLACEHOLDER: '.pinned-placeholder-container',
126+
127+
// Drop indicator selectors
128+
DROP_INDICATORS: '.drop-indicator-horizontal, .drop-indicator-vertical',
129+
};
130+
131+
// Timing constants (in milliseconds unless noted)
132+
export const TIMING = {
133+
// Auto-open delays
134+
FOLDER_AUTO_OPEN_DELAY: 250,
135+
136+
// UI feedback durations
137+
ERROR_POPUP_DURATION: 3000,
138+
TOAST_DURATION: 2000,
139+
140+
// Debounce intervals
141+
DEBOUNCE_DEFAULT: 250,
142+
DEBOUNCE_UI_REFRESH: 100,
143+
144+
// Drag operation timing
145+
DRAG_CLASS_DELAY: 0, // setTimeout(fn, 0) for class addition
146+
};
147+
148+
// Mouse button constants
149+
export const MOUSE_BUTTON = {
150+
LEFT: 0,
151+
MIDDLE: 1,
152+
RIGHT: 2,
153+
};
154+
155+
// Tab type constants
156+
export const TAB_TYPE = {
157+
PINNED: 'pinned',
158+
TEMPORARY: 'temporary',
159+
};
160+
161+
// Drop position constants
162+
export const DROP_POSITION = {
163+
ABOVE: 'above',
164+
BELOW: 'below',
165+
LEFT: 'left',
166+
RIGHT: 'right',
167+
};
168+
169+
// Axis constants for drag operations
170+
export const AXIS = {
171+
X: 'x',
172+
Y: 'y',
173+
};

domManager.js

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,3 +586,191 @@ export function setupQuickPinListener(moveTabToSpace, moveTabToPinned, moveTabTo
586586
}
587587
});
588588
}
589+
590+
// ============================================================================
591+
// Unified Drag Helper Functions
592+
// ============================================================================
593+
594+
/**
595+
* Unified function to find the element to insert after during drag-and-drop.
596+
* Replaces getDragAfterElementSwitcher, getDragAfterElement, and getDragAfterElementFavicon.
597+
*
598+
* @param {HTMLElement} container - The container element
599+
* @param {number} position - The mouse position (clientX for horizontal, clientY for vertical)
600+
* @param {Object} options - Configuration options
601+
* @param {string} options.axis - 'x' for horizontal, 'y' for vertical (default: 'y')
602+
* @param {string} options.selector - CSS selector for draggable elements
603+
* @param {string} options.placeholderSelector - CSS selector for placeholder element
604+
* @returns {HTMLElement|null} - The element to insert after, or null
605+
*/
606+
export function getDragAfterElement(container, position, options = {}) {
607+
const { axis = 'y', selector, placeholderSelector } = options;
608+
const draggableElements = [...container.querySelectorAll(selector)];
609+
610+
// If no draggable elements exist, return the placeholder as a reference for empty containers
611+
if (draggableElements.length === 0) {
612+
return placeholderSelector ? container.querySelector(placeholderSelector) : null;
613+
}
614+
615+
return draggableElements.reduce((closest, child) => {
616+
const box = child.getBoundingClientRect();
617+
const offset = axis === 'x'
618+
? position - box.left - box.width / 2
619+
: position - box.top - box.height / 2;
620+
621+
if (offset < 0 && offset > closest.offset) {
622+
return { offset, element: child };
623+
}
624+
return closest;
625+
}, { offset: Number.NEGATIVE_INFINITY }).element;
626+
}
627+
628+
// ============================================================================
629+
// Container Query Helpers
630+
// ============================================================================
631+
632+
/**
633+
* Get a space element by its ID
634+
* @param {number|string} spaceId - The space ID
635+
* @returns {HTMLElement|null}
636+
*/
637+
export function getSpaceElement(spaceId) {
638+
return document.querySelector(`[data-space-id="${spaceId}"]`);
639+
}
640+
641+
/**
642+
* Get both pinned and temporary containers for a space element
643+
* @param {HTMLElement} spaceElement - The space element
644+
* @returns {{pinned: HTMLElement|null, temp: HTMLElement|null}}
645+
*/
646+
export function getContainers(spaceElement) {
647+
return {
648+
pinned: spaceElement?.querySelector('[data-tab-type="pinned"]') ?? null,
649+
temp: spaceElement?.querySelector('[data-tab-type="temporary"]') ?? null
650+
};
651+
}
652+
653+
/**
654+
* Get a tab element by its ID
655+
* @param {number|string} tabId - The tab ID
656+
* @returns {HTMLElement|null}
657+
*/
658+
export function getTabElement(tabId) {
659+
return document.querySelector(`[data-tab-id="${tabId}"]`);
660+
}
661+
662+
/**
663+
* Get pinned container for a space element
664+
* @param {HTMLElement} spaceElement - The space element
665+
* @returns {HTMLElement|null}
666+
*/
667+
export function getPinnedContainer(spaceElement) {
668+
return spaceElement?.querySelector('[data-tab-type="pinned"]') ?? null;
669+
}
670+
671+
/**
672+
* Get temporary container for a space element
673+
* @param {HTMLElement} spaceElement - The space element
674+
* @returns {HTMLElement|null}
675+
*/
676+
export function getTempContainer(spaceElement) {
677+
return spaceElement?.querySelector('[data-tab-type="temporary"]') ?? null;
678+
}
679+
680+
// ============================================================================
681+
// Active State Management
682+
// ============================================================================
683+
684+
/**
685+
* Clear active state from all tabs and pinned favicons
686+
*/
687+
export function clearAllActiveStates() {
688+
document.querySelectorAll('.tab, .pinned-favicon')
689+
.forEach(el => el.classList.remove('active'));
690+
}
691+
692+
// ============================================================================
693+
// Drop Indicator Functions
694+
// ============================================================================
695+
696+
/**
697+
* Hide all drop indicators in the document
698+
*/
699+
export function hideAllDropIndicators() {
700+
document.querySelectorAll('.drop-indicator-horizontal, .drop-indicator-vertical').forEach(element => {
701+
element.classList.remove('drop-indicator-horizontal', 'drop-indicator-vertical', 'above', 'below', 'left', 'right');
702+
});
703+
}
704+
705+
/**
706+
* Show a drop indicator on the target element
707+
* @param {HTMLElement} targetElement - The element to show the indicator on
708+
* @param {string} position - Position: 'above', 'below', 'left', or 'right'
709+
* @param {boolean} isHorizontal - True for horizontal layout (favicons), false for vertical (tabs)
710+
*/
711+
export function showDropIndicator(targetElement, position, isHorizontal = false) {
712+
// First, hide all existing indicators
713+
hideAllDropIndicators();
714+
715+
if (!targetElement) return;
716+
717+
if (isHorizontal) {
718+
// For horizontal favicons (left/right positioning)
719+
targetElement.classList.add('drop-indicator-vertical');
720+
targetElement.classList.add(position); // 'left' or 'right'
721+
} else {
722+
// For vertical sidebar tabs (above/below positioning)
723+
targetElement.classList.add('drop-indicator-horizontal');
724+
targetElement.classList.add(position); // 'above' or 'below'
725+
}
726+
}
727+
728+
/**
729+
* Get the drop position relative to an element
730+
* @param {HTMLElement} element - The target element
731+
* @param {number} clientX - Mouse X position
732+
* @param {number} clientY - Mouse Y position
733+
* @param {boolean} isHorizontal - True for horizontal layout, false for vertical
734+
* @returns {string|null} - Position: 'above', 'below', 'left', 'right', or null
735+
*/
736+
export function getDropPosition(element, clientX, clientY, isHorizontal = false) {
737+
if (!element) return null;
738+
739+
const rect = element.getBoundingClientRect();
740+
741+
if (isHorizontal) {
742+
// For horizontal favicons, use X position to determine left/right
743+
const centerX = rect.left + rect.width / 2;
744+
return clientX < centerX ? 'left' : 'right';
745+
} else {
746+
// For vertical tabs, use Y position to determine above/below
747+
const centerY = rect.top + rect.height / 2;
748+
return clientY < centerY ? 'above' : 'below';
749+
}
750+
}
751+
752+
/**
753+
* Handle empty container drops consistently
754+
* @param {HTMLElement} container - The container element
755+
* @param {HTMLElement} draggingElement - The element being dragged
756+
* @param {HTMLElement} placeholder - The placeholder element
757+
* @returns {boolean} - True if handled successfully
758+
*/
759+
export function handleEmptyContainerDrop(container, draggingElement, placeholder) {
760+
if (!container || !draggingElement || !placeholder) return false;
761+
762+
// Append element to container
763+
container.appendChild(draggingElement);
764+
765+
// Hide placeholder appropriately based on type
766+
if (placeholder.classList.contains('pinned-placeholder-container')) {
767+
// For favorites area - use display none
768+
placeholder.style.display = 'none';
769+
} else if (placeholder.classList.contains('tab-placeholder')) {
770+
// For space containers - use hidden class
771+
placeholder.classList.add('hidden');
772+
}
773+
774+
Logger.log('Handled empty container drop, hiding placeholder');
775+
return true;
776+
}

0 commit comments

Comments
 (0)