|
135 | 135 |
|
136 | 136 | let rootNode = null; |
137 | 137 | let clickHandler = null; |
| 138 | + let contextMenuHandler = null; |
138 | 139 | let renderToken = 0; |
139 | | - let expandedMemberKey = ''; |
140 | 140 |
|
141 | 141 | const getHostTable = () => doc?.querySelector('table#docker_containers') || null; |
142 | 142 | const isFolderToken = (value) => String(value || '').trim().startsWith('folder-'); |
|
192 | 192 | if (clickHandler && rootNode) { |
193 | 193 | rootNode.removeEventListener('click', clickHandler); |
194 | 194 | } |
| 195 | + if (contextMenuHandler && rootNode) { |
| 196 | + rootNode.removeEventListener('contextmenu', contextMenuHandler); |
| 197 | + } |
195 | 198 | clickHandler = null; |
| 199 | + contextMenuHandler = null; |
196 | 200 | if (rootNode && rootNode.parentNode) { |
197 | 201 | rootNode.parentNode.removeChild(rootNode); |
198 | 202 | } |
|
228 | 232 | return { state, label: 'stopped', icon: 'fa-stop' }; |
229 | 233 | }; |
230 | 234 |
|
231 | | - const getMemberKey = (folderId, memberName) => `${String(folderId || '').trim()}::${String(memberName || '').trim()}`; |
232 | | - |
233 | | - const dispatchContainerControl = (action, containerId) => { |
234 | | - const safeAction = String(action || '').trim(); |
235 | | - const safeContainerId = String(containerId || '').trim(); |
236 | | - if (!safeAction || !safeContainerId || typeof win?.eventControl !== 'function') { |
237 | | - return false; |
238 | | - } |
239 | | - win.eventControl({ action: safeAction, container: safeContainerId }, 'loadlist'); |
240 | | - return true; |
241 | | - }; |
242 | | - |
243 | 235 | const openFolderCardWebuis = (folderCard) => { |
244 | 236 | if (!(folderCard instanceof HTMLElement)) { |
245 | 237 | return false; |
|
263 | 255 | return true; |
264 | 256 | }; |
265 | 257 |
|
266 | | - const buildMemberMenuButtons = (member) => { |
267 | | - const actions = []; |
268 | | - if (member.stateMeta.state === 'stopped') { |
269 | | - actions.push({ action: 'start', label: 'Start' }); |
270 | | - } else if (member.stateMeta.state === 'paused') { |
271 | | - actions.push({ action: 'resume', label: 'Resume' }); |
272 | | - actions.push({ action: 'stop', label: 'Stop' }); |
273 | | - } else { |
274 | | - actions.push({ action: 'stop', label: 'Stop' }); |
275 | | - actions.push({ action: 'pause', label: 'Pause' }); |
| 258 | + const getNativeMemberTrigger = (containerName) => { |
| 259 | + const safeName = String(containerName || '').trim(); |
| 260 | + if (!safeName || !doc) { |
| 261 | + return null; |
276 | 262 | } |
277 | | - if (member.id) { |
278 | | - actions.push({ action: 'restart', label: 'Restart' }); |
| 263 | + const sourceRow = doc.getElementById(`ct-${safeName}`); |
| 264 | + if (!(sourceRow instanceof HTMLElement)) { |
| 265 | + return null; |
279 | 266 | } |
280 | | - return actions.map((entry) => ` |
281 | | - <button type="button" class="fv-docker-command-member-menu-button" data-fv-command-member-action="${escapeHtml(entry.action)}" data-member-name="${escapeHtml(member.name)}"> |
282 | | - ${escapeHtml(entry.label)} |
283 | | - </button> |
284 | | - `).join(''); |
| 267 | + return sourceRow.querySelector('td.ct-name > span.outer > span.hand') |
| 268 | + || sourceRow.querySelector('td.ct-name > span.outer > span.inner > span.appname > a.exec'); |
| 269 | + }; |
| 270 | + |
| 271 | + const proxyNativeMemberTrigger = (containerName, eventType = 'click') => { |
| 272 | + const trigger = getNativeMemberTrigger(containerName); |
| 273 | + if (!(trigger instanceof HTMLElement)) { |
| 274 | + return false; |
| 275 | + } |
| 276 | + const safeType = eventType === 'contextmenu' ? 'contextmenu' : 'click'; |
| 277 | + const event = new MouseEvent(safeType, { |
| 278 | + bubbles: true, |
| 279 | + cancelable: true, |
| 280 | + view: win || undefined, |
| 281 | + button: safeType === 'contextmenu' ? 2 : 0, |
| 282 | + buttons: safeType === 'contextmenu' ? 2 : 1 |
| 283 | + }); |
| 284 | + return trigger.dispatchEvent(event); |
285 | 285 | }; |
286 | 286 |
|
287 | 287 | const computeOrderedFolderIds = (folders, prefs, hostOrder, unraidOrder) => { |
|
426 | 426 | card.childCount > 0 ? `${card.childCount} child folders` : '' |
427 | 427 | ].filter(Boolean).join(' • '); |
428 | 428 | const memberTiles = card.members.map((member) => ` |
429 | | - <div class="fv-docker-command-member-tile ${escapeHtml(member.stateMeta.state)}${member.updateReady ? ' has-update' : ''}${expandedMemberKey === getMemberKey(card.folderId, member.name) ? ' is-expanded' : ''}" data-member-name="${escapeHtml(member.name)}" data-member-id="${escapeHtml(member.id)}" data-member-webui-url="${escapeHtml(member.webuiUrl)}" data-member-shell="${escapeHtml(member.shell)}"> |
430 | | - <button type="button" class="fv-docker-command-member-surface" data-fv-command-member-action="toggle-menu" data-member-name="${escapeHtml(member.name)}"> |
| 429 | + <div class="fv-docker-command-member-tile ${escapeHtml(member.stateMeta.state)}${member.updateReady ? ' has-update' : ''}" data-member-name="${escapeHtml(member.name)}" data-member-id="${escapeHtml(member.id)}" data-member-webui-url="${escapeHtml(member.webuiUrl)}" data-member-shell="${escapeHtml(member.shell)}"> |
| 430 | + <div class="fv-docker-command-member-surface hand" data-fv-command-member-surface="true" data-member-name="${escapeHtml(member.name)}"> |
431 | 431 | <span class="fv-docker-command-member-icon-wrap"> |
432 | 432 | <img src="${member.icon}" class="fv-docker-command-member-icon" alt="" loading="lazy" onerror='this.src="${DOCKER_ICON_FALLBACK}"'> |
433 | 433 | </span> |
|
441 | 441 | ${member.updateReady ? '<span class="fv-docker-command-member-update">update ready</span>' : ''} |
442 | 442 | </span> |
443 | 443 | </span> |
444 | | - <span class="fv-docker-command-member-quick-actions"> |
445 | | - ${member.webuiUrl ? `<button type="button" class="fv-docker-command-member-icon-button" title="Open WebUI" aria-label="Open WebUI" data-fv-command-member-action="webui" data-member-name="${escapeHtml(member.name)}"><i class="fa fa-globe" aria-hidden="true"></i></button>` : ''} |
446 | | - <button type="button" class="fv-docker-command-member-icon-button" title="Open console" aria-label="Open console" data-fv-command-member-action="console" data-member-name="${escapeHtml(member.name)}"><i class="fa fa-terminal" aria-hidden="true"></i></button> |
447 | | - <button type="button" class="fv-docker-command-member-icon-button" title="Open logs" aria-label="Open logs" data-fv-command-member-action="logs" data-member-name="${escapeHtml(member.name)}"><i class="fa fa-bars" aria-hidden="true"></i></button> |
| 444 | + <span class="fv-docker-command-member-actions"> |
| 445 | + ${member.webuiUrl ? `<span class="folder-element-custom-btn folder-element-webui"><a href="${escapeHtml(member.webuiUrl)}" title="Open WebUI" aria-label="Open WebUI" data-fv-command-member-action="webui" data-member-name="${escapeHtml(member.name)}"><i class="fa fa-globe" aria-hidden="true"></i></a></span>` : ''} |
| 446 | + <span class="folder-element-custom-btn folder-element-console"><a href="#" title="Open console" aria-label="Open console" data-fv-command-member-action="console" data-member-name="${escapeHtml(member.name)}"><i class="fa fa-terminal" aria-hidden="true"></i></a></span> |
| 447 | + <span class="folder-element-custom-btn folder-element-logs"><a href="#" title="Open logs" aria-label="Open logs" data-fv-command-member-action="logs" data-member-name="${escapeHtml(member.name)}"><i class="fa fa-bars" aria-hidden="true"></i></a></span> |
448 | 448 | </span> |
449 | | - </button> |
450 | | - <div class="fv-docker-command-member-menu"> |
451 | | - ${buildMemberMenuButtons(member)} |
452 | 449 | </div> |
453 | 450 | </div> |
454 | 451 | `).join(''); |
|
517 | 514 | const folderCard = memberButton.closest('[data-folder-id]'); |
518 | 515 | const folderId = folderCard instanceof HTMLElement ? String(folderCard.getAttribute('data-folder-id') || '').trim() : ''; |
519 | 516 | const memberName = memberTile instanceof HTMLElement ? String(memberTile.getAttribute('data-member-name') || '').trim() : ''; |
520 | | - const memberId = memberTile instanceof HTMLElement ? String(memberTile.getAttribute('data-member-id') || '').trim() : ''; |
521 | 517 | const memberWebuiUrl = memberTile instanceof HTMLElement ? String(memberTile.getAttribute('data-member-webui-url') || '').trim() : ''; |
522 | 518 | const memberShell = memberTile instanceof HTMLElement ? String(memberTile.getAttribute('data-member-shell') || '').trim() || '/bin/sh' : '/bin/sh'; |
523 | 519 | if (!folderId || !memberName) { |
524 | 520 | return; |
525 | 521 | } |
526 | | - const memberKey = getMemberKey(folderId, memberName); |
527 | | - if (memberAction === 'toggle-menu') { |
528 | | - expandedMemberKey = expandedMemberKey === memberKey ? '' : memberKey; |
529 | | - rootNode.querySelectorAll('.fv-docker-command-member-tile.is-expanded').forEach((node) => { |
530 | | - if (node !== memberTile) { |
531 | | - node.classList.remove('is-expanded'); |
532 | | - } |
533 | | - }); |
534 | | - if (memberTile instanceof HTMLElement) { |
535 | | - memberTile.classList.toggle('is-expanded', expandedMemberKey === memberKey); |
536 | | - } |
537 | | - return; |
538 | | - } |
539 | 522 | if (memberAction === 'webui') { |
540 | 523 | openWebuiInNewTab(memberWebuiUrl); |
541 | 524 | return; |
|
548 | 531 | openTerminal('docker', memberName, '.log'); |
549 | 532 | return; |
550 | 533 | } |
551 | | - if (memberAction === 'start' || memberAction === 'stop' || memberAction === 'pause' || memberAction === 'resume' || memberAction === 'restart') { |
552 | | - if (dispatchContainerControl(memberAction, memberId)) { |
553 | | - queueLoadlistRefresh({ suppressLoadingUi: true }); |
554 | | - } |
555 | | - return; |
| 534 | + } |
| 535 | + const memberSurface = event.target instanceof Element |
| 536 | + ? event.target.closest('[data-fv-command-member-surface="true"]') |
| 537 | + : null; |
| 538 | + if (memberSurface instanceof HTMLElement) { |
| 539 | + const memberName = String(memberSurface.getAttribute('data-member-name') || '').trim(); |
| 540 | + if (memberName) { |
| 541 | + event.preventDefault(); |
| 542 | + event.stopPropagation(); |
| 543 | + proxyNativeMemberTrigger(memberName, 'click'); |
556 | 544 | } |
| 545 | + return; |
557 | 546 | } |
558 | 547 | const button = event.target instanceof Element |
559 | 548 | ? event.target.closest('[data-fv-command-action]') |
|
619 | 608 | } |
620 | 609 | }; |
621 | 610 | rootNode.addEventListener('click', clickHandler); |
| 611 | + if (contextMenuHandler) { |
| 612 | + rootNode.removeEventListener('contextmenu', contextMenuHandler); |
| 613 | + } |
| 614 | + contextMenuHandler = (event) => { |
| 615 | + const memberSurface = event.target instanceof Element |
| 616 | + ? event.target.closest('[data-fv-command-member-surface="true"]') |
| 617 | + : null; |
| 618 | + if (!(memberSurface instanceof HTMLElement)) { |
| 619 | + return; |
| 620 | + } |
| 621 | + const memberName = String(memberSurface.getAttribute('data-member-name') || '').trim(); |
| 622 | + if (!memberName) { |
| 623 | + return; |
| 624 | + } |
| 625 | + event.preventDefault(); |
| 626 | + event.stopPropagation(); |
| 627 | + proxyNativeMemberTrigger(memberName, 'contextmenu'); |
| 628 | + }; |
| 629 | + rootNode.addEventListener('contextmenu', contextMenuHandler); |
622 | 630 | }; |
623 | 631 |
|
624 | 632 | const resolveSnapshot = async (options = {}) => { |
|
0 commit comments