1971218fcSKanaru92<?php 2971218fcSKanaru92/** 3971218fcSKanaru92 * DokuWiki Plugin infobox (Syntax Component) 4971218fcSKanaru92 * 5971218fcSKanaru92 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6971218fcSKanaru92 */ 7971218fcSKanaru92if (!defined('DOKU_INC')) die(); 8971218fcSKanaru92 9971218fcSKanaru92class syntax_plugin_infobox extends DokuWiki_Syntax_Plugin { 10971218fcSKanaru92 public function getType() { 11971218fcSKanaru92 return 'substition'; 12971218fcSKanaru92 } 13971218fcSKanaru92 14971218fcSKanaru92 public function getPType() { 15971218fcSKanaru92 return 'block'; 16971218fcSKanaru92 } 17971218fcSKanaru92 18971218fcSKanaru92 public function getSort() { 19971218fcSKanaru92 return 199; 20971218fcSKanaru92 } 21971218fcSKanaru92 22971218fcSKanaru92 public function connectTo($mode) { 23*a5ba79f0SKanaru92 $this->Lexer->addEntryPattern('\{\{infobox>', $mode, 'plugin_infobox'); 24*a5ba79f0SKanaru92 } 25*a5ba79f0SKanaru92 26*a5ba79f0SKanaru92 public function postConnect() { 27*a5ba79f0SKanaru92 $this->Lexer->addExitPattern('\}\}', 'plugin_infobox'); 28971218fcSKanaru92 } 29971218fcSKanaru92 30971218fcSKanaru92 public function handle($match, $state, $pos, Doku_Handler $handler) { 31*a5ba79f0SKanaru92 switch ($state) { 32*a5ba79f0SKanaru92 case DOKU_LEXER_ENTER: 33*a5ba79f0SKanaru92 return array('state' => 'enter'); 34*a5ba79f0SKanaru92 35*a5ba79f0SKanaru92 case DOKU_LEXER_UNMATCHED: 36*a5ba79f0SKanaru92 // This contains the actual content between {{infobox> and }} 37*a5ba79f0SKanaru92 $lines = explode("\n", $match); 38ffecb261SKanaru92 39ffecb261SKanaru92 $params = [ 40ffecb261SKanaru92 'fields' => [], 41ffecb261SKanaru92 'images' => [], 42ffecb261SKanaru92 'sections' => [], 43ffecb261SKanaru92 'collapsed_sections' => [] 44ffecb261SKanaru92 ]; 45ffecb261SKanaru92 46ffecb261SKanaru92 $currentSection = null; 47*a5ba79f0SKanaru92 $currentSubgroup = null; 48ffecb261SKanaru92 $imageIndex = 0; 49*a5ba79f0SKanaru92 $currentKey = null; 50*a5ba79f0SKanaru92 $currentValue = ''; 51ffecb261SKanaru92 52971218fcSKanaru92 foreach ($lines as $line) { 53*a5ba79f0SKanaru92 // Don't trim the line yet - we need to preserve indentation for multi-line values 54ffecb261SKanaru92 55*a5ba79f0SKanaru92 // Check if we're currently capturing a multi-line value 56*a5ba79f0SKanaru92 if ($currentKey !== null) { 57*a5ba79f0SKanaru92 // Continue capturing multi-line value 58*a5ba79f0SKanaru92 $currentValue .= "\n" . $line; 59*a5ba79f0SKanaru92 60*a5ba79f0SKanaru92 // Check if all plugin syntaxes are closed 61*a5ba79f0SKanaru92 if (substr_count($currentValue, '{{') === substr_count($currentValue, '}}')) { 62*a5ba79f0SKanaru92 $this->_saveField($params, $currentKey, trim($currentValue), $currentSection, $currentSubgroup); 63*a5ba79f0SKanaru92 $currentKey = null; 64*a5ba79f0SKanaru92 $currentValue = ''; 65*a5ba79f0SKanaru92 } 66*a5ba79f0SKanaru92 continue; 67*a5ba79f0SKanaru92 } 68*a5ba79f0SKanaru92 69*a5ba79f0SKanaru92 $trimmedLine = trim($line); 70*a5ba79f0SKanaru92 if (empty($trimmedLine)) continue; 71*a5ba79f0SKanaru92 72*a5ba79f0SKanaru92 // Check for section headers 73*a5ba79f0SKanaru92 if (preg_match('/^(={2,3})\s*(.+?)\s*\1$/', $trimmedLine, $sectionMatches)) { 74*a5ba79f0SKanaru92 $currentSection = $sectionMatches[2]; 75*a5ba79f0SKanaru92 $currentSubgroup = null; // Reset subgroup when entering new section 76ffecb261SKanaru92 $params['sections'][$currentSection] = []; 77*a5ba79f0SKanaru92 if ($sectionMatches[1] === '===') { 78ffecb261SKanaru92 $params['collapsed_sections'][$currentSection] = true; 79ffecb261SKanaru92 } 80ffecb261SKanaru92 continue; 81ffecb261SKanaru92 } 82ffecb261SKanaru92 83*a5ba79f0SKanaru92 // Check for subgroup headers (:::) 84*a5ba79f0SKanaru92 if (preg_match('/^:::\s*(.+?)\s*:::$/', $trimmedLine, $subgroupMatches)) { 85*a5ba79f0SKanaru92 if ($currentSection !== null) { 86*a5ba79f0SKanaru92 $currentSubgroup = $subgroupMatches[1]; 87*a5ba79f0SKanaru92 if (!isset($params['sections'][$currentSection]['_subgroups'])) { 88*a5ba79f0SKanaru92 $params['sections'][$currentSection]['_subgroups'] = []; 89*a5ba79f0SKanaru92 } 90*a5ba79f0SKanaru92 $params['sections'][$currentSection]['_subgroups'][$currentSubgroup] = []; 91ffecb261SKanaru92 } 92ffecb261SKanaru92 continue; 93ffecb261SKanaru92 } 94ffecb261SKanaru92 95*a5ba79f0SKanaru92 // Check if this line contains a key=value pair 96*a5ba79f0SKanaru92 if (strpos($trimmedLine, '=') !== false) { 97*a5ba79f0SKanaru92 // Split only on the first = to handle values containing = 98*a5ba79f0SKanaru92 $pos = strpos($trimmedLine, '='); 99*a5ba79f0SKanaru92 $key = trim(substr($trimmedLine, 0, $pos)); 100*a5ba79f0SKanaru92 $value = trim(substr($trimmedLine, $pos + 1)); 101ffecb261SKanaru92 102*a5ba79f0SKanaru92 // Check if value contains unclosed plugin syntax 103*a5ba79f0SKanaru92 $openCount = substr_count($value, '{{'); 104*a5ba79f0SKanaru92 $closeCount = substr_count($value, '}}'); 105*a5ba79f0SKanaru92 106*a5ba79f0SKanaru92 if ($openCount > $closeCount) { 107*a5ba79f0SKanaru92 // Value contains unclosed plugin syntax, start multi-line capture 108*a5ba79f0SKanaru92 $currentKey = $key; 109*a5ba79f0SKanaru92 $currentValue = $value; 110*a5ba79f0SKanaru92 } else { 111*a5ba79f0SKanaru92 // Value is complete on this line 112*a5ba79f0SKanaru92 $this->_saveField($params, $key, $value, $currentSection, $currentSubgroup); 113*a5ba79f0SKanaru92 } 114*a5ba79f0SKanaru92 } 115*a5ba79f0SKanaru92 } 116*a5ba79f0SKanaru92 117*a5ba79f0SKanaru92 // Save any remaining multi-line value 118*a5ba79f0SKanaru92 if ($currentKey !== null) { 119*a5ba79f0SKanaru92 $this->_saveField($params, $currentKey, trim($currentValue), $currentSection, $currentSubgroup); 120*a5ba79f0SKanaru92 } 121*a5ba79f0SKanaru92 122*a5ba79f0SKanaru92 return array('state' => 'content', 'params' => $params); 123*a5ba79f0SKanaru92 124*a5ba79f0SKanaru92 case DOKU_LEXER_EXIT: 125*a5ba79f0SKanaru92 return array('state' => 'exit'); 126*a5ba79f0SKanaru92 } 127*a5ba79f0SKanaru92 128*a5ba79f0SKanaru92 return false; 129*a5ba79f0SKanaru92 } 130*a5ba79f0SKanaru92 131*a5ba79f0SKanaru92 private function _saveField(&$params, $key, $value, $currentSection, $currentSubgroup = null) { 132ffecb261SKanaru92 // Skip empty values unless explicitly showing them 133ffecb261SKanaru92 if (empty($value) && $value !== '0') { 134*a5ba79f0SKanaru92 return; 135ffecb261SKanaru92 } 136ffecb261SKanaru92 137*a5ba79f0SKanaru92 // Handle image fields 138*a5ba79f0SKanaru92 if (preg_match('/^image(\d*)$/', $key, $matches)) { 139*a5ba79f0SKanaru92 $imgNum = $matches[1] ?: '1'; 140ffecb261SKanaru92 // Check if image has a caption (format: filename|caption) 141ffecb261SKanaru92 if (strpos($value, '|') !== false) { 142ffecb261SKanaru92 list($imgPath, $caption) = explode('|', $value, 2); 143*a5ba79f0SKanaru92 $params['images'][$imgNum] = [ 144ffecb261SKanaru92 'path' => trim($imgPath), 145ffecb261SKanaru92 'caption' => trim($caption) 146ffecb261SKanaru92 ]; 147ffecb261SKanaru92 } else { 148*a5ba79f0SKanaru92 $params['images'][$imgNum] = [ 149*a5ba79f0SKanaru92 'path' => trim($value), 150ffecb261SKanaru92 'caption' => '' 151ffecb261SKanaru92 ]; 152ffecb261SKanaru92 } 153ffecb261SKanaru92 } elseif ($key === 'name' || $key === 'title') { 154ffecb261SKanaru92 $params['name'] = $value; 155ffecb261SKanaru92 } elseif ($key === 'header_image') { 156ffecb261SKanaru92 $params['header_image'] = $value; 157*a5ba79f0SKanaru92 } elseif ($key === 'class') { 158*a5ba79f0SKanaru92 $params['class'] = $value; 159ffecb261SKanaru92 } else { 160*a5ba79f0SKanaru92 // Add to current section/subgroup or main fields 161ffecb261SKanaru92 if ($currentSection !== null) { 162*a5ba79f0SKanaru92 if ($currentSubgroup !== null) { 163*a5ba79f0SKanaru92 // Add to subgroup 164*a5ba79f0SKanaru92 $params['sections'][$currentSection]['_subgroups'][$currentSubgroup][$key] = $value; 165ffecb261SKanaru92 } else { 166*a5ba79f0SKanaru92 // Add to section (not in a subgroup) 167*a5ba79f0SKanaru92 $params['sections'][$currentSection][$key] = $value; 168*a5ba79f0SKanaru92 } 169*a5ba79f0SKanaru92 } else { 170*a5ba79f0SKanaru92 // Add to main fields 171ffecb261SKanaru92 $params['fields'][$key] = $value; 172971218fcSKanaru92 } 173971218fcSKanaru92 } 174ffecb261SKanaru92 } 175971218fcSKanaru92 176971218fcSKanaru92 public function render($mode, Doku_Renderer $renderer, $data) { 177971218fcSKanaru92 if ($mode != 'xhtml') return false; 178971218fcSKanaru92 179*a5ba79f0SKanaru92 if (!is_array($data) || !isset($data['state'])) return false; 180*a5ba79f0SKanaru92 181*a5ba79f0SKanaru92 switch ($data['state']) { 182*a5ba79f0SKanaru92 case 'enter': 183*a5ba79f0SKanaru92 // Start of infobox - nothing to do 184*a5ba79f0SKanaru92 break; 185*a5ba79f0SKanaru92 186*a5ba79f0SKanaru92 case 'content': 187*a5ba79f0SKanaru92 // Render the actual infobox 188*a5ba79f0SKanaru92 $params = $data['params']; 189*a5ba79f0SKanaru92 $this->_renderInfobox($renderer, $params); 190*a5ba79f0SKanaru92 break; 191*a5ba79f0SKanaru92 192*a5ba79f0SKanaru92 case 'exit': 193*a5ba79f0SKanaru92 // End of infobox - nothing to do 194*a5ba79f0SKanaru92 break; 195*a5ba79f0SKanaru92 } 196*a5ba79f0SKanaru92 197*a5ba79f0SKanaru92 return true; 198*a5ba79f0SKanaru92 } 199*a5ba79f0SKanaru92 200*a5ba79f0SKanaru92 private function _renderInfobox($renderer, $data) { 201ffecb261SKanaru92 // Generate unique ID for this infobox 202ffecb261SKanaru92 $boxId = 'infobox_' . md5(serialize($data)); 203971218fcSKanaru92 204ffecb261SKanaru92 // Allow custom CSS classes 205*a5ba79f0SKanaru92 $customClass = isset($data['class']) ? ' ' . hsc($data['class']) : ''; 206ffecb261SKanaru92 207ffecb261SKanaru92 $renderer->doc .= '<div class="infobox' . $customClass . '" id="' . $boxId . '" role="complementary" aria-label="Information box">'; 208ffecb261SKanaru92 209ffecb261SKanaru92 // Header image (optional) 210ffecb261SKanaru92 if (isset($data['header_image'])) { 211ffecb261SKanaru92 $renderer->doc .= '<div class="infobox-header-image">'; 212ffecb261SKanaru92 $renderer->internalmedia( 213ffecb261SKanaru92 $data['header_image'], 214ffecb261SKanaru92 null, 215ffecb261SKanaru92 null, 216ffecb261SKanaru92 null, 217ffecb261SKanaru92 null, 218ffecb261SKanaru92 'cache', 219ffecb261SKanaru92 'details' 220ffecb261SKanaru92 ); 221ffecb261SKanaru92 $renderer->doc .= '</div>'; 222ffecb261SKanaru92 } 223ffecb261SKanaru92 224ffecb261SKanaru92 // Title 225971218fcSKanaru92 if (isset($data['name'])) { 226ad4c48d3Sbpmcneilly $renderer->doc .= '<div class="infobox-title">' . $this->_parseWikiText($data['name']) . '</div>'; 227971218fcSKanaru92 } 228971218fcSKanaru92 229ffecb261SKanaru92 // Multiple images with tabs 230ffecb261SKanaru92 if (!empty($data['images'])) { 231ffecb261SKanaru92 $renderer->doc .= '<div class="infobox-images">'; 232ffecb261SKanaru92 233ffecb261SKanaru92 // Image tabs 234ffecb261SKanaru92 if (count($data['images']) > 1) { 235ffecb261SKanaru92 $renderer->doc .= '<div class="infobox-image-tabs" role="tablist">'; 236ffecb261SKanaru92 $first = true; 237ffecb261SKanaru92 $tabCount = 1; 238ffecb261SKanaru92 foreach ($data['images'] as $num => $imgData) { 239ffecb261SKanaru92 $tabLabel = $imgData['caption'] ?: 'Image ' . $tabCount; 240ffecb261SKanaru92 $activeClass = $first ? ' active' : ''; 241ffecb261SKanaru92 $tabIndex = $first ? '0' : '-1'; 242ffecb261SKanaru92 $ariaSelected = $first ? 'true' : 'false'; 243ffecb261SKanaru92 $renderer->doc .= '<button class="infobox-tab' . $activeClass . '" role="tab" aria-selected="' . $ariaSelected . '" onclick="showInfoboxImage(\'' . $boxId . '\', ' . $num . ')" aria-label="View ' . hsc($tabLabel) . '" tabindex="' . $tabIndex . '">'; 244ffecb261SKanaru92 // Add tab number if no custom caption provided 245ffecb261SKanaru92 if (!$imgData['caption'] && count($data['images']) > 1) { 246ffecb261SKanaru92 $renderer->doc .= '<span class="infobox-tab-number">' . $tabCount . '. </span>'; 247ffecb261SKanaru92 } 248ffecb261SKanaru92 $renderer->doc .= hsc($tabLabel); 249ffecb261SKanaru92 $renderer->doc .= '</button>'; 250ffecb261SKanaru92 $first = false; 251ffecb261SKanaru92 $tabCount++; 252ffecb261SKanaru92 } 253ffecb261SKanaru92 $renderer->doc .= '</div>'; 254971218fcSKanaru92 } 255971218fcSKanaru92 256ffecb261SKanaru92 // Image containers 257ffecb261SKanaru92 $first = true; 258ffecb261SKanaru92 foreach ($data['images'] as $num => $imgData) { 259ffecb261SKanaru92 $activeClass = $first ? ' active' : ''; 260ffecb261SKanaru92 $renderer->doc .= '<div class="infobox-image-container' . $activeClass . '" id="' . $boxId . '_img_' . $num . '" role="tabpanel">'; 261ffecb261SKanaru92 262ffecb261SKanaru92 // Use DokuWiki's internal media rendering for proper lightbox support 263ffecb261SKanaru92 $renderer->internalmedia( 264ffecb261SKanaru92 $imgData['path'], 265ffecb261SKanaru92 $imgData['caption'] ?: null, 266ffecb261SKanaru92 null, 267ffecb261SKanaru92 300, 268ffecb261SKanaru92 null, 269ffecb261SKanaru92 'cache', 270ffecb261SKanaru92 'details' 271ffecb261SKanaru92 ); 272ffecb261SKanaru92 273ffecb261SKanaru92 if ($imgData['caption'] && count($data['images']) == 1) { 274ffecb261SKanaru92 $renderer->doc .= '<div class="infobox-image-caption">' . hsc($imgData['caption']) . '</div>'; 275971218fcSKanaru92 } 276ffecb261SKanaru92 $renderer->doc .= '</div>'; 277ffecb261SKanaru92 $first = false; 278971218fcSKanaru92 } 279971218fcSKanaru92 280971218fcSKanaru92 $renderer->doc .= '</div>'; 281ffecb261SKanaru92 } 282ffecb261SKanaru92 283ffecb261SKanaru92 // Main fields table 284ffecb261SKanaru92 if (!empty($data['fields'])) { 285ffecb261SKanaru92 $renderer->doc .= '<table class="infobox-table">'; 286ffecb261SKanaru92 foreach ($data['fields'] as $key => $value) { 287ffecb261SKanaru92 $renderer->doc .= '<tr>'; 288ffecb261SKanaru92 $renderer->doc .= '<th>' . hsc($this->_formatKey($key)) . '</th>'; 289ffecb261SKanaru92 $renderer->doc .= '<td>' . $this->_parseWikiText($value) . '</td>'; 290ffecb261SKanaru92 $renderer->doc .= '</tr>'; 291ffecb261SKanaru92 } 292ffecb261SKanaru92 $renderer->doc .= '</table>'; 293ffecb261SKanaru92 } 294ffecb261SKanaru92 295ffecb261SKanaru92 // Sections 296*a5ba79f0SKanaru92 foreach ($data['sections'] as $sectionName => $sectionData) { 297ffecb261SKanaru92 $sectionId = $boxId . '_section_' . md5($sectionName); 298ffecb261SKanaru92 $isCollapsed = isset($data['collapsed_sections'][$sectionName]); 299ffecb261SKanaru92 $collapsibleClass = $isCollapsed ? ' collapsible collapsed' : ''; 300ffecb261SKanaru92 301ffecb261SKanaru92 $renderer->doc .= '<div class="infobox-section' . $collapsibleClass . '">'; 302ffecb261SKanaru92 if ($isCollapsed) { 303ffecb261SKanaru92 $renderer->doc .= '<div class="infobox-section-header" onclick="toggleInfoboxSection(\'' . $sectionId . '\')" ' . 304ffecb261SKanaru92 'role="button" tabindex="0" aria-expanded="false" aria-controls="' . $sectionId . '">'; 305ffecb261SKanaru92 } else { 306ffecb261SKanaru92 $renderer->doc .= '<div class="infobox-section-header">'; 307ffecb261SKanaru92 } 308ffecb261SKanaru92 $renderer->doc .= hsc($sectionName); 309ffecb261SKanaru92 if ($isCollapsed) { 310ffecb261SKanaru92 $renderer->doc .= '<span class="infobox-section-toggle">▼</span>'; 311ffecb261SKanaru92 } 312ffecb261SKanaru92 $renderer->doc .= '</div>'; 313ffecb261SKanaru92 314ffecb261SKanaru92 $contentClass = $isCollapsed ? 'infobox-section-content collapsed' : 'infobox-section-content'; 315ffecb261SKanaru92 $renderer->doc .= '<div class="' . $contentClass . '" id="' . $sectionId . '">'; 316ffecb261SKanaru92 317*a5ba79f0SKanaru92 // Check if this section has subgroups 318*a5ba79f0SKanaru92 $hasSubgroups = isset($sectionData['_subgroups']) && !empty($sectionData['_subgroups']); 319*a5ba79f0SKanaru92 320*a5ba79f0SKanaru92 if ($hasSubgroups) { 321*a5ba79f0SKanaru92 // Render subgroups 322*a5ba79f0SKanaru92 $renderer->doc .= '<div class="infobox-subgroups">'; 323*a5ba79f0SKanaru92 foreach ($sectionData['_subgroups'] as $subgroupName => $subgroupFields) { 324*a5ba79f0SKanaru92 $renderer->doc .= '<div class="infobox-subgroup">'; 325*a5ba79f0SKanaru92 $renderer->doc .= '<div class="infobox-subgroup-header">' . hsc($subgroupName) . '</div>'; 326*a5ba79f0SKanaru92 327*a5ba79f0SKanaru92 if (!empty($subgroupFields)) { 328ffecb261SKanaru92 $renderer->doc .= '<table class="infobox-table">'; 329*a5ba79f0SKanaru92 foreach ($subgroupFields as $key => $value) { 330ffecb261SKanaru92 $renderer->doc .= '<tr>'; 331ffecb261SKanaru92 $renderer->doc .= '<th>' . hsc($this->_formatKey($key)) . '</th>'; 332ffecb261SKanaru92 $renderer->doc .= '<td>' . $this->_parseWikiText($value) . '</td>'; 333ffecb261SKanaru92 $renderer->doc .= '</tr>'; 334ffecb261SKanaru92 } 335ffecb261SKanaru92 $renderer->doc .= '</table>'; 336ffecb261SKanaru92 } 337ffecb261SKanaru92 $renderer->doc .= '</div>'; 338*a5ba79f0SKanaru92 } 339*a5ba79f0SKanaru92 $renderer->doc .= '</div>'; 340*a5ba79f0SKanaru92 } 341*a5ba79f0SKanaru92 342*a5ba79f0SKanaru92 // Render regular section fields (not in subgroups) 343*a5ba79f0SKanaru92 $regularFields = array_filter($sectionData, function($key) { 344*a5ba79f0SKanaru92 return $key !== '_subgroups'; 345*a5ba79f0SKanaru92 }, ARRAY_FILTER_USE_KEY); 346*a5ba79f0SKanaru92 347*a5ba79f0SKanaru92 if (!empty($regularFields)) { 348*a5ba79f0SKanaru92 $renderer->doc .= '<table class="infobox-table">'; 349*a5ba79f0SKanaru92 foreach ($regularFields as $key => $value) { 350*a5ba79f0SKanaru92 $renderer->doc .= '<tr>'; 351*a5ba79f0SKanaru92 $renderer->doc .= '<th>' . hsc($this->_formatKey($key)) . '</th>'; 352*a5ba79f0SKanaru92 $renderer->doc .= '<td>' . $this->_parseWikiText($value) . '</td>'; 353*a5ba79f0SKanaru92 $renderer->doc .= '</tr>'; 354*a5ba79f0SKanaru92 } 355*a5ba79f0SKanaru92 $renderer->doc .= '</table>'; 356*a5ba79f0SKanaru92 } 357*a5ba79f0SKanaru92 358*a5ba79f0SKanaru92 $renderer->doc .= '</div>'; 359ffecb261SKanaru92 $renderer->doc .= '</div>'; 360ffecb261SKanaru92 } 361ffecb261SKanaru92 362ffecb261SKanaru92 $renderer->doc .= '</div>'; 363ffecb261SKanaru92 364ffecb261SKanaru92 // Add JavaScript for image tabs (only once per page) 365ffecb261SKanaru92 static $jsAdded = false; 366ffecb261SKanaru92 if (!$jsAdded) { 367ffecb261SKanaru92 $renderer->doc .= '<script> 368ffecb261SKanaru92 function showInfoboxImage(boxId, imageNum) { 369ffecb261SKanaru92 // Hide all images in this infobox 370ffecb261SKanaru92 var containers = document.querySelectorAll("#" + boxId + " .infobox-image-container"); 371ffecb261SKanaru92 containers.forEach(function(container) { 372ffecb261SKanaru92 container.classList.remove("active"); 373ffecb261SKanaru92 }); 374ffecb261SKanaru92 375ffecb261SKanaru92 // Remove active class from all tabs in this infobox 376ffecb261SKanaru92 var tabs = document.querySelectorAll("#" + boxId + " .infobox-tab"); 377ffecb261SKanaru92 tabs.forEach(function(tab) { 378ffecb261SKanaru92 tab.classList.remove("active"); 379ffecb261SKanaru92 tab.setAttribute("tabindex", "-1"); 380ffecb261SKanaru92 tab.setAttribute("aria-selected", "false"); 381ffecb261SKanaru92 }); 382ffecb261SKanaru92 383ffecb261SKanaru92 // Show selected image 384ffecb261SKanaru92 document.getElementById(boxId + "_img_" + imageNum).classList.add("active"); 385ffecb261SKanaru92 386ffecb261SKanaru92 // Add active class to clicked tab 387ffecb261SKanaru92 var activeTab = tabs[imageNum - 1]; 388ffecb261SKanaru92 activeTab.classList.add("active"); 389ffecb261SKanaru92 activeTab.setAttribute("tabindex", "0"); 390ffecb261SKanaru92 activeTab.setAttribute("aria-selected", "true"); 391ffecb261SKanaru92 } 392ffecb261SKanaru92 393ffecb261SKanaru92 function toggleInfoboxSection(sectionId) { 394ffecb261SKanaru92 var content = document.getElementById(sectionId); 395ffecb261SKanaru92 var header = content.previousElementSibling; 396ffecb261SKanaru92 var toggle = header.querySelector(".infobox-section-toggle"); 397ffecb261SKanaru92 398ffecb261SKanaru92 if (content.classList.contains("collapsed")) { 399ffecb261SKanaru92 content.classList.remove("collapsed"); 400ffecb261SKanaru92 header.setAttribute("aria-expanded", "true"); 401ffecb261SKanaru92 if (toggle) toggle.textContent = "▲"; 402ffecb261SKanaru92 } else { 403ffecb261SKanaru92 content.classList.add("collapsed"); 404ffecb261SKanaru92 header.setAttribute("aria-expanded", "false"); 405ffecb261SKanaru92 if (toggle) toggle.textContent = "▼"; 406ffecb261SKanaru92 } 407ffecb261SKanaru92 } 408ffecb261SKanaru92 409ffecb261SKanaru92 // Add keyboard support 410ffecb261SKanaru92 document.addEventListener("DOMContentLoaded", function() { 411ffecb261SKanaru92 // Keyboard navigation for image tabs 412ffecb261SKanaru92 var tabGroups = document.querySelectorAll(".infobox-image-tabs"); 413ffecb261SKanaru92 tabGroups.forEach(function(tabGroup) { 414ffecb261SKanaru92 var tabs = tabGroup.querySelectorAll(".infobox-tab"); 415ffecb261SKanaru92 416ffecb261SKanaru92 tabs.forEach(function(tab, index) { 417ffecb261SKanaru92 tab.addEventListener("keydown", function(e) { 418ffecb261SKanaru92 var newIndex = -1; 419ffecb261SKanaru92 420ffecb261SKanaru92 switch(e.key) { 421ffecb261SKanaru92 case "ArrowLeft": 422ffecb261SKanaru92 e.preventDefault(); 423ffecb261SKanaru92 newIndex = index - 1; 424ffecb261SKanaru92 if (newIndex < 0) newIndex = tabs.length - 1; 425ffecb261SKanaru92 break; 426ffecb261SKanaru92 case "ArrowRight": 427ffecb261SKanaru92 e.preventDefault(); 428ffecb261SKanaru92 newIndex = index + 1; 429ffecb261SKanaru92 if (newIndex >= tabs.length) newIndex = 0; 430ffecb261SKanaru92 break; 431ffecb261SKanaru92 case "Home": 432ffecb261SKanaru92 e.preventDefault(); 433ffecb261SKanaru92 newIndex = 0; 434ffecb261SKanaru92 break; 435ffecb261SKanaru92 case "End": 436ffecb261SKanaru92 e.preventDefault(); 437ffecb261SKanaru92 newIndex = tabs.length - 1; 438ffecb261SKanaru92 break; 439ffecb261SKanaru92 } 440ffecb261SKanaru92 441ffecb261SKanaru92 if (newIndex >= 0) { 442ffecb261SKanaru92 tabs[newIndex].focus(); 443ffecb261SKanaru92 tabs[newIndex].click(); 444ffecb261SKanaru92 } 445ffecb261SKanaru92 }); 446ffecb261SKanaru92 }); 447ffecb261SKanaru92 }); 448ffecb261SKanaru92 449ffecb261SKanaru92 // Keyboard support for collapsible sections 450ffecb261SKanaru92 var headers = document.querySelectorAll(".infobox-section.collapsible .infobox-section-header"); 451ffecb261SKanaru92 headers.forEach(function(header) { 452ffecb261SKanaru92 header.addEventListener("keydown", function(e) { 453ffecb261SKanaru92 if (e.key === "Enter" || e.key === " ") { 454ffecb261SKanaru92 e.preventDefault(); 455ffecb261SKanaru92 header.click(); 456ffecb261SKanaru92 } 457ffecb261SKanaru92 }); 458ffecb261SKanaru92 }); 459ffecb261SKanaru92 460ffecb261SKanaru92 // Fix for section header lines 461ffecb261SKanaru92 var infoboxes = document.querySelectorAll(".infobox"); 462ffecb261SKanaru92 if (infoboxes.length > 0) { 463ffecb261SKanaru92 var headers = document.querySelectorAll("h1, h2, h3, h4, h5, h6"); 464ffecb261SKanaru92 headers.forEach(function(header) { 465ffecb261SKanaru92 header.style.overflow = "hidden"; 466ffecb261SKanaru92 }); 467ffecb261SKanaru92 468ffecb261SKanaru92 var pageContent = document.querySelector(".dokuwiki .page"); 469ffecb261SKanaru92 if (pageContent && !pageContent.classList.contains("has-infobox")) { 470ffecb261SKanaru92 pageContent.classList.add("has-infobox"); 471ffecb261SKanaru92 } 472ffecb261SKanaru92 } 473ffecb261SKanaru92 }); 474ffecb261SKanaru92 </script>'; 475ffecb261SKanaru92 $jsAdded = true; 476ffecb261SKanaru92 } 477ffecb261SKanaru92 478971218fcSKanaru92 return true; 479971218fcSKanaru92 } 480971218fcSKanaru92 481971218fcSKanaru92 private function _parseWikiText($text) { 482ffecb261SKanaru92 // Handle multi-line values properly 483ffecb261SKanaru92 $text = str_replace('\n', "\n", $text); 484ffecb261SKanaru92 485971218fcSKanaru92 $info = array(); 486971218fcSKanaru92 $xhtml = p_render('xhtml', p_get_instructions($text), $info); 487ffecb261SKanaru92 488ffecb261SKanaru92 // Remove wrapping <p> tags if it's a single paragraph 489ffecb261SKanaru92 if (substr_count($xhtml, '<p>') == 1) { 490ffecb261SKanaru92 $xhtml = preg_replace('/^\s*<p>(.*)<\/p>\s*$/s', '$1', $xhtml); 491ffecb261SKanaru92 } 492ffecb261SKanaru92 493971218fcSKanaru92 return $xhtml; 494971218fcSKanaru92 } 495ffecb261SKanaru92 496ffecb261SKanaru92 private function _formatKey($key) { 497ffecb261SKanaru92 // Convert underscores to spaces and capitalize words 498ffecb261SKanaru92 return ucwords(str_replace('_', ' ', $key)); 499ffecb261SKanaru92 } 500971218fcSKanaru92}