claude-mpm 4.2.12__py3-none-any.whl → 4.2.14__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/commands/mpm-agents.md +44 -8
- claude_mpm/core/constants.py +65 -0
- claude_mpm/core/error_handler.py +625 -0
- claude_mpm/core/file_utils.py +770 -0
- claude_mpm/core/logging_utils.py +502 -0
- claude_mpm/dashboard/static/dist/components/code-tree.js +1 -1
- claude_mpm/dashboard/static/dist/components/file-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/dashboard.js +1 -1
- claude_mpm/dashboard/static/dist/socket-client.js +1 -1
- claude_mpm/dashboard/static/js/components/code-simple.js +44 -1
- claude_mpm/dashboard/static/js/components/code-tree/tree-breadcrumb.js +353 -0
- claude_mpm/dashboard/static/js/components/code-tree/tree-constants.js +235 -0
- claude_mpm/dashboard/static/js/components/code-tree/tree-search.js +409 -0
- claude_mpm/dashboard/static/js/components/code-tree/tree-utils.js +435 -0
- claude_mpm/dashboard/static/js/components/code-tree.js +29 -5
- claude_mpm/dashboard/static/js/components/file-viewer.js +69 -27
- claude_mpm/dashboard/static/js/components/session-manager.js +1 -1
- claude_mpm/dashboard/static/js/components/working-directory.js +18 -9
- claude_mpm/dashboard/static/js/dashboard.js +55 -9
- claude_mpm/dashboard/static/js/shared/dom-helpers.js +396 -0
- claude_mpm/dashboard/static/js/shared/event-bus.js +330 -0
- claude_mpm/dashboard/static/js/shared/logger.js +385 -0
- claude_mpm/dashboard/static/js/shared/tooltip-service.js +253 -0
- claude_mpm/dashboard/static/js/socket-client.js +18 -0
- claude_mpm/dashboard/templates/index.html +21 -8
- claude_mpm/services/monitor/handlers/__init__.py +2 -1
- claude_mpm/services/monitor/handlers/file.py +263 -0
- claude_mpm/services/monitor/server.py +81 -1
- claude_mpm/services/socketio/handlers/file.py +40 -5
- {claude_mpm-4.2.12.dist-info → claude_mpm-4.2.14.dist-info}/METADATA +1 -1
- {claude_mpm-4.2.12.dist-info → claude_mpm-4.2.14.dist-info}/RECORD +36 -24
- {claude_mpm-4.2.12.dist-info → claude_mpm-4.2.14.dist-info}/WHEEL +0 -0
- {claude_mpm-4.2.12.dist-info → claude_mpm-4.2.14.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.2.12.dist-info → claude_mpm-4.2.14.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.2.12.dist-info → claude_mpm-4.2.14.dist-info}/top_level.txt +0 -0
|
@@ -1214,38 +1214,61 @@ async function updateFileViewerModal(modal, filePath, workingDir) {
|
|
|
1214
1214
|
|
|
1215
1215
|
try {
|
|
1216
1216
|
// Get the Socket.IO client
|
|
1217
|
-
const socket = window.socket || window.dashboard?.socketClient?.socket;
|
|
1217
|
+
const socket = window.socket || window.dashboard?.socketClient?.socket || window.socketClient?.socket;
|
|
1218
|
+
|
|
1219
|
+
console.log('[FileViewer] Socket search results:', {
|
|
1220
|
+
'window.socket': !!window.socket,
|
|
1221
|
+
'window.socket.connected': window.socket?.connected,
|
|
1222
|
+
'dashboard.socketClient.socket': !!window.dashboard?.socketClient?.socket,
|
|
1223
|
+
'dashboard.socketClient.socket.connected': window.dashboard?.socketClient?.socket?.connected,
|
|
1224
|
+
'window.socketClient.socket': !!window.socketClient?.socket,
|
|
1225
|
+
'window.socketClient.socket.connected': window.socketClient?.socket?.connected
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1218
1228
|
if (!socket) {
|
|
1219
|
-
throw new Error('No socket connection available');
|
|
1229
|
+
throw new Error('No socket connection available. Please ensure the dashboard is connected.');
|
|
1220
1230
|
}
|
|
1231
|
+
|
|
1232
|
+
if (!socket.connected) {
|
|
1233
|
+
console.warn('[FileViewer] Socket found but not connected, attempting to use anyway...');
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
console.log('[FileViewer] Socket found, setting up listener for file_content_response');
|
|
1221
1237
|
|
|
1222
1238
|
// Set up one-time listener for file content response
|
|
1223
1239
|
const responsePromise = new Promise((resolve, reject) => {
|
|
1224
1240
|
const responseHandler = (data) => {
|
|
1241
|
+
console.log('[FileViewer] Received file_content_response:', data);
|
|
1225
1242
|
if (data.file_path === filePath) {
|
|
1226
1243
|
socket.off('file_content_response', responseHandler);
|
|
1227
1244
|
if (data.success) {
|
|
1245
|
+
console.log('[FileViewer] File content loaded successfully');
|
|
1228
1246
|
resolve(data);
|
|
1229
1247
|
} else {
|
|
1248
|
+
console.error('[FileViewer] File read failed:', data.error);
|
|
1230
1249
|
reject(new Error(data.error || 'Failed to read file'));
|
|
1231
1250
|
}
|
|
1232
1251
|
}
|
|
1233
1252
|
};
|
|
1234
1253
|
|
|
1235
1254
|
socket.on('file_content_response', responseHandler);
|
|
1255
|
+
console.log('[FileViewer] Listener registered for file_content_response');
|
|
1236
1256
|
|
|
1237
1257
|
// Timeout after 10 seconds
|
|
1238
1258
|
setTimeout(() => {
|
|
1239
1259
|
socket.off('file_content_response', responseHandler);
|
|
1240
|
-
|
|
1260
|
+
console.error('[FileViewer] Request timeout after 10 seconds');
|
|
1261
|
+
reject(new Error('Request timeout - server did not respond'));
|
|
1241
1262
|
}, 10000);
|
|
1242
1263
|
});
|
|
1243
1264
|
|
|
1244
1265
|
// Send file read request
|
|
1245
|
-
|
|
1266
|
+
const requestData = {
|
|
1246
1267
|
file_path: filePath,
|
|
1247
1268
|
working_dir: workingDir
|
|
1248
|
-
}
|
|
1269
|
+
};
|
|
1270
|
+
console.log('[FileViewer] Emitting read_file event with data:', requestData);
|
|
1271
|
+
socket.emit('read_file', requestData);
|
|
1249
1272
|
|
|
1250
1273
|
// File viewer request sent
|
|
1251
1274
|
|
|
@@ -1505,15 +1528,19 @@ function formatFileSize(bytes) {
|
|
|
1505
1528
|
|
|
1506
1529
|
// File Viewer Modal Functions
|
|
1507
1530
|
window.showFileViewerModal = async function(filePath) {
|
|
1531
|
+
console.log('[FileViewer] Opening file:', filePath);
|
|
1532
|
+
|
|
1508
1533
|
// Use the dashboard's current working directory
|
|
1509
1534
|
let workingDir = '';
|
|
1510
1535
|
if (window.dashboard && window.dashboard.currentWorkingDir) {
|
|
1511
1536
|
workingDir = window.dashboard.currentWorkingDir;
|
|
1537
|
+
console.log('[FileViewer] Using working directory:', workingDir);
|
|
1512
1538
|
}
|
|
1513
1539
|
|
|
1514
1540
|
// Create modal if it doesn't exist
|
|
1515
1541
|
let modal = document.getElementById('file-viewer-modal');
|
|
1516
1542
|
if (!modal) {
|
|
1543
|
+
console.log('[FileViewer] Creating new modal');
|
|
1517
1544
|
modal = createFileViewerModal();
|
|
1518
1545
|
document.body.appendChild(modal);
|
|
1519
1546
|
|
|
@@ -1521,14 +1548,16 @@ window.showFileViewerModal = async function(filePath) {
|
|
|
1521
1548
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1522
1549
|
}
|
|
1523
1550
|
|
|
1551
|
+
// Show the modal as flex container first (ensures proper rendering)
|
|
1552
|
+
modal.style.display = 'flex';
|
|
1553
|
+
document.body.style.overflow = 'hidden'; // Prevent background scrolling
|
|
1554
|
+
|
|
1524
1555
|
// Update modal content
|
|
1525
1556
|
updateFileViewerModal(modal, filePath, workingDir).catch(error => {
|
|
1526
1557
|
console.error('Error updating file viewer modal:', error);
|
|
1558
|
+
// Show error in the modal
|
|
1559
|
+
displayFileContentError(modal, { error: error.message });
|
|
1527
1560
|
});
|
|
1528
|
-
|
|
1529
|
-
// Show the modal as flex container
|
|
1530
|
-
modal.style.display = 'flex';
|
|
1531
|
-
document.body.style.overflow = 'hidden'; // Prevent background scrolling
|
|
1532
1561
|
};
|
|
1533
1562
|
|
|
1534
1563
|
window.hideFileViewerModal = function() {
|
|
@@ -1589,6 +1618,19 @@ function displayFileContentError(modal, result) {
|
|
|
1589
1618
|
const errorArea = modal.querySelector('.file-viewer-error');
|
|
1590
1619
|
const messageElement = modal.querySelector('.error-message');
|
|
1591
1620
|
const suggestionsElement = modal.querySelector('.error-suggestions');
|
|
1621
|
+
const loadingElement = modal.querySelector('.file-viewer-loading');
|
|
1622
|
+
const contentArea = modal.querySelector('.file-viewer-content-area');
|
|
1623
|
+
|
|
1624
|
+
// Hide loading and content areas, show error
|
|
1625
|
+
if (loadingElement) {
|
|
1626
|
+
loadingElement.style.display = 'none';
|
|
1627
|
+
}
|
|
1628
|
+
if (contentArea) {
|
|
1629
|
+
contentArea.style.display = 'none';
|
|
1630
|
+
}
|
|
1631
|
+
if (errorArea) {
|
|
1632
|
+
errorArea.style.display = 'flex';
|
|
1633
|
+
}
|
|
1592
1634
|
|
|
1593
1635
|
// Create user-friendly error messages
|
|
1594
1636
|
let errorMessage = result.error || 'Unknown error occurred';
|
|
@@ -1599,6 +1641,10 @@ function displayFileContentError(modal, result) {
|
|
|
1599
1641
|
errorMessage = '🔒 Permission denied accessing this file';
|
|
1600
1642
|
} else if (errorMessage.includes('too large')) {
|
|
1601
1643
|
errorMessage = '📏 File is too large to display';
|
|
1644
|
+
} else if (errorMessage.includes('socket connection')) {
|
|
1645
|
+
errorMessage = '🔌 Not connected to the server. Please check your connection.';
|
|
1646
|
+
} else if (errorMessage.includes('timeout')) {
|
|
1647
|
+
errorMessage = '⏱️ Request timed out. The server may be busy or unresponsive.';
|
|
1602
1648
|
} else if (!errorMessage.includes('📁') && !errorMessage.includes('🔒') && !errorMessage.includes('📏')) {
|
|
1603
1649
|
errorMessage = `⚠️ ${errorMessage}`;
|
|
1604
1650
|
}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM Helper Utilities
|
|
3
|
+
*
|
|
4
|
+
* Common DOM manipulation utilities for dashboard components.
|
|
5
|
+
* Provides safe, consistent methods for element creation and manipulation.
|
|
6
|
+
*
|
|
7
|
+
* @module dom-helpers
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const domHelpers = {
|
|
11
|
+
/**
|
|
12
|
+
* Create an element with attributes and content
|
|
13
|
+
* @param {string} tag - Element tag name
|
|
14
|
+
* @param {Object} attrs - Attributes to set
|
|
15
|
+
* @param {string|Element|Array} content - Element content
|
|
16
|
+
* @returns {HTMLElement} Created element
|
|
17
|
+
*/
|
|
18
|
+
createElement(tag, attrs = {}, content = null) {
|
|
19
|
+
const element = document.createElement(tag);
|
|
20
|
+
|
|
21
|
+
// Set attributes
|
|
22
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
23
|
+
if (key === 'className') {
|
|
24
|
+
element.className = value;
|
|
25
|
+
} else if (key === 'style' && typeof value === 'object') {
|
|
26
|
+
Object.assign(element.style, value);
|
|
27
|
+
} else if (key === 'dataset' && typeof value === 'object') {
|
|
28
|
+
for (const [dataKey, dataValue] of Object.entries(value)) {
|
|
29
|
+
element.dataset[dataKey] = dataValue;
|
|
30
|
+
}
|
|
31
|
+
} else if (key.startsWith('on') && typeof value === 'function') {
|
|
32
|
+
const eventName = key.slice(2).toLowerCase();
|
|
33
|
+
element.addEventListener(eventName, value);
|
|
34
|
+
} else {
|
|
35
|
+
element.setAttribute(key, value);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Add content
|
|
40
|
+
if (content !== null) {
|
|
41
|
+
this.setContent(element, content);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return element;
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Set element content (supports text, HTML, elements, and arrays)
|
|
49
|
+
* @param {HTMLElement} element - Target element
|
|
50
|
+
* @param {string|Element|Array} content - Content to set
|
|
51
|
+
*/
|
|
52
|
+
setContent(element, content) {
|
|
53
|
+
if (typeof content === 'string') {
|
|
54
|
+
element.textContent = content;
|
|
55
|
+
} else if (content instanceof HTMLElement) {
|
|
56
|
+
element.appendChild(content);
|
|
57
|
+
} else if (Array.isArray(content)) {
|
|
58
|
+
content.forEach(item => {
|
|
59
|
+
if (typeof item === 'string') {
|
|
60
|
+
element.appendChild(document.createTextNode(item));
|
|
61
|
+
} else if (item instanceof HTMLElement) {
|
|
62
|
+
element.appendChild(item);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Safely query selector with null check
|
|
70
|
+
* @param {string} selector - CSS selector
|
|
71
|
+
* @param {Element} context - Context element (default: document)
|
|
72
|
+
* @returns {Element|null} Found element or null
|
|
73
|
+
*/
|
|
74
|
+
query(selector, context = document) {
|
|
75
|
+
try {
|
|
76
|
+
return context.querySelector(selector);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
console.error(`Invalid selector: ${selector}`, e);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Safely query all matching elements
|
|
85
|
+
* @param {string} selector - CSS selector
|
|
86
|
+
* @param {Element} context - Context element (default: document)
|
|
87
|
+
* @returns {Array} Array of elements
|
|
88
|
+
*/
|
|
89
|
+
queryAll(selector, context = document) {
|
|
90
|
+
try {
|
|
91
|
+
return Array.from(context.querySelectorAll(selector));
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.error(`Invalid selector: ${selector}`, e);
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Add classes to element
|
|
100
|
+
* @param {HTMLElement} element - Target element
|
|
101
|
+
* @param {...string} classes - Classes to add
|
|
102
|
+
*/
|
|
103
|
+
addClass(element, ...classes) {
|
|
104
|
+
if (element && element.classList) {
|
|
105
|
+
element.classList.add(...classes.filter(c => c));
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Remove classes from element
|
|
111
|
+
* @param {HTMLElement} element - Target element
|
|
112
|
+
* @param {...string} classes - Classes to remove
|
|
113
|
+
*/
|
|
114
|
+
removeClass(element, ...classes) {
|
|
115
|
+
if (element && element.classList) {
|
|
116
|
+
element.classList.remove(...classes);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Toggle classes on element
|
|
122
|
+
* @param {HTMLElement} element - Target element
|
|
123
|
+
* @param {string} className - Class to toggle
|
|
124
|
+
* @param {boolean} force - Force add (true) or remove (false)
|
|
125
|
+
* @returns {boolean} Whether class is now present
|
|
126
|
+
*/
|
|
127
|
+
toggleClass(element, className, force) {
|
|
128
|
+
if (element && element.classList) {
|
|
129
|
+
return element.classList.toggle(className, force);
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Check if element has class
|
|
136
|
+
* @param {HTMLElement} element - Target element
|
|
137
|
+
* @param {string} className - Class to check
|
|
138
|
+
* @returns {boolean} Whether element has class
|
|
139
|
+
*/
|
|
140
|
+
hasClass(element, className) {
|
|
141
|
+
return element && element.classList && element.classList.contains(className);
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Set multiple styles on element
|
|
146
|
+
* @param {HTMLElement} element - Target element
|
|
147
|
+
* @param {Object} styles - Style properties and values
|
|
148
|
+
*/
|
|
149
|
+
setStyles(element, styles) {
|
|
150
|
+
if (element && element.style && styles) {
|
|
151
|
+
Object.assign(element.style, styles);
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get computed style value
|
|
157
|
+
* @param {HTMLElement} element - Target element
|
|
158
|
+
* @param {string} property - CSS property name
|
|
159
|
+
* @returns {string} Computed style value
|
|
160
|
+
*/
|
|
161
|
+
getStyle(element, property) {
|
|
162
|
+
if (element) {
|
|
163
|
+
return window.getComputedStyle(element).getPropertyValue(property);
|
|
164
|
+
}
|
|
165
|
+
return '';
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Show element (removes display: none)
|
|
170
|
+
* @param {HTMLElement} element - Element to show
|
|
171
|
+
* @param {string} displayValue - Display value to use (default: '')
|
|
172
|
+
*/
|
|
173
|
+
show(element, displayValue = '') {
|
|
174
|
+
if (element && element.style) {
|
|
175
|
+
element.style.display = displayValue;
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Hide element (sets display: none)
|
|
181
|
+
* @param {HTMLElement} element - Element to hide
|
|
182
|
+
*/
|
|
183
|
+
hide(element) {
|
|
184
|
+
if (element && element.style) {
|
|
185
|
+
element.style.display = 'none';
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Toggle element visibility
|
|
191
|
+
* @param {HTMLElement} element - Element to toggle
|
|
192
|
+
* @param {boolean} show - Force show (true) or hide (false)
|
|
193
|
+
*/
|
|
194
|
+
toggle(element, show) {
|
|
195
|
+
if (element) {
|
|
196
|
+
if (show === undefined) {
|
|
197
|
+
show = element.style.display === 'none';
|
|
198
|
+
}
|
|
199
|
+
if (show) {
|
|
200
|
+
this.show(element);
|
|
201
|
+
} else {
|
|
202
|
+
this.hide(element);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Remove element from DOM
|
|
209
|
+
* @param {HTMLElement} element - Element to remove
|
|
210
|
+
*/
|
|
211
|
+
remove(element) {
|
|
212
|
+
if (element && element.parentNode) {
|
|
213
|
+
element.parentNode.removeChild(element);
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Empty element content
|
|
219
|
+
* @param {HTMLElement} element - Element to empty
|
|
220
|
+
*/
|
|
221
|
+
empty(element) {
|
|
222
|
+
if (element) {
|
|
223
|
+
while (element.firstChild) {
|
|
224
|
+
element.removeChild(element.firstChild);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Insert element after reference element
|
|
231
|
+
* @param {HTMLElement} newElement - Element to insert
|
|
232
|
+
* @param {HTMLElement} referenceElement - Reference element
|
|
233
|
+
*/
|
|
234
|
+
insertAfter(newElement, referenceElement) {
|
|
235
|
+
if (referenceElement && referenceElement.parentNode) {
|
|
236
|
+
referenceElement.parentNode.insertBefore(newElement, referenceElement.nextSibling);
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Wrap element with wrapper element
|
|
242
|
+
* @param {HTMLElement} element - Element to wrap
|
|
243
|
+
* @param {HTMLElement} wrapper - Wrapper element
|
|
244
|
+
*/
|
|
245
|
+
wrap(element, wrapper) {
|
|
246
|
+
if (element && element.parentNode) {
|
|
247
|
+
element.parentNode.insertBefore(wrapper, element);
|
|
248
|
+
wrapper.appendChild(element);
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Get element dimensions
|
|
254
|
+
* @param {HTMLElement} element - Target element
|
|
255
|
+
* @returns {Object} Width and height
|
|
256
|
+
*/
|
|
257
|
+
getDimensions(element) {
|
|
258
|
+
if (element) {
|
|
259
|
+
return {
|
|
260
|
+
width: element.offsetWidth,
|
|
261
|
+
height: element.offsetHeight,
|
|
262
|
+
innerWidth: element.clientWidth,
|
|
263
|
+
innerHeight: element.clientHeight
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
return { width: 0, height: 0, innerWidth: 0, innerHeight: 0 };
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get element position relative to viewport
|
|
271
|
+
* @param {HTMLElement} element - Target element
|
|
272
|
+
* @returns {Object} Position coordinates
|
|
273
|
+
*/
|
|
274
|
+
getPosition(element) {
|
|
275
|
+
if (element) {
|
|
276
|
+
const rect = element.getBoundingClientRect();
|
|
277
|
+
return {
|
|
278
|
+
top: rect.top,
|
|
279
|
+
right: rect.right,
|
|
280
|
+
bottom: rect.bottom,
|
|
281
|
+
left: rect.left,
|
|
282
|
+
x: rect.x,
|
|
283
|
+
y: rect.y,
|
|
284
|
+
width: rect.width,
|
|
285
|
+
height: rect.height
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
return { top: 0, right: 0, bottom: 0, left: 0, x: 0, y: 0, width: 0, height: 0 };
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Check if element is visible in viewport
|
|
293
|
+
* @param {HTMLElement} element - Element to check
|
|
294
|
+
* @param {boolean} partial - Allow partial visibility
|
|
295
|
+
* @returns {boolean} Whether element is visible
|
|
296
|
+
*/
|
|
297
|
+
isInViewport(element, partial = false) {
|
|
298
|
+
if (!element) return false;
|
|
299
|
+
|
|
300
|
+
const rect = element.getBoundingClientRect();
|
|
301
|
+
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
|
|
302
|
+
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
|
|
303
|
+
|
|
304
|
+
const vertInView = partial
|
|
305
|
+
? rect.top < windowHeight && rect.bottom > 0
|
|
306
|
+
: rect.top >= 0 && rect.bottom <= windowHeight;
|
|
307
|
+
|
|
308
|
+
const horInView = partial
|
|
309
|
+
? rect.left < windowWidth && rect.right > 0
|
|
310
|
+
: rect.left >= 0 && rect.right <= windowWidth;
|
|
311
|
+
|
|
312
|
+
return vertInView && horInView;
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Smoothly scroll element into view
|
|
317
|
+
* @param {HTMLElement} element - Element to scroll to
|
|
318
|
+
* @param {Object} options - Scroll options
|
|
319
|
+
*/
|
|
320
|
+
scrollIntoView(element, options = {}) {
|
|
321
|
+
if (element && element.scrollIntoView) {
|
|
322
|
+
const defaultOptions = {
|
|
323
|
+
behavior: 'smooth',
|
|
324
|
+
block: 'nearest',
|
|
325
|
+
inline: 'nearest'
|
|
326
|
+
};
|
|
327
|
+
element.scrollIntoView({ ...defaultOptions, ...options });
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Create DocumentFragment from HTML string
|
|
333
|
+
* @param {string} html - HTML string
|
|
334
|
+
* @returns {DocumentFragment} Document fragment
|
|
335
|
+
*/
|
|
336
|
+
createFragment(html) {
|
|
337
|
+
const template = document.createElement('template');
|
|
338
|
+
template.innerHTML = html.trim();
|
|
339
|
+
return template.content;
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Escape HTML special characters
|
|
344
|
+
* @param {string} text - Text to escape
|
|
345
|
+
* @returns {string} Escaped text
|
|
346
|
+
*/
|
|
347
|
+
escapeHtml(text) {
|
|
348
|
+
const div = document.createElement('div');
|
|
349
|
+
div.textContent = text;
|
|
350
|
+
return div.innerHTML;
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Debounce function calls
|
|
355
|
+
* @param {Function} func - Function to debounce
|
|
356
|
+
* @param {number} wait - Wait time in ms
|
|
357
|
+
* @returns {Function} Debounced function
|
|
358
|
+
*/
|
|
359
|
+
debounce(func, wait) {
|
|
360
|
+
let timeout;
|
|
361
|
+
return function executedFunction(...args) {
|
|
362
|
+
const later = () => {
|
|
363
|
+
clearTimeout(timeout);
|
|
364
|
+
func(...args);
|
|
365
|
+
};
|
|
366
|
+
clearTimeout(timeout);
|
|
367
|
+
timeout = setTimeout(later, wait);
|
|
368
|
+
};
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Throttle function calls
|
|
373
|
+
* @param {Function} func - Function to throttle
|
|
374
|
+
* @param {number} limit - Time limit in ms
|
|
375
|
+
* @returns {Function} Throttled function
|
|
376
|
+
*/
|
|
377
|
+
throttle(func, limit) {
|
|
378
|
+
let inThrottle;
|
|
379
|
+
return function(...args) {
|
|
380
|
+
if (!inThrottle) {
|
|
381
|
+
func.apply(this, args);
|
|
382
|
+
inThrottle = true;
|
|
383
|
+
setTimeout(() => inThrottle = false, limit);
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// Support both module and global usage
|
|
390
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
391
|
+
module.exports = domHelpers;
|
|
392
|
+
} else if (typeof define === 'function' && define.amd) {
|
|
393
|
+
define([], () => domHelpers);
|
|
394
|
+
} else {
|
|
395
|
+
window.domHelpers = domHelpers;
|
|
396
|
+
}
|