mirror of
https://github.com/unraid/webgui.git
synced 2026-05-03 00:09:27 -05:00
refactor: abstract tab vs no tab content into separate components
This commit is contained in:
@@ -1,11 +1,18 @@
|
||||
<?
|
||||
<?php
|
||||
/**
|
||||
* Main content template for the Unraid web interface.
|
||||
* Handles the rendering of tabs and page content.
|
||||
* Main content delegator for the Unraid web interface.
|
||||
* Includes the correct template based on tabbed state.
|
||||
*
|
||||
* Even if DisplaySettings is not enabled for tabs, pages with Tabs="true" will use tabs
|
||||
* and pages with Tabs="false" will not use tabs.
|
||||
*/
|
||||
$display['tabs'] = isset($myPage['Tabs'])
|
||||
? (strtolower($myPage['Tabs']) == 'true' ? 0 : 1)
|
||||
: $display['tabs'];
|
||||
$tabbed = $display['tabs'] == 0 && count($pages) > 1;
|
||||
$contentInclude = $tabbed ? 'MainContentTabbed.php' : 'MainContentNotab.php';
|
||||
|
||||
$defaultIcon = "<i class=\"icon-app PanelIcon\"></i>";
|
||||
// Helper function to process icon
|
||||
function process_icon($icon, $docroot, $root) {
|
||||
global $defaultIcon;
|
||||
if (substr($icon, -4) == '.png') {
|
||||
@@ -25,76 +32,6 @@ function process_icon($icon, $docroot, $root) {
|
||||
}
|
||||
return $icon;
|
||||
}
|
||||
|
||||
$tab = 1;
|
||||
// even if DisplaySettings is not enabled for tabs, pages with Tabs="true" will use tabs
|
||||
$display['tabs'] = isset($myPage['Tabs']) ? (strtolower($myPage['Tabs']) == 'true' ? 0 : 1) : 1;
|
||||
$tabbed = $display['tabs'] == 0 && count($pages) > 1;
|
||||
?>
|
||||
<div id="displaybox">
|
||||
<div class="tabs">
|
||||
<? foreach ($pages as $page):
|
||||
$close = false;
|
||||
if (isset($page['Title'])):
|
||||
$title = htmlspecialchars($page['Title']) ?? '';
|
||||
if ($tabbed): ?>
|
||||
<div class="tab">
|
||||
<input type="radio" id="tab<?= $tab ?>" name="tabs" onclick="settab(this.id)">
|
||||
<label for="tab<?= $tab ?>">
|
||||
<?= tab_title($title, $page['root'], _var($page, 'Tag', false)) ?>
|
||||
</label>
|
||||
<div class="content">
|
||||
<? $close = true;
|
||||
else:
|
||||
if ($tab == 1): ?>
|
||||
<div class="tab">
|
||||
<input type="radio" id="tab<?= $tab ?>" name="tabs">
|
||||
<div class="content shift">
|
||||
<? endif; ?>
|
||||
<div class="title">
|
||||
<span class="left">
|
||||
<?= tab_title($title, $page['root'], _var($page, 'Tag', false)) ?>
|
||||
</span>
|
||||
</div>
|
||||
<? endif;
|
||||
$tab++;
|
||||
endif;
|
||||
|
||||
// Handle menu type pages
|
||||
if (isset($page['Type']) && $page['Type'] == 'menu'):
|
||||
$pgs = find_pages($page['name']);
|
||||
foreach ($pgs as $pg):
|
||||
// Set title variable with proper escaping (suppress errors)
|
||||
@$title = htmlspecialchars($pg['Title']);
|
||||
$icon = _var($pg, 'Icon', $defaultIcon);
|
||||
$icon = process_icon($icon, $docroot, $pg['root']); ?>
|
||||
<div class="Panel">
|
||||
<a href="/<?= $path ?>/<?= $pg['name'] ?>" onclick="$.cookie('one','tab1')">
|
||||
<span><?= $icon ?></span>
|
||||
<div class="PanelText"><?= _($title) ?></div>
|
||||
</a>
|
||||
</div>
|
||||
<? endforeach;
|
||||
endif;
|
||||
|
||||
// Annotate with HTML comment
|
||||
annotate($page['file']);
|
||||
|
||||
// Create page content
|
||||
if (empty($page['Markdown']) || $page['Markdown'] == 'true'):
|
||||
eval('?>'.Markdown(parse_text($page['text'])));
|
||||
else:
|
||||
eval('?>'.parse_text($page['text']));
|
||||
endif;
|
||||
|
||||
if ($close): ?>
|
||||
</div><!-- /.content -->
|
||||
</div><!-- /.tab -->
|
||||
<? endif;
|
||||
endforeach; ?>
|
||||
</div><!-- /.tabs -->
|
||||
</div><!-- /#displaybox -->
|
||||
<?
|
||||
// Clean up variables
|
||||
unset($pages, $page, $pgs, $pg, $icon, $nchan, $running, $start, $stop, $row, $script, $opt, $nchan_run);
|
||||
?>
|
||||
<?php require_once __DIR__ . "/$contentInclude"; ?>
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/**
|
||||
* Non-tabbed content template for the Unraid web interface.
|
||||
* Renders all pages in sequence without tabs, using original per-page logic.
|
||||
*/
|
||||
?>
|
||||
<div id="displaybox">
|
||||
<div class="content">
|
||||
<? foreach ($pages as $page): ?>
|
||||
<? annotate($page['file']); ?>
|
||||
<? includePageStylesheets($page); ?>
|
||||
|
||||
<? if (isset($page['Title'])): ?>
|
||||
<div class="title">
|
||||
<?= tab_title($page['Title'], $page['root'], _var($page, 'Tag', false)) ?>
|
||||
</div>
|
||||
<? endif; ?>
|
||||
|
||||
<? if (isset($page['Type']) && $page['Type'] == 'menu'): ?>
|
||||
<? $pgs = find_pages($page['name']); ?>
|
||||
<? foreach ($pgs as $pg): ?>
|
||||
<?
|
||||
@$panelTitle = htmlspecialchars($pg['Title']);
|
||||
$icon = _var($pg, 'Icon', $defaultIcon);
|
||||
$icon = process_icon($icon, $docroot, $pg['root']);
|
||||
?>
|
||||
<div class="Panel">
|
||||
<a href="/<?= $path ?>/<?= $pg['name'] ?>" onclick="$.cookie('one','tab1')">
|
||||
<span><?= $icon ?></span>
|
||||
<div class="PanelText"><?= _($panelTitle) ?></div>
|
||||
</a>
|
||||
</div>
|
||||
<? endforeach; ?>
|
||||
<? endif; ?>
|
||||
|
||||
<? if (empty($page['Markdown']) || $page['Markdown'] == 'true'): ?>
|
||||
<? eval('?>'.Markdown(parse_text($page['text']))); ?>
|
||||
<? else: ?>
|
||||
<? eval('?>'.parse_text($page['text'])); ?>
|
||||
<? endif; ?>
|
||||
<? endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
/**
|
||||
* Tabbed content template for the Unraid web interface.
|
||||
* Accessible, modern, and decoupled from non-tabbed logic.
|
||||
*/
|
||||
?>
|
||||
<div id="displaybox">
|
||||
<nav class="tabs" role="tablist" aria-label="Page Tabs">
|
||||
<div class="tabs-container">
|
||||
<?php
|
||||
$i = 0;
|
||||
foreach ($pages as $page):
|
||||
if (!isset($page['Title'])) continue;
|
||||
$title = htmlspecialchars((string)$page['Title']);
|
||||
$tabId = "tab" . ($i+1);
|
||||
?>
|
||||
<button role="tab" id="<?= $tabId ?>" aria-controls="<?= $tabId ?>-panel" tabindex="<?= $i === 0 ? '0' : '-1' ?>" aria-selected="<?= $i === 0 ? 'true' : 'false' ?>">
|
||||
<?= tab_title($title, $page['root'], _var($page, 'Tag', false)) ?>
|
||||
</button>
|
||||
<?php
|
||||
$i++;
|
||||
endforeach; ?>
|
||||
</div>
|
||||
</nav>
|
||||
<?php
|
||||
$i = 0;
|
||||
foreach ($pages as $page):
|
||||
if (!isset($page['Title'])) continue;
|
||||
$title = htmlspecialchars((string)$page['Title']);
|
||||
$tabId = "tab" . ($i+1);
|
||||
?>
|
||||
<section id="<?= $tabId ?>-panel" role="tabpanel" aria-labelledby="<?= $tabId ?>" style="display:none;" class="content" tabindex="0">
|
||||
<?php
|
||||
if (isset($page['Type']) && $page['Type'] == 'menu') {
|
||||
$pgs = find_pages($page['name']);
|
||||
foreach ($pgs as $pg) {
|
||||
@$title = htmlspecialchars($pg['Title']);
|
||||
$icon = _var($pg, 'Icon', $defaultIcon);
|
||||
$icon = process_icon($icon, $docroot, $pg['root']);
|
||||
echo "<div class=\"Panel\"><a href=\"/$path/{$pg['name']}\"><span>$icon</span><div class=\"PanelText\">"._($title)."</div></a></div>";
|
||||
}
|
||||
}
|
||||
annotate($page['file']);
|
||||
// Handle menu type pages
|
||||
if (isset($page['Type']) && $page['Type'] == 'menu'):
|
||||
$pgs = find_pages($page['name']);
|
||||
foreach ($pgs as $pg):
|
||||
// Set title variable with proper escaping (suppress errors)
|
||||
@$title = htmlspecialchars($pg['Title']);
|
||||
$icon = _var($pg, 'Icon', $defaultIcon);
|
||||
$icon = process_icon($icon, $docroot, $pg['root']); ?>
|
||||
<div class="Panel">
|
||||
<a href="/<?= $path ?>/<?= $pg['name'] ?>" onclick="$.cookie('one','tab1')">
|
||||
<span><?= $icon ?></span>
|
||||
<div class="PanelText"><?= _($title) ?></div>
|
||||
</a>
|
||||
</div>
|
||||
<? endforeach;
|
||||
endif;
|
||||
if (empty($page['Markdown']) || $page['Markdown'] == 'true') {
|
||||
eval('?>'.Markdown(parse_text($page['text'])));
|
||||
} else {
|
||||
eval('?>'.parse_text($page['text']));
|
||||
}
|
||||
?>
|
||||
</section>
|
||||
<?php
|
||||
$i++;
|
||||
endforeach; ?>
|
||||
</div>
|
||||
<script>
|
||||
// Cookie helpers
|
||||
function getCookie(name) {
|
||||
const v = document.cookie.match('(^|;) ?' + name + '=([^;]*)(;|$)');
|
||||
return v ? v[2] : null;
|
||||
}
|
||||
function setCookie(name, value) {
|
||||
document.cookie = name + '=' + value + '; path=/';
|
||||
}
|
||||
|
||||
const tabs = document.querySelectorAll('.tabs [role="tab"]');
|
||||
const panels = document.querySelectorAll('[role="tabpanel"]');
|
||||
|
||||
// Hide all panels by default (avoid flash)
|
||||
panels.forEach(panel => panel.style.display = 'none');
|
||||
|
||||
// Figure out which cookie to use (matches settab logic)
|
||||
let cookieName = 'tab';
|
||||
<?php
|
||||
// Emulate settab's switch logic for cookie name
|
||||
switch ($myPage['name']) {
|
||||
case 'Main':
|
||||
echo "cookieName = 'tab';\n";
|
||||
break;
|
||||
case 'Cache': case 'Data': case 'Device': case 'Flash': case 'Parity':
|
||||
echo "cookieName = 'one';\n";
|
||||
break;
|
||||
default:
|
||||
echo "cookieName = 'one';\n";
|
||||
break;
|
||||
}
|
||||
?>
|
||||
|
||||
// On load: select correct tab from cookie, or default to first
|
||||
let activeIdx = 0;
|
||||
const cookieVal = getCookie(cookieName);
|
||||
if (cookieVal) {
|
||||
const idx = Array.from(tabs).findIndex(tab => tab.id === cookieVal);
|
||||
if (idx !== -1) activeIdx = idx;
|
||||
}
|
||||
tabs.forEach((tab, i) => {
|
||||
if (i === activeIdx) {
|
||||
tab.setAttribute('aria-selected', 'true');
|
||||
tab.setAttribute('tabindex', '0');
|
||||
panels[i].style.display = 'block';
|
||||
} else {
|
||||
tab.setAttribute('aria-selected', 'false');
|
||||
tab.setAttribute('tabindex', '-1');
|
||||
panels[i].style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// On tab click: update cookie and show correct panel
|
||||
// Also update ARIA
|
||||
// No content flash
|
||||
|
||||
tabs.forEach((tab, i) => {
|
||||
tab.addEventListener('click', () => {
|
||||
tabs.forEach((t, j) => {
|
||||
t.setAttribute('aria-selected', j === i ? 'true' : 'false');
|
||||
t.setAttribute('tabindex', j === i ? '0' : '-1');
|
||||
panels[j].style.display = j === i ? 'block' : 'none';
|
||||
});
|
||||
setCookie(cookieName, tab.id);
|
||||
tab.focus();
|
||||
});
|
||||
tab.addEventListener('keydown', e => {
|
||||
let idx = Array.prototype.indexOf.call(tabs, document.activeElement);
|
||||
if (e.key === 'ArrowRight') {
|
||||
tabs[(idx+1)%tabs.length].focus();
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
tabs[(idx-1+tabs.length)%tabs.length].focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php unset($pages, $page, $pgs, $pg, $icon, $nchan, $running, $start, $stop, $row, $script, $opt, $nchan_run); ?>
|
||||
@@ -764,7 +764,8 @@ table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
border-style: hidden;
|
||||
margin: -30px 0 0 0;
|
||||
/* margin: -30px 0 0 0; */
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
@@ -1034,11 +1035,7 @@ span.cpu-speed {
|
||||
color: var(--blue-900);
|
||||
}
|
||||
span.status {
|
||||
float: right;
|
||||
font-size: 1.4rem;
|
||||
margin-top: 30px;
|
||||
padding-right: 8px;
|
||||
letter-spacing: 1.8px;
|
||||
}
|
||||
span.status.vhshift {
|
||||
margin-top: 0;
|
||||
@@ -1156,12 +1153,9 @@ a.list {
|
||||
color: inherit;
|
||||
}
|
||||
div.content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-bottom: 30px;
|
||||
z-index: -1;
|
||||
padding-bottom: 3rem;
|
||||
clear: both;
|
||||
}
|
||||
div.content.shift {
|
||||
@@ -1171,7 +1165,9 @@ label + .content {
|
||||
margin-top: 86px;
|
||||
}
|
||||
div.tabs {
|
||||
position: relative;
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
align-items: stretch !important;
|
||||
}
|
||||
div.tab {
|
||||
float: left;
|
||||
@@ -2315,3 +2311,67 @@ span#wlan0 {
|
||||
margin: 0 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs,
|
||||
.tabs-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tabs button[role="tab"] {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-color: var(--radio-background-color);
|
||||
border: 1px solid var(--disabled-input-border-color);
|
||||
border-radius: 0;
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
border-bottom: 1px solid transparent;
|
||||
color: var(--gray-300);
|
||||
font-weight: normal;
|
||||
font-family: inherit;
|
||||
font-size: 1.4rem;
|
||||
letter-spacing: 1.8px;
|
||||
margin-right: 4px;
|
||||
margin-bottom: 0;
|
||||
padding: .75rem 1rem;
|
||||
min-width: 0;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
vertical-align: middle;
|
||||
line-height: 1.0;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, color 0.2s, background 0.2s, opacity 0.2s;
|
||||
text-transform: none;
|
||||
background-image: none;
|
||||
opacity: .75;
|
||||
}
|
||||
.tabs button[role="tab"] > .tab-icon {
|
||||
margin-right: 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
.tabs button[role="tab"]:focus,
|
||||
.tabs button[role="tab"]:hover,
|
||||
.tabs button[role="tab"][aria-selected="true"] {
|
||||
background: transparent;
|
||||
color: var(--text-color);
|
||||
border-color: var(--brand-orange);
|
||||
border-bottom: 1px solid transparent;
|
||||
opacity: 1;
|
||||
}
|
||||
/* .tabs button[role="tab"]:focus {
|
||||
outline: 1px solid var(--brand-orange);
|
||||
} */
|
||||
.tabs button[role="tab"]:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user