<?php require_once 'htmlelements.class.php'; /** * ______________________________________________________________________ * * _ __ __ _ * | || \/ || |__ Institute for Medical Education * |_||_|\/|_||____| University of Bern * * ______________________________________________________________________ * * RENDERER FOR ADNINLTE template https://adminlte.io * its docs: https://adminlte.io/docs/3.2/ * https://adminlte.io/themes/v3/index3.html * * This is a php class to render * - grid layout * - navigation * - widgets, components and forms * * DOCS: https://os-docs.iml.unibe.ch/adminlte-renderer/ * ---------------------------------------------------------------------- * 2023-09-11 <axel.hahn@unibe.ch> add shadows on card + callout * 2023-09-27 <axel.hahn@unibe.ch> add form input fields * 2023-11-17 <axel.hahn@unibe.ch> add tabbed content; "=" renders hamburger item * 2024-05-03 <axel.hahn@unibe.ch> add line in sidebar menu; add getFormSelect * 2024-05-10 <axel.hahn@unibe.ch> add support for bootstrap-select in getFormSelect * 2024-05-18 <axel.hahn@unibe.ch> add variable types * 2024-07-04 <axel.hahn@unibe.ch> added type declarations * ====================================================================== */ class renderadminlte { protected array $aPresets = [ 'bgcolor' => [ 'description' => 'background colors', 'group' => 'styling', 'values' => [ // https://adminlte.io/themes/v3/pages/UI/general.html '' => 'no value', 'indigo' => 'indigo', 'lightblue' => '', 'navy' => '', 'purple' => '', 'fuchsia' => '', 'pink' => '', 'maroon' => '', 'orange' => '', 'lime' => '', 'teal' => '', 'olive' => '', 'black' => 'black', 'dark' => 'dark gray', 'gray' => 'gray', 'light' => 'light gray', ] ], 'type' => [ 'description' => 'type or status like info/ warning/ danger to define a color', 'group' => 'styling', 'values' => [ '' => 'no value', 'danger' => 'red', 'info' => 'aqua', 'primary' => 'blue', 'secondary' => 'gray', 'success' => 'green', 'warning' => 'yellow', 'dark' => 'dark gray', 'gray' => 'gray', ] ], 'shadow' => [ 'description' => 'use a shadow', 'group' => 'styling', 'values' => [ '' => 'no value', 'none' => 'none', 'small' => 'small', 'regular' => 'regular', 'large' => 'large' ] ], 'size' => [ 'description' => 'set a size', 'group' => 'styling', 'values' => [ '' => 'no value', 'lg' => '', 'sm' => '', 'xs' => '', 'flat' => '', ] ], 'variant' => [ 'description' => 'coloring style', 'group' => 'styling', 'values' => [ '' => 'no value', 'outline' => 'small stripe on top', 'solid' => 'full filled widget', 'gradient' => 'full filled with gradient', ] ], 'visibility' => [ 'description' => '', 'group' => 'customizing', 'values' => [ '' => 'no value', '0' => 'hide', '1' => 'show', ] ], // for keys: state 'windowstate' => [ 'description' => 'state of a resizable widget', 'group' => 'customizing', 'values' => [ '' => 'no value', 'collapsed' => 'header only', 'maximized' => 'full window', ] ], // for keys: dismissable 'yesno' => [ 'description' => '', 'group' => 'customizing', 'values' => [ '' => 'no value', '0' => 'no', '1' => 'yes', ] ], ]; protected array $_aValueMappings = [ 'shadow' => [ 'default' => '', 'none' => 'shadow-none', 'small' => 'shadow-small', 'regular' => 'shadow', 'large' => 'shadow-lg', ] ]; protected array $_aElements = []; /** * instance of htmlelements object * @var object */ protected object $_oHtml; // ---------------------------------------------------------------------- // // CONSTRUCTOR // // ---------------------------------------------------------------------- public function __construct() { $this->_oHtml = new htmlelements(); $this->_initElements(); // return true; } // ---------------------------------------------------------------------- // // PRIVATE FUNCTIONS // // ---------------------------------------------------------------------- /** * used in cosntructor * initialize all element definitions * @return void */ protected function _initElements(): void { $this->_aElements = [ // ------------------------------------------------------------ 'alert' => [ 'label' => 'Alert', 'description' => 'Colored box with title and a text', 'method' => 'getAlert', 'params' => [ 'type' => ['select' => $this->aPresets['type'], 'example_value' => 'warning'], 'dismissible' => ['select' => $this->aPresets['yesno'], 'example_value' => ''], 'class' => [ 'group' => 'styling', 'description' => 'optional: css classes', 'example_value' => '' ], 'title' => [ 'description' => 'Title in a bit bigger font', 'group' => 'content', 'example_value' => 'Alert title' ], 'text' => [ 'description' => 'Message text', 'group' => 'content', 'example_value' => 'I am a message. Read me, please.' ], ] ], // ------------------------------------------------------------ 'badge' => [ 'label' => 'Badge', 'description' => 'Tiny additional info; mostly as counter', 'method' => 'getBadge', 'params' => [ 'type' => ['select' => $this->aPresets['type'], 'example_value' => 'danger'], 'bgcolor' => ['select' => $this->aPresets['bgcolor'], 'example_value' => ''], 'class' => [ 'group' => 'styling', 'description' => 'optional: css classes', 'example_value' => '' ], 'id' => [ 'group' => 'customizing', 'description' => 'optional: id attribute', 'example_value' => '' ], 'title' => [ 'group' => 'content', 'description' => 'optional: title attribute for mouseover', 'example_value' => 'Errors: 5' ], 'text' => [ 'group' => 'content', 'description' => 'Text or value in the badge', 'example_value' => '5' ], ] ], // ------------------------------------------------------------ 'button' => [ 'label' => 'Button', 'description' => 'Buttons<br>In this component you can add other parmeter keys too - these will be added as attributes in the button tag.', 'method' => 'getButton', 'params' => [ 'type' => ['select' => $this->aPresets['type'], 'example_value' => 'primary'], 'size' => ['select' => $this->aPresets['size'], 'example_value' => ''], 'class' => [ 'group' => 'styling', 'description' => 'optional: css classes', 'example_value' => '' ], 'text' => [ 'group' => 'content', 'description' => 'Text/ html code on the button', 'example_value' => 'Click me' ], ] ], // ------------------------------------------------------------ 'callout' => [ 'label' => 'Callout', 'description' => 'Kind of infobox', 'method' => 'getCallout', 'params' => [ 'type' => ['select' => $this->aPresets['type'], 'example_value' => 'danger'], 'shadow' => ['select' => $this->aPresets['shadow'], 'example_value' => ''], 'class' => [ 'group' => 'styling', 'description' => 'optional: css classes', 'example_value' => '' ], 'title' => [ 'group' => 'content', 'description' => 'Title in a bit bigger font', 'example_value' => 'I am a callout' ], 'text' => [ 'group' => 'content', 'description' => 'Message text', 'example_value' => 'Here is some description to whatever.' ], ] ], // ------------------------------------------------------------ 'card' => [ 'label' => 'Card', 'description' => 'Content box with header, text, footer', 'method' => 'getCard', 'params' => [ 'type' => ['select' => $this->aPresets['type'], 'example_value' => 'primary'], 'variant' => ['select' => $this->aPresets['variant'], 'example_value' => 'outline'], 'shadow' => ['select' => $this->aPresets['shadow'], 'example_value' => ''], 'class' => [ 'group' => 'styling', 'description' => 'optional: css classes', 'example_value' => '' ], 'state' => [ 'group' => 'customizing', 'select' => $this->aPresets['windowstate'], 'example_value' => '' ], 'tb-collapse' => ['description' => 'show minus symbol as collapse button', 'select' => $this->aPresets['visibility'], 'example_value' => ''], 'tb-expand' => ['description' => 'show plus symbol to expand card', 'select' => $this->aPresets['visibility'], 'example_value' => ''], 'tb-maximize' => ['description' => 'show maximize button for fullscreen', 'select' => $this->aPresets['visibility'], 'example_value' => ''], 'tb-minimize' => ['description' => 'show minimize button to minimize', 'select' => $this->aPresets['visibility'], 'example_value' => ''], 'tb-remove' => ['description' => 'show cross symbol to remove card', 'select' => $this->aPresets['visibility'], 'example_value' => ''], 'title' => [ 'group' => 'content', 'description' => 'Title in the top row', 'example_value' => 'I am a card' ], 'tools' => [ 'group' => 'content', 'description' => 'Html code for the top right', 'example_value' => '' ], 'text' => [ 'group' => 'content', 'description' => 'Main content', 'example_value' => 'Here is some beautiful content.' ], 'footer' => [ 'group' => 'content', 'description' => 'optional: footer content', 'example_value' => 'Footer' ], ] ], // ------------------------------------------------------------ 'infobox' => [ 'label' => 'Info box', 'description' => 'Box with icon to highlight a single value; optional with a progress bar', 'method' => 'getInfobox', 'params' => [ 'type' => ['select' => $this->aPresets['type'], 'example_value' => ''], 'iconbg' => ['select' => $this->aPresets['type'], 'example_value' => 'info'], 'shadow' => ['select' => $this->aPresets['shadow'], 'example_value' => ''], 'class' => [ 'group' => 'styling', 'description' => 'optional: css classes', 'example_value' => '' ], 'icon' => [ 'group' => 'content', 'description' => 'css class for an icon', 'example_value' => 'fa-regular fa-thumbs-up' ], 'text' => [ 'group' => 'content', 'description' => 'short information text', 'example_value' => 'Likes' ], 'number' => [ 'group' => 'content', 'description' => 'a number to highlight', 'example_value' => "41,410" ], 'progressvalue' => [ 'group' => 'content', 'description' => 'optional: progress value 0..100 to draw a progress bar', 'example_value' => 70 ], 'progresstext' => [ 'group' => 'content', 'description' => 'optional: text below progress bar', 'example_value' => '70% Increase in 30 Days' ] ] ], // ------------------------------------------------------------ 'smallbox' => [ 'label' => 'Small box', 'description' => 'Solid colored box to highlight a single value; optional with a link', 'method' => 'getSmallbox', 'params' => [ 'type' => ['select' => $this->aPresets['type'], 'example_value' => 'info'], 'shadow' => ['select' => $this->aPresets['shadow'], 'example_value' => ''], 'class' => [ 'group' => 'styling', 'description' => 'optional: css classes', 'example_value' => '' ], 'icon' => ['group' => 'content', 'description' => 'css class for an icon', 'example_value' => 'fa-solid fa-shopping-cart'], 'text' => ['group' => 'content', 'description' => 'short information text', 'example_value' => 'New orders'], 'number' => ['group' => 'content', 'description' => 'a number to highlight', 'example_value' => "150"], 'url' => ['group' => 'content', 'description' => 'optional: url to set a link on the bottom', 'example_value' => '#'], 'linktext' => ['group' => 'content', 'description' => 'used if a url was given: linked text', 'example_value' => 'More info'] ] ], // ------------------------------------------------------------ 'input' => [ 'label' => 'Form: input', 'description' => 'Input form fiels', 'method' => 'getFormInput', 'params' => [ 'label' => [ 'group' => 'styling', 'description' => 'label for the input field', 'example_value' => 'Enter something' ], 'type' => [ 'select' => [ 'description' => 'type or input field', 'group' => 'styling', 'values' => [ 'button' => 'button', 'checkbox' => 'checkbox', 'color' => 'color', 'date' => 'date', 'datetime-local' => 'datetime-local', 'email' => 'email', 'file' => 'file', 'hidden' => 'hidden', 'image' => 'image', 'month' => 'month', 'number' => 'number', 'password' => 'password', 'radio' => 'radio', 'range' => 'range', 'reset' => 'reset', 'search' => 'search', 'submit' => 'submit', 'tel' => 'tel', 'text' => 'text', 'time' => 'time', 'url' => 'url', 'week' => 'week', ] ], 'example_value' => 'text' ], 'class' => [ 'group' => 'styling', 'description' => 'optional: css classes', 'example_value' => 'myclass' ], 'prepend' => [ 'group' => 'styling', 'description' => 'optional: content on input start', 'example_value' => '' ], 'append' => [ 'group' => 'styling', 'description' => 'optional: content on input end', 'example_value' => '' ], 'name' => [ 'group' => 'content', 'description' => 'name attribute', 'example_value' => 'firstname' ], 'value' => [ 'group' => 'content', 'description' => 'Value', 'example_value' => 'Jack' ], ] ], // ------------------------------------------------------------ // WIP 'select' => [ 'label' => 'Form: select', 'description' => 'Select box', 'method' => 'getFormSelect', 'params' => [ 'label' => [ 'group' => 'styling', 'description' => 'label for the select field', 'example_value' => 'Enter text' ], 'bootstrap-select' => [ 'select' => [ 'description' => 'Enable bootstrap-select plugin', 'group' => 'styling', 'values' => [ '0' => 'no', '1' => 'yes', ] ] ], 'class' => [ 'group' => 'styling', 'description' => 'optional: css classes', 'example_value' => 'myclass' ], 'options' => [ 'example_value' => [ ["value" => "1", "label" => "one"], ["value" => "2", "label" => "two"], ["value" => "3", "label" => "three"], ], ] ], ], // ------------------------------------------------------------ 'textarea' => [ 'label' => 'Form: textarea', 'description' => 'textarea or html editor', 'method' => 'getFormTextarea', 'params' => [ 'label' => [ 'group' => 'styling', 'description' => 'label for the input field', 'example_value' => 'Enter text' ], 'type' => [ 'select' => [ 'description' => 'type or input field', 'group' => 'styling', 'values' => [ '' => 'text', 'html' => 'html editor', ] ], ], 'class' => [ 'group' => 'styling', 'description' => 'optional: css classes', 'example_value' => 'myclass' ], 'name' => [ 'group' => 'content', 'description' => 'name attribute', 'example_value' => 'textdata' ], 'value' => [ 'group' => 'content', 'description' => 'Value', 'example_value' => 'Here is some text...' ], ] ], ]; } /** * helper function: a shortcut for $this->_oHtml->getTag * @param string $sTag name of html tag * @param array $aAttributes array of its attributes * @param string $sContent content between opening and closing tag * @param bool $bClosetag flag: write a closing tag or not? default: true * @return string */ protected function _tag(string $sTag, array $aAttributes, string $sContent = '', bool $bClosetag = true): string { if ($sContent) { $aAttributes['label'] = (isset($aAttributes['label']) ? $aAttributes['label'] : '') . $sContent; } return $this->_oHtml->getTag($sTag, $aAttributes, $bClosetag); } // ---------------------------------------------------------------------- // // PUBLIC FUNCTIONS AdminLTE 3.2 // // ---------------------------------------------------------------------- /** * render a page by using template * @param string $stemplate html template with placeholders * @param array $aReplace key = what to replace .. value = new value * @return string */ public function render(string $sTemplate, array $aReplace): string { return str_replace( array_keys($aReplace), array_values($aReplace), $sTemplate ); } /** * add a wrapper: wrap some content into a tag * * @param string $sTag name of html tag * @param array $aOptions array of its attributes * @param string $sContent html content inside * @return string */ public function addWrapper(string $sTag, array $aOptions, string $sContent): string { $aOptions['label'] = $sContent; return $this->_tag($sTag, $aOptions) . PHP_EOL; } // ---------------------------------------------------------------------- // // PUBLIC FUNCTIONS :: NAVIGATION // // ---------------------------------------------------------------------- /** * get a single navigation item on top bar * * @param array $aLink Array of html attributes; key children can contain subitems * @param integer $iLevel Menu level; 1=top bar; 2=pupup menu with subitems * @return string */ public function getNavItem(array $aLink, int $iLevel = 1): string { static $iCounter; if (!isset($iCounter)) { $iCounter = 0; } switch ($aLink['label']) { // special menu entry: horizontal bar (label is "-") case '-': return '<div class="dropdown-divider"></div>'; break; // special menu entry: hamburger menu item (label is "=") case '=': return '<li class="nav-item"><a class="nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars"></i></a></li>'; break; // special menu entry: hamburger menu item (label is "|") // requires css: .navbar-nav li.divider{border-left: 1px solid rgba(0,0,0,0.2);} case '|': return '<li class="divider"></li>'; break; } $aChildren = isset($aLink['children']) && is_array($aLink['children']) && count($aLink['children']) ? $aLink['children'] : false; $aLink['class'] = 'nav-link' . (isset($aLink['class']) ? ' ' . $aLink['class'] : '') . ($aChildren ? ' dropdown-toggle' : '') ; if ($aChildren) { $iCounter++; $sNavId = "navbarDropdown" . $iCounter; $aLink = array_merge($aLink, [ 'id' => $sNavId, 'role' => "button", 'data-toggle' => "dropdown", 'aria-haspopup' => "true", 'aria-expanded' => "false", ]); unset($aLink['children']); // remove from html attributes to draw } $sReturn = $this->_tag('a', $aLink) . "\n"; if ($aChildren) { $iLevel++; $sReturn .= $this->addWrapper( 'div', [ 'aria-labelledby' => $sNavId, 'class' => 'dropdown-menu' ], $this->getNavItems($aChildren, $iLevel) ) . "\n"; $iLevel--; } if ($iLevel == 1) { $sLiClass = 'nav-item' . ($aChildren ? ' dropdown' : ''); $sReturn = $this->addWrapper( 'li', ['class' => $sLiClass], $sReturn ); } return $sReturn; } /** * get html code for navigation on top bar * * @param array $aLinks array of navigation items * @param int $iLevel current navigation level; default: 1 * @return string|bool */ public function getNavItems(array $aLinks, int $iLevel = 1): string|bool { $sReturn = ''; if (!$aLinks || !is_array($aLinks) || !count($aLinks)) { return false; } foreach ($aLinks as $aLink) { $sReturn .= $this->getNavItem($aLink, $iLevel); } return $sReturn; } /** * get a top left navigation for a top navigation bar * * @example * <code> * $aTopnav=[ * ['href'=>'/index.php', 'label'=>'MyHome' , 'class'=>'bg-gray'], * ['href'=>'#', 'label'=>'Contact'], * ['href'=>'#', 'label'=>'Help', * 'children'=>[ * ['href'=>'#', 'label'=>'FAQ'], * ['label'=>'-'], * ['href'=>'#', 'label'=>'Support'], * ] * ] * ]; * echo '<nav class="main-header navbar navbar-expand navbar-white navbar-light">' * .$renderAdminLTE->getTopNavigation($aTopnav) * .'</nav>' * </code> * * @param array $aNavItems array of navigation items/ tree * @param array $aUlOptions array of html attrubutes for wrapping UL tag * @param array $aNavItemsRight array of html attrubutes for wrapping UL tag * @param array $aUlOptionsRight array of html attrubutes for wrapping UL tag * @return string */ public function getTopNavigation(array $aNavItems, array $aUlOptions = [], array $aNavItemsRight = [], array $aUlOptionsRight = []): string { // array_unshift($aNavItems, ['class'=>'nav-link', 'data-widget'=>'pushmenu', 'href'=>'#', 'role'=>'button', 'label'=>'<i class="fa-solid fa-bars"></i>']); $aUlOptLeft = count($aUlOptions) ? $aUlOptions : ['class' => 'navbar-nav']; $aUlOptRight = count($aUlOptionsRight) ? $aUlOptionsRight : ['class' => 'navbar-nav ml-auto']; return $this->addWrapper('ul', $aUlOptLeft, $this->getNavItems($aNavItems)) . (count($aNavItemsRight) ? $this->addWrapper('ul', $aUlOptRight, $this->getNavItems($aNavItemsRight)) : '' ) ; } // ---------------------------------------------------------------------- /** * Get a navigation items for sidebar * Links can be nested with the key "children". * * Remark: for a horizontal line ($aLink['label']='-') this css is required * .nav-item hr{color: #505860; border-top: 1px solid; height: 1px; padding: 0; margin: 0; } * * @param array $aLinks list of link items * @return string|bool */ public function getNavi2Items(array $aLinks): string|bool { $sReturn = ''; if (!$aLinks || !is_array($aLinks) || !count($aLinks)) { return false; } foreach ($aLinks as $aLink) { if ($aLink['label'] == '-') { // TODO: draw a nice line $sReturn .= '<li class="nav-item"><hr></li>'; continue; } // to render active or open links: $aLink['class'] = 'nav-link' . (isset($aLink['class']) ? ' ' . $aLink['class'] : ''); $aChildren = isset($aLink['children']) ? $aLink['children'] : false; $aLiClass = 'nav-item' . ($aChildren && strstr($aLink['class'], 'active') ? ' menu-open' : ''); $sSubmenu = ''; if ($aChildren) { unset($aLink['children']); $aLink['label'] .= '<i class="right fa-solid fa-angle-left"></i>'; $sSubmenu .= $this->getSidebarNavigation($aChildren, ['class' => 'nav nav-treeview']); } $aLink['label'] = $this->addWrapper('p', [], $aLink['label']); $sReturn .= $this->addWrapper( 'li', ['class' => $aLiClass], $this->_tag('a', $aLink) . $sSubmenu ) . "\n"; } return $sReturn; } /** * get html code for sidebar navigation * * @param array $aNavItems navigation item * @param array $aUlOptions aatributes for UL tag * @param string */ public function getSidebarNavigation( array $aNavItems, array $aUlOptions = [ 'class' => 'nav nav-pills nav-sidebar flex-column nav-flat_ nav-child-indent', 'data-widget' => 'treeview', 'role' => 'menu', 'data-accordion' => 'false' ] ): string { return $this->addWrapper( 'ul', $aUlOptions, $this->getNavi2Items($aNavItems) ) . "\n"; } // ---------------------------------------------------------------------- // // PUBLIC FUNCTIONS :: CONTENT - BASIC FUNCTIONS // // ---------------------------------------------------------------------- /** * add page row * * @param string $sContent html content inside * @return string */ public function addRow(string $sContent): string { return $this->addWrapper('div', ['class' => 'row'], $sContent); } /** * add page column * * @param string $sContent html content inside * @param integer $iCols column width; 12 = full width * @param string $sFloat css value for float attribute; default=false * @return string */ public function addCol(string $sContent, int $iCols = 6, string $sFloat = ''): string { return $this->addWrapper('div', ['class' => 'col-md-' . $iCols, 'style' => 'float:' . $sFloat], $sContent); } // ---------------------------------------------------------------------- // // PUBLIC FUNCTIONS :: CONTENT - WIDGET HELPERS // // ---------------------------------------------------------------------- /** * get a list of all defined components that can be rendered * @param bool $bSendData flag: send including subkeys of the hash; default: false (keys only) * @return array */ public function getComponents(bool $bSendData = false): array { return $bSendData ? $this->_aElements : array_keys($this->_aElements) ; } /** * get data of a component * @param string $sComponent id of the component * @return array|bool */ public function getComponent(string $sComponent): array|bool { if (!isset($this->_aElements[$sComponent])) { return false; } $aReturn = array_merge(['id' => $sComponent], $this->_aElements[$sComponent]); unset($aReturn['params']); return $aReturn; } /** * get parameter keys of a component * @param string $sComponent id of the component * @param bool $bSendData flag: send including subkeys of the hash; default: false (keys only) * @return array|bool */ public function getComponentParamkeys(string $sComponent, bool $bSendData = false) { if (!isset($this->_aElements[$sComponent])) { return false; } $aKeys = array_keys($this->_aElements[$sComponent]['params']); if (!$bSendData) { return $aKeys; } $aReturn = []; foreach ($aKeys as $sKey) { $aReturn[$sKey] = $this->getComponentParamkey($sComponent, $sKey); } // $aReturn=$this->_aElements[$sComponent]['params']; return $aReturn; } /** * get information a parameter keys of a component * @param string $sComponent id of the component * @param string $sKey key in the options array * @return array|bool */ public function getComponentParamkey(string $sComponent, string $sKey): array|bool { if (!isset($this->_aElements[$sComponent]['params'][$sKey])) { return false; } $aReturn = $this->_aElements[$sComponent]['params'][$sKey]; // get description from a preset if (!isset($aReturn['description']) && isset($aReturn['select']['description'])) { $aReturn['description'] = $aReturn['select']['description']; } if (!isset($aReturn['group']) && isset($aReturn['select']['group'])) { $aReturn['group'] = $aReturn['select']['group']; } return $aReturn; } /** * get a flat list of valid parameters for a key in a component * @param string $sComponent id of the component * @param string $sKey key in the options array * @return array|bool */ public function getValidParamValues(string $sComponent, string $sKey): array|bool { $aOptionkey = $this->getComponentParamkey($sComponent, $sKey); if (!$aOptionkey || !isset($aOptionkey['select']['values'])) { return false; } return array_keys($aOptionkey['select']['values']); } // ---------------------------------------------------------------------- /** * helper: add a css value with prefix * this handles option keys in get[COMPONENT] methods * if a value is set then this function returns a space + prefix (param 2) + value * @param string $sValue option value * @param string $sPrefix prefix in front of css value * @return string */ protected function _addClassValue(string $sValue, string $sPrefix = ''): string { return $sValue ? ' ' . $sPrefix . $sValue : ''; } /** * helper function for get[COMPONENTNAME] methods: * ensure that all wanted keys exist in an array. Non existing keys will * be added with value false * * @param string $sComponent id of the component * @param array $aOptions hash with keys for all options * @return array */ protected function _ensureOptions(string $sComponent, array $aOptions = []): array { $aParams = $this->getComponentParamkeys($sComponent, 0); if (!$aParams) { $aOptions['_infos'][] = "Warning: no definition was found for component $sComponent."; return $aOptions; } foreach ($aParams as $sKey) { if (!isset($aOptions) || !isset($aOptions[$sKey])) { $aOptions[$sKey] = false; if (!isset($aOptions['_infos'])) { $aOptions['_infos'] = []; } $aOptions['_infos'][] = "added missing key: $sKey"; } // $aParamdata $aValidvalues = $this->getValidParamValues($sComponent, $sKey); if ($aValidvalues) { if (array_search($aOptions[$sKey], $aValidvalues) === false) { echo "ERROR: [" . $sComponent . "] value "" . $aOptions[$sKey] . "" is not a valid for param key [" . $sKey . "]; it must be one of " . implode("|", $aValidvalues) . '<br>'; } } // $this->_checkValue($sKey, $aOptions[$sKey], __METHOD__); } // echo '<pre>' . print_r($aOptions, 1) . '</pre>'; return $aOptions; } // ---------------------------------------------------------------------- // // PUBLIC FUNCTIONS :: CONTENT - WIDGETS // // ---------------------------------------------------------------------- /** * return a alert box * https://adminlte.io/themes/v3/pages/UI/general.html * * @param array $aOptions hash with keys for all options * - type - one of [none]|danger|info|primary|success|warning * - dismissible - if dismissible - one of true|false; default: false * - title * - text * @return string */ public function getAlert(array $aOptions): string { $aOptions = $this->_ensureOptions('alert', $aOptions); $aAlertIcons = [ 'danger' => 'icon fa-solid fa-ban', 'info' => 'icon fa-solid fa-info', 'warning' => 'icon fa-solid fa-exclamation-triangle', 'success' => 'icon fa-solid fa-check', ]; $aElement = [ 'class' => 'alert' . $this->_addClassValue($aOptions['type'], 'alert-') . $this->_addClassValue($aOptions['dismissible'], 'alert-') , 'label' => '' . ($aOptions['dismissible'] ? '<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>' : '') . $this->_tag('h5', [ 'label' => '' . (isset($aAlertIcons[$aOptions['type']]) ? '<i class="' . $aAlertIcons[$aOptions['type']] . '"></i> ' : '') . $aOptions['title'] ]) . $aOptions['text'] ]; return $this->_tag('div', $aElement); } /** * get html code for a badge * * Examples * <span class="badge badge-danger navbar-badge">3</span> * <span title="3 New Messages" class="badge badge-warning">3</span> * <span class="right badge badge-danger">New</span> * <span class="badge badge-danger float-right">$350</span> * * @param array $aOptions hash with keys for all options * - bgcolor - background color (without prefix "bg") * - class - css class * - id - optional: id attribute * - text - visible text * - title - optional: title attribute * - type - one of [none]|danger|dark|info|primary|secondary|success|warning * @return string */ public function getBadge(array $aOptions): string { $aOptions = $this->_ensureOptions('badge', $aOptions); $aElement = []; $aElement['class'] = 'badge' . $this->_addClassValue($aOptions['class'], '') . $this->_addClassValue($aOptions['type'], 'badge-') . $this->_addClassValue($aOptions['bgcolor'], 'bg-') ; if ($aOptions['id']) { $aElement['id'] = $aOptions['id']; } $aElement['title'] = $aOptions['title']; $aElement['label'] = $aOptions['text']; return $this->_tag('span', $aElement); } /** * Get a button. * You can use any other key that are not named here. Those keys will be rendered * as additional html attributes without modification. * https://adminlte.io/themes/v3/pages/UI/buttons.html * * <button type="button" class="btn btn-block btn-default">Default</button> * @param array $aOptions hash with keys for all options * - type - one of [none]|danger|dark|info|primary|secondary|success|warning * - size - one of [none]|lg|sm|xs|flat * - class - any css class for customizing, eg. "disabled" * - icon - not supported yet * - text - text on button * @return string */ public function getButton(array $aOptions): string { $aOptions = $this->_ensureOptions('button', $aOptions); $aElement = $aOptions; $aElement['class'] = 'btn' . $this->_addClassValue($aOptions['type'], 'btn-') . $this->_addClassValue($aOptions['size'], 'btn-') . $this->_addClassValue($aOptions['class'], '') ; $aElement['label'] = $aOptions['text'] ? $aOptions['text'] : ' '; foreach (['_infos', 'type', 'size', 'icon', 'text'] as $sDeleteKey) { unset($aElement[$sDeleteKey]); } return $this->_tag('button', $aElement); } /** * get a callout (box with coloered left border; has type, title + text) * https://adminlte.io/themes/v3/pages/UI/general.html * * @param array $aOptions hash with keys for all options * >> styling * - type - one of [none]|danger|dark|info|primary|secondary|success|warning * - class - optional css class * - shadow - size of shadow; one of [none] (=default: between small and regular)|none|small|regular|large * * >> texts/ html content * - title - text: title of the card * - text - text: content of the card * @return string */ public function getCallout(array $aOptions): string { $aOptions = $this->_ensureOptions('callout', $aOptions); $sClass = 'callout' . $this->_addClassValue($aOptions['type'], 'callout-') . $this->_addClassValue($aOptions['class'], '') . ($aOptions['shadow'] && isset($this->_aValueMappings['shadow'][$aOptions['shadow']]) ? ' ' . $this->_aValueMappings['shadow'][$aOptions['shadow']] : '') ; return $this->addWrapper( 'div', ['class' => $sClass], ($aOptions['title'] ? $this->_tag('h5', ['label' => $aOptions['title']]) : '') . ($aOptions['text'] ? $this->_tag('p', ['label' => $aOptions['text']]) : '') ); } /** * get a card * https://adminlte.io/docs/3.2/components/cards.html * https://adminlte.io/docs/3.2/javascript/card-widget.html * * @param array $aOptions hash with keys for all options * >> styling * - variant: "default" - titlebar is colored * "outline" - a small stripe on top border is colored * "solid" - whole card is colored * "gradient" - whole card is colored with a gradient * - type - one of [none]|danger|dark|info|primary|secondary|success|warning * - shadow - size of shadow; one of [none] (=default: between small and regular)|none|small|regular|large * - class - any css class for customizing, eg. "disabled" * - state - one of [none]|collapsed|maximized * * >> toolbar icons - set to true to show it; default: none of it is visible * - tb-collapse * - tb-expand it is added automatically if you set 'state'=>'collapsed' * - tb-maximize * - tb-minimize it is added automatically if you set 'state'=>'maximized' * - tb-remove * * >> texts/ html content * - title - text: title of the card * - tools - text: titlebar top right elements * - text - text: content of the card * - footer - text: footer of the card * @return string */ public function getCard(array $aOptions): string { $aOptions = $this->_ensureOptions('card', $aOptions); // css class prefixes based on "variant" value $aVariants = [ 'default' => 'card-', 'outline' => 'card-outline card-', 'solid' => 'bg-', 'gradient' => 'bg-gradient-', ]; // window states: css class and toolbar buttons to add $aStates = [ 'collapsed' => ['class' => 'collapsed-card', 'tool' => 'tb-expand'], 'maximized' => ['class' => 'maximized-card', 'tool' => 'tb-minimize'], ]; $aTools = [ 'tb-collapse' => '<button type="button" class="btn btn-tool" data-card-widget="collapse"><i class="fa-solid fa-minus"></i></button>', 'tb-expand' => '<button type="button" class="btn btn-tool" data-card-widget="collapse"><i class="fa-solid fa-plus"></i></button>', 'tb-maximize' => '<button type="button" class="btn btn-tool" data-card-widget="maximize"><i class="fa-solid fa-expand"></i></button>', 'tb-minimize' => '<button type="button" class="btn btn-tool" data-card-widget="maximize"><i class="fa-solid fa-compress"></i></button>', 'tb-remove' => '<button type="button" class="btn btn-tool" data-card-widget="remove"><i class="fa-solid fa-times"></i></button>', ]; // print_r($aOptions); $sVariantPrefix = isset($aVariants[$aOptions['variant']]) ? $aVariants[$aOptions['variant']] : $aVariants['default']; $sClass = 'card' . $this->_addClassValue($aOptions['type'], $sVariantPrefix) . ($aOptions['shadow'] && isset($this->_aValueMappings['shadow'][$aOptions['shadow']]) ? ' ' . $this->_aValueMappings['shadow'][$aOptions['shadow']] : '') . $this->_addClassValue($aOptions['class'], '') ; // check window state foreach ($aStates as $sStatus => $aStatus) { if ($aOptions['state'] === $sStatus) { $sClass .= ' ' . $aStatus['class']; $aOptions[$aStatus['tool']] = 1; } } // add toolbar buttons - from given options or by window state foreach ($aTools as $sTool => $sHtml) { $aOptions['tools'] .= ($aOptions[$sTool] ? $sHtml : ''); } // build parts of the card $sCardHeader = $aOptions['title'] ? $this->addWrapper( 'div', ['class' => 'card-header'], $this->_tag('h3', ['class' => 'card-title', 'label' => $aOptions['title']]) . ($aOptions['tools'] ? $this->_tag('div', ['class' => 'card-tools', 'label' => $aOptions['tools']]) : '') ) : '' ; $sCardBody = $this->_tag('div', ['class' => 'card-body', 'label' => $aOptions['text']]); $sCardFooter = $aOptions['footer'] ? $this->_tag('div', ['class' => 'card-footer', 'label' => $aOptions['footer']]) : ''; // merge all return $this->addWrapper('div', ['class' => $sClass], $sCardHeader . $sCardBody . $sCardFooter); } /** * return an info-box: * A colored box with large icon, text and a value. * https://adminlte.io/docs/3.2/components/boxes.html * * @param array $aOptions hash with keys for all options * styling: * - type - color of the box; one of [none]|danger|dark|info|primary|secondary|success|warning * - iconbg - background color or type of icon; use it for default type (type="") * - shadow - size of shadow; one of [none] (=default: between small and regular)|none|small|regular|large * * content * - icon - icon class * - text - information text * - number - value (comes in bold text) * - progressvalue - integer: progress bar; range: 0..100 * - progresstext - text below progress bar * @return string */ public function getInfobox(array $aOptions): string { $aOptions = $this->_ensureOptions('infobox', $aOptions); // print_r($aOptions); $sClass = 'info-box' . $this->_addClassValue($aOptions['type'], 'bg-') . $this->_addClassValue($aOptions['class'], '') . ($aOptions['shadow'] && isset($this->_aValueMappings['shadow'][$aOptions['shadow']]) ? ' ' . $this->_aValueMappings['shadow'][$aOptions['shadow']] : '') ; // build parts $sIcon = $aOptions['icon'] ? $this->addWrapper("span", [ 'class' => 'info-box-icon' . ($aOptions['iconbg'] ? ' bg-' . $aOptions['iconbg'] : '') ], $this->_tag('i', ['class' => $aOptions['icon']])) : '' ; $sContent = $this->addWrapper( "div", ['class' => 'info-box-content'], '' . ($aOptions['text'] ? $this->_tag('span', ['class' => 'info-box-text', 'label' => $aOptions['text']]) : '') . ($aOptions['number'] ? $this->_tag('span', ['class' => 'info-box-number', 'label' => $aOptions['number']]) : '') . ($aOptions['progressvalue'] !== false && $aOptions['progressvalue'] !== '' ? $this->addWrapper( 'div', ['class' => 'progress'], $this->_tag('div', ['class' => 'progress-bar' . ($aOptions['iconbg'] ? ' bg-' . $aOptions['iconbg'] : ''), 'style' => 'width: ' . (int) $aOptions['progressvalue'] . '%']) ) . ($aOptions['progresstext'] ? $this->_tag('span', ['class' => 'progress-description', 'label' => $aOptions['progresstext']]) : '') : '' ) ); // merge all return $this->_tag('div', ['class' => $sClass], $sIcon . $sContent); } /** * return a small box: * A colored box with large icon, text and a value. * https://adminlte.io/docs/3.2/components/boxes.html * https://adminlte.io/themes/v3/pages/widgets.html * * @param array $aOptions hash with keys for all options * styling: * - type - color of the box; one of [none]|danger|dark|info|primary|secondary|success|warning * - shadow - size of shadow; one of [none] (=default: between small and regular)|none|small|regular|large * content * - icon - icon class for icon on the right * - text - information text * - number - value (comes in bold text) * - url - integer: progress bar; range: 0..100 * - linktext- text below progress bar * @return string */ public function getSmallbox(array $aOptions): string { $aOptions = $this->_ensureOptions('smallbox', $aOptions); // print_r($aOptions); $sClass = 'small-box' . $this->_addClassValue($aOptions['type'], 'bg-') . $this->_addClassValue($aOptions['class'], '') . ($aOptions['shadow'] && isset($this->_aValueMappings['shadow'][$aOptions['shadow']]) ? ' ' . $this->_aValueMappings['shadow'][$aOptions['shadow']] : '') ; // build parts $sContent = $this->addWrapper( "div", ['class' => 'inner'], '' . ($aOptions['number'] ? $this->_tag('h3', ['label' => $aOptions['number']]) : '') . ($aOptions['text'] ? $this->_tag('p', ['class' => 'info-box-text', 'label' => $aOptions['text']]) : '') ); $sIcon = $aOptions['icon'] ? $this->addWrapper( "div", ['class' => 'icon'], $this->_tag('i', ['class' => $aOptions['icon']]) ) : '' ; $sFooter = ($aOptions['url'] ? $this->addWrapper( "a", [ 'class' => 'small-box-footer', 'href' => $aOptions['url'], ], '' . ($aOptions['linktext'] ? $aOptions['linktext'] : $aOptions['url']) . ' ' . $this->_tag('i', ['class' => 'fa-solid fa-arrow-circle-right']) ) : '' ); // merge all return $this->_tag('div', ['class' => $sClass], $sContent . $sIcon . $sFooter); } // ---------------------------------------------------------------------- // // PUBLIC FUNCTIONS :: CONTENT - FORM // // ---------------------------------------------------------------------- /** * Generates a horizontal form element with a label, input, and optional hint. * * @param string $sInput The HTML input element to be rendered. * @param string $sLabel The label for the input element. * @param string $sId The ID attribute for the label and input elements. * @param string $sHint An optional hint to be displayed below the input element. * @return string The generated HTML for the horizontal form element. */ public function getHorizontalFormElement(string $sInput, string $sLabel = '', string $sId = '', string $sHint=''): string { return '<div class="form-group row">' . '<label for="' . $sId . '" class="col-sm-2 col-form-label">' . $sLabel . '</label>' . '<div class="col-sm-10">' . ($sHint ? '<div class="text-navy hint">' . $sHint . '</div>' : '') . $sInput . '</div>' . '</div>' ; } /** * return a text input field: * https://adminlte.io/themes/v3/pages/forms/general.html * * @param array $aOptions hash with keys for all options * styling: * - type - field type: text, email, password, hidden and all other html 5 input types * content * - label - label tag * - name - name attribute for sending form * - value - value in field * more: * - hint - hint to be displayed above the field * If not set, no hint is displayed. * css for ".row .hint" to customize look and feel * @return string */ public function GetFormInput(array $aOptions): string { // $aOptions=$this->_ensureOptions('input', $aOptions); $aElement = $aOptions; $aElement['class'] = '' . 'form-control ' . (isset($aOptions['class']) ? $aOptions['class'] : '') ; $sFormid = (isset($aOptions['id']) ? $aOptions['id'] : (isset($aOptions['name']) ? $aOptions['name'] : 'field') . '-' . md5(microtime(true)) ); $aElement['id'] = $sFormid; $sLabel = isset($aOptions['label']) ? $aOptions['label'] : ''; $sHint = isset($aOptions['hint']) ? $aOptions['hint'] : ''; $sPrepend = ''; $sAppend = ''; if (isset($aOptions['prepend']) && $aOptions['prepend']) { $sWrapperclass = 'input-group'; $sPrepend = $this->_tag( 'div', ['class' => 'input-group-prepend'], $this->_tag('span', ['class' => 'input-group-text'], $aOptions['prepend']) ); } if (isset($aOptions['append']) && $aOptions['append']) { $sWrapperclass = 'input-group'; $sAppend = $this->_tag( 'div', ['class' => 'input-group-append'], $this->_tag('span', ['class' => 'input-group-text'], $aOptions['append']) ); } foreach (['_infos', 'label', 'append', 'prepend', 'debug'] as $sDeleteKey) { if (isset($aElement[$sDeleteKey])) { unset($aElement[$sDeleteKey]); } } // return data switch ($aElement['type']) { case 'checkbox': case 'radio': $aElement['class'] = str_replace('form-control ', 'form-check-input', $aElement['class']); $aElement['title'] = $aElement['title'] ?? $sHint; return $this->_tag( 'div', ['class' => 'form-check'], $this->_tag('input', $aElement, '', false) . $this->_tag('label', ['for' => $sFormid, 'label' => $sLabel], '') ); break; case 'hidden': $aElement['title'] = $aElement['title'] ?? $sHint; return $this->_tag('input', $aElement, '', false); break; default: return $this->getHorizontalFormElement( $sPrepend . $this->_tag('input', $aElement, '', false) . $sAppend, $sLabel, $sFormid, $sHint ); } } /** * return a textarea field .. or html editor using summernote * @param array $aOptions hash with keys for all options * styling: * - type - field type: [none]|html * content * - label - label tag * - name - name attribute for sending form * - value - value in * more: * - hint - hint to be displayed above the field * If not set, no hint is displayed. * css for ".row .hint" to customize look and feel * @return string */ public function getFormTextarea(array $aOptions): string { // $aOptions=$this->_ensureOptions('textarea', $aOptions); $aElement = $aOptions; $aElement['class'] = '' . 'form-control ' . ((isset($aOptions['type']) && $aOptions['type'] == 'html') ? 'summernote ' : '') . (isset($aOptions['class']) ? $aOptions['class'] : '') ; $sFormid = (isset($aOptions['id']) ? $aOptions['id'] : (isset($aOptions['name']) ? $aOptions['name'] : 'field') . '-' . md5(microtime(true)) ); $sLabel = isset($aOptions['label']) ? $aOptions['label'] : ''; $sHint = isset($aOptions['hint']) ? $aOptions['hint'] : ''; $aElement['id'] = $sFormid; $value = isset($aOptions['value']) ? $aOptions['value'] : ''; foreach (['_infos', 'label', 'debug', 'type', 'value'] as $sDeleteKey) { if (isset($aElement[$sDeleteKey])) { unset($aElement[$sDeleteKey]); } } return $this->getHorizontalFormElement( $this->_tag('textarea', $aElement, $value), $sLabel, $sFormid, $sHint ); } /** * return a select box field * @param array $aOptions hash with keys for all options * option fields * - options - array of options with keys per item: * - value - value in the option * - label - visible text in the option * other keys are attributes in the option * styling: * - bootstrap-select - set true to enable select * box with bootstrap-select and * live search * - class - css class * select tag * - label - label tag * - name - name attribute for sending form * other keys are attributes in the select * more: * - hint - hint to be displayed above the field * If not set, no hint is displayed. * css for ".row .hint" to customize look and feel * @return string */ public function getFormSelect(array $aOptions): string { $aElement = $aOptions; $aElement['class'] = '' . 'form-control ' . (isset($aOptions['class']) ? $aOptions['class'] . ' ' : '') . (isset($aOptions['bootstrap-select']) ? 'selectpicker ' : '') //$aOptions ; if (isset($aOptions['bootstrap-select']) && $aOptions['bootstrap-select']) { $aElement['data-live-search'] = "true"; } $sFormid = (isset($aOptions['id']) ? $aOptions['id'] : (isset($aOptions['name']) ? $aOptions['name'] : 'field') . '-' . md5(microtime(true)) ); $aElement['id'] = $sFormid; $sLabel = isset($aOptions['label']) ? $aOptions['label'] : ''; $sHint = isset($aOptions['hint']) ? $aOptions['hint'] : ''; $sOptionTags = ''; foreach ($aOptions['options'] as $aField) { $optionText = $aField['label']; unset($aField['label']); $sOptionTags .= $this->_tag('option', $aField, $optionText) . "\n"; } foreach (['_infos', 'label', 'debug', 'type', 'value', 'options'] as $sDeleteKey) { if (isset($aElement[$sDeleteKey])) { unset($aElement[$sDeleteKey]); } } return $this->getHorizontalFormElement( $this->_tag( 'div', ['class' => 'form-group'], $this->_tag('select', $aElement, $sOptionTags) ), $sLabel, $sFormid, $sHint ); } // ---------------------------------------------------------------------- // // PUBLIC FUNCTIONS :: CONTENT - TABBED CONTENT // // ---------------------------------------------------------------------- /** * return a box with tabbed content * @param array $aOptions hash with keys for all options * - tabs {array} key=tab label; value=content * @param bool $asArray optional flag: return hash with keys or as string * @retunr bool|string|array */ public function getTabbedContent(array $aOptions, bool $asArray = false): bool|string|array { static $iTabCounter; if (!isset($aOptions['tabs']) || !is_array($aOptions['tabs'])) { return false; } if (!isset($iTabCounter)) { $iTabCounter = 1; } else { $iTabCounter++; } $id = 'tab-content-' . $iTabCounter; $iCounter = 0; $sTabs = ''; $sContent = ''; foreach ($aOptions['tabs'] as $sLabel => $sTabContent) { $iCounter++; $sTabId = $id . '-tabitem-' . $iCounter . '-tab'; $sContentId = $id . '-tabitem-' . $iCounter . '-content'; $sTabs .= $this->_tag( 'li', ['class' => 'nav-item'], $this->_tag( 'a', [ 'class' => 'nav-link' . ($iCounter == 1 ? ' active' : ''), 'id' => $sTabId, 'data-toggle' => 'tab', 'href' => '#' . $sContentId, 'role' => 'tab', 'aria-controls' => 'custom-tabs-one-profile', 'aria-selected' => ($iCounter == 1 ? true : false), ], $sLabel ) ); $sContent .= $this->_tag('div', [ 'class' => 'tab-pane fade' . ($iCounter == 1 ? ' active show' : ''), 'id' => $sContentId, 'role' => 'tabpanel', 'aria-labelledby' => $sTabId, ], $sTabContent); } $sTabs = $this->_tag('ul', ['class' => 'nav nav-tabs', 'role' => 'tablist'], $sTabs); $sContent = $this->_tag('div', ['class' => 'tab-content'], $sContent); return $asArray ? ['tabs' => $sTabs, 'content' => $sContent] : $sTabs . $sContent ; } }