refactor: abstract tab vs no tab content into separate components

This commit is contained in:
Zack Spear
2025-05-07 10:48:13 -07:00
parent 8379c02192
commit 843efed231
4 changed files with 273 additions and 86 deletions
@@ -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); ?>
+71 -11
View File
@@ -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;
}