Rev 22 | Blame | Compare with Previous | Last modification | View Log | Download
<?php namespace ProcessWire;/*** AdminThemeUikit** @property bool $isSuperuser Is current user a superuser?* @property bool $isEditor Does current user have page-edit permission?* @property bool $isLoggedIn Is current user logged in?* @property bool $useOffset Use offset/margin for all Inputfields?* @property array $noBorderTypes Inputfield class names that should always use the noBorder option (when 100% width).* @property array $cardTypes Inputfield class names that should always use the card option.* @property array $offsetTypes Inputfield class names that should always use the offset option.* @property string $logoURL URL to custom logo, relative to PW installation root.* @property string $cssURL URL to custom CSS file, relative to PW installation root.* @property string $layout Layout type (blank=default, sidenav=multi-pane, sidenav-tree=left-tree, sidenav-tree-alt=right-tree)* @property int $logoAction Logo click action (0=admin root page list, 1=offcanvas nav)* @property string $userLabel Text containing user {vars} to use for user label in masthead (default="{Name}")* @property int $maxWidth Maximum layout width in pixels, or 0 for no max (default=1600).* @property bool|int $groupNotices Whether or not notices should be grouped by type* @property string $inputSize Size for input/select elements. One of "s" for small, "m" for medium (default), or "l" for large.* @property bool|int $ukGrid When true, use uk-width classes for Inputfields (rather than CSS percentages).* @property int $toggleBehavior (0=Standard, 1=Consistent)* @property string $configPhpHash Hash used internally to detect changes to $config->AdminThemeUikit settings.* @property int $cssVersion Current version number of core CSS/LESS files** @method string renderBreadcrumbs()* @method string getUikitCSS()***/class AdminThemeUikit extends AdminThemeFramework implements Module, ConfigurableModule {public static function getModuleInfo() {return array('title' => 'Uikit','version' => 33,'summary' => 'Uikit v3 admin theme','autoload' => 'template=admin',);}/*** Development mode, to be used when developing this module’s code** Makes it use runtime/temporary compiled CSS files rather than the final ones.**/const dev = false;/*** Set to true when upgrading Uikit version**/const upgrade = false;/*** Required CSS/LESS files version** Increment on core less file changes that will also require a recompile of /site/assets/admin.css**/const requireCssVersion = 1;/*** Default logo image file (relative to this dir)**/const logo = 'uikit-pw/images/pw-mark.png';/*** sidenavType: primary navigation on left sidebar**/const sidenavTypePrimary = 0;/*** sidenavType: tree navigation on left sidebar**/const sidenavTypeTree = 1;/*** Construct and establish default module config settings**/public function __construct() {parent::__construct();$this->set('useOffset', false);$this->set('cardTypes', array());$this->set('offsetTypes', array());$this->set('logoURL', '');$this->set('cssURL', '');$this->set('layout', '');$this->set('noBorderTypes', array()); // 'InputfieldCKEditor' is a good one for this$this->set('logoAction', 0);$this->set('toggleBehavior', 0);$this->set('userLabel', '{Name}');$this->set('userAvatar', 'icon.user-circle');$this->set('maxWidth', 1600);$this->set('groupNotices', true);$this->set('inputSize', 'm'); // m=medium (default), s=small, l=large$this->set('ukGrid', false);$this->set('configPhpHash', '');$this->set('cssVersion', 0);$this->setClasses(array('input' => 'uk-input','input-small' => 'uk-input uk-form-small','input-checkbox' => 'uk-checkbox','input-radio' => 'uk-radio','input-password' => 'uk-input uk-form-width-medium','select' => 'uk-select','select-asm' => 'uk-select uk-form-small','select-small' => 'uk-select uk-form-small','textarea' => 'uk-textarea','table' => 'uk-table uk-table-divider uk-table-justify uk-table-small','dl' => 'uk-description-list uk-description-list-divider',));}public function wired() {parent::wired();$this->addHookAfter('InputfieldSelector::ajaxReady', $this, 'hookInputfieldSelectorAjax');}/*** Initialize and attach hooks**/public function init() {parent::init();// if this is not the current admin theme, exit now so no hooks are attachedif(!$this->isCurrent()) return;/** @var Page $page */$page = $this->wire('page');/** @var Modules $modules */$modules = $this->wire('modules');/** @var Modules $modules */$session = $this->wire('session');$sidenav = strpos($this->layout, 'sidenav') === 0;// disable sidebar layout if SystemNotifications is activeif($sidenav && $modules->isInstalled('SystemNotifications')) {/** @var SystemNotifications $systemNotifications */$systemNotifications = $modules->get('SystemNotifications');if(!$systemNotifications->disabled) {$this->layout = '';$sidenav = false;}}if(!$page || $page->template != 'admin') {// front-endif($sidenav) {// ensure that page edit links on front-end load the sidenav-init$session->setFor('Page', 'appendEditUrl', "&layout=sidenav-init");}return;}$inputSize = $this->get('inputSize');if($inputSize && $inputSize != 'm') {$inputClass = $inputSize === 'l' ? 'uk-form-large' : 'uk-form-small';foreach(array('input', 'select', 'textarea') as $name) {$this->addClass($name, $inputClass);}}if(!$this->ukGrid) {$this->addClass('body', 'AdminThemeUikitNoGrid');}if($this->className() !== 'AdminThemeUikit') {$this->addBodyClass('AdminThemeUikit');}$session->removeFor('Page', 'appendEditUrl');/** @var JqueryUI $jqueryUI */$jqueryUI = $modules->get('JqueryUI');$jqueryUI->use('panel');// add rendering hooks$this->addHookBefore('Inputfield::render', $this, 'hookBeforeRenderInputfield');$this->addHookBefore('Inputfield::renderValue', $this, 'hookBeforeRenderInputfield');$this->addHookAfter('Inputfield::getConfigInputfields', $this, 'hookAfterInputfieldGetConfigInputfields');$this->addHookAfter('Inputfield::getConfigAllowContext', $this, 'hookAfterInputfieldGetConfigAllowContext');$this->addHookAfter('MarkupAdminDataTable::render', $this, 'hookAfterTableRender');// hooks and settings specific to sidebar layoutsif($sidenav) {$this->addHookAfter('ProcessLogin::afterLoginURL', $this, 'hookAfterLoginURL');if(strpos($this->layout, 'sidenav-tree') === 0) {// page-edit breadcrumbs go to page editor when page tree is always in sidebar$this->wire('config')->pageEdit('editCrumbs', true);}}// add cache clearing hooks$this->wire('pages')->addHookAfter('saved', $this, 'hookClearCaches');$modules->addHookAfter('refresh', $this, 'hookClearCaches');}/*** Render an extra markup region** @param string $for* @return mixed|string**/public function renderExtraMarkup($for) {$out = parent::renderExtraMarkup($for);if($for === 'notices') {}return $out;}/*** Test all notice types** @return bool**/public function testNotices() {if(parent::testNotices()) {$v = $this->wire('input')->get('test_notices');if($v === 'group-off') $this->groupNotices = false;if($v === 'group-on') $this->groupNotices = true;return true;}return false;}/*** Get Uikit uk-width-* class for given column width** @param int $columnWidth* @param array $widths* @return string**/protected function getUkWidthClass($columnWidth, array $widths) {static $minColumnWidth = null;$ukWidthClass = '1-1';if($minColumnWidth === null) {$widthKeys = array_keys($widths);sort($widthKeys, SORT_NATURAL);$minColumnWidth = (int) reset($widthKeys);}if($columnWidth < 10) {// use uk-width-1-1} else if($columnWidth && $columnWidth < 100) {if($columnWidth < $minColumnWidth) $columnWidth = $minColumnWidth;// determine column width classforeach($widths as $pct => $uk) {$pct = (int) $pct;if($columnWidth >= $pct) {$ukWidthClass = $uk;break;}}}if($ukWidthClass === '1-1') {return "uk-width-1-1";} else {return "uk-width-$ukWidthClass@m";}}/******************************************************************************************** HOOKS**//*** Hook called before each Inputfield::render** This updates the Inputfield classes and settings for Uikit.** - themeBorder: none, hide, card, line* - themeOffset: s, m, l* - themeInputSize: s, m, l* - themeInputWidth: xs, s, m, l, f* - themeColor: primary, secondary, warning, danger, success, highlight, none* - themeBlank: no input borders, true or false** @param HookEvent $event**/public function hookBeforeRenderInputfield(HookEvent $event) {/** @var Inputfield $inputfield */$inputfield = $event->object;$class = $inputfield->className();$formSettings = $event->wire('config')->get('InputfieldForm');$widths = $formSettings['ukGridWidths'];$columnWidth = (int) $inputfield->getSetting('columnWidth');$field = $inputfield->hasField;$isFieldset = $inputfield instanceof InputfieldFieldset;$isMarkup = $inputfield instanceof InputfieldMarkup;$isWrapper = $inputfield instanceof InputfieldWrapper && !$isFieldset && !$isMarkup;$ukWidthClass = 'uk-width-1-1';$globalInputSize = $this->get('inputSize');$ukGrid = $this->get('ukGrid');$themeColor = '';$themeBorder = '';$themeOffset = '';$themeInputSize = '';$themeInputWidth = '';$themeBlank = false;$wrapClasses = array();$inputClasses = array();$removeInputClasses = array();if($inputfield instanceof InputfieldForm) {if($globalInputSize == 's') {$inputfield->addClass('InputfieldFormSmallInputs');} else if($globalInputSize == 'l') {$inputfield->addClass('InputfieldFormLargeInputs');}return;} else if($inputfield instanceof InputfieldSubmit) {// button$inputfield->addClass('uk-width-auto uk-margin-top', 'wrapClass');return; // no further settings needed for button}if($ukGrid) {$ukWidthClass = $this->getUkWidthClass($columnWidth, $widths);if($ukWidthClass) $wrapClasses[] = $ukWidthClass;}if($isWrapper) {if($ukWidthClass != 'uk-width-1-1') $inputfield->addClass($ukWidthClass, 'wrapClass');return;} else if($inputfield instanceof InputfieldTextarea) {$inputClasses[] = $this->getClass('textarea');} else if($inputfield instanceof InputfieldPassword) {$inputClasses[] = $this->getClass('input-password');} else if($inputfield instanceof InputfieldText) {$inputClasses[] = $this->getClass('input');} else if($inputfield instanceof InputfieldInteger) {$inputClasses[] = $this->getClass('input');} else if($inputfield instanceof InputfieldDatetime) {$inputClasses[] = $this->getClass('input');} else if($inputfield instanceof InputfieldCheckboxes || $inputfield instanceof InputfieldCheckbox) {$inputClasses[] = $this->getClass('input-checkbox');$inputfield->addClass('uk-form-controls-text', 'contentClass');} else if($inputfield instanceof InputfieldRadios) {$inputClasses[] = $this->getClass('input-radio');$inputfield->addClass('uk-form-controls-text', 'contentClass');} else if($inputfield instanceof InputfieldAsmSelect) {$inputClasses[] = $this->getClass('select-asm');} else if($inputfield instanceof InputfieldSelect && !$inputfield instanceof InputfieldHasArrayValue) {$inputClasses[] = $this->getClass('select');} else if($inputfield instanceof InputfieldFile) {$themeColor = 'secondary';}if($field) {// pull optional uikit settings from Field object$themeBorder = $field->get('themeBorder');$themeOffset = $field->get('themeOffset');$themeInputSize = $field->get('themeInputSize');$themeInputWidth = $field->get('themeInputWidth');$themeColor = $field->get('themeColor') ? $field->get('themeColor') : $themeColor;$themeBlank = $field->get('themeBlank');}// determine custom settings which may be defined with Inputfieldif(!$themeBorder) $themeBorder = $inputfield->getSetting('themeBorder');if(!$themeOffset) $themeOffset = $inputfield->getSetting('themeOffset'); // || in_array($class, $this->offsetTypes);if(!$themeColor) $themeColor = $inputfield->getSetting('themeColor');if(!$themeInputSize) $themeInputSize = $inputfield->getSetting('themeInputSize');if(!$themeInputWidth) $themeInputWidth = $inputfield->getSetting('themeInputWidth');if(!$themeBlank) $themeBlank = $inputfield->getSetting('themeBlank');if(!$themeBorder) {if($formSettings['useBorders'] === false || in_array($class, $this->noBorderTypes)) {$themeBorder = (!$columnWidth || $columnWidth == 100) ? 'none' : 'hide';} else if(in_array($class, $this->cardTypes)) {$themeBorder = 'card';} else {$themeBorder = 'line';}}if($themeInputSize && $globalInputSize != $themeInputSize) {if($globalInputSize === 's') {$removeInputClasses[] = 'uk-form-small';} else if($globalInputSize === 'l') {$removeInputClasses[] = 'uk-form-large';}if($themeInputSize === 'm') {$inputClasses[] = 'uk-form-medium';} else if($themeInputSize === 's') {$inputClasses[] = 'uk-form-small';} else if($themeInputSize === 'l') {$inputClasses[] = 'uk-form-large';}}if($themeInputWidth) {$inputWidthClasses = array('xs' => 'uk-form-width-xsmall','s' => 'uk-form-width-small','m' => 'uk-form-width-medium','l' => 'uk-form-width-large','f' => 'InputfieldMaxWidth',);$inputfield->removeClass($inputWidthClasses);if(isset($inputWidthClasses[$themeInputWidth])) {$inputClasses[] = $inputWidthClasses[$themeInputWidth];if($themeInputWidth != 'f') $inputClasses[] = 'InputfieldSetWidth';}}if($themeBlank) {$inputClasses[] = 'uk-form-blank';}if($themeColor) {$wrapClasses[] = 'InputfieldIsColor';}switch($themeColor) {case 'primary': $wrapClasses[] = 'InputfieldIsPrimary'; break;case 'secondary': $wrapClasses[] = 'InputfieldIsSecondary'; break;case 'warning': $wrapClasses[] = 'InputfieldIsWarning'; break;case 'danger': $wrapClasses[] = 'InputfieldIsError'; break;case 'success': $wrapClasses[] = 'InputfieldIsSuccess'; break;case 'highlight': $wrapClasses[] = 'InputfieldIsHighlight'; break;case 'none': break;}switch($themeBorder) {case 'none': $wrapClasses[] = 'InputfieldNoBorder'; break;case 'hide': $wrapClasses[] = 'InputfieldHideBorder'; break;case 'card': $wrapClasses[] = 'uk-card uk-card-default'; break;}if($themeOffset && $themeOffset !== 'none') {$wrapClasses[] = 'InputfieldIsOffset';if($themeOffset === 's') {$wrapClasses[] = 'InputfieldIsOffsetSm';} else if($themeOffset === 'l') {$wrapClasses[] = 'InputfieldIsOffsetLg';}}if(count($inputClasses)) {$inputfield->addClass(implode(' ', $inputClasses));}if(count($removeInputClasses)) {$inputfield->removeClass($removeInputClasses);}if(count($wrapClasses)) {$inputfield->addClass(implode(' ', $wrapClasses), 'wrapClass');}}/*** Hook after Inputfield::getConfigInputfields() to add theme-specific configuration settings** @param HookEvent $event**/public function hookAfterInputfieldGetConfigInputfields(HookEvent $event) {/** @var Inputfield $inputfield */$inputfield = $event->object;if($inputfield instanceof InputfieldWrapper) return;/** @var InputfieldWrapper $inputfields */$inputfields = $event->return;if(!$inputfields instanceof InputfieldWrapper) return;include_once(dirname(__FILE__) . '/config.php');$configHelper = new AdminThemeUikitConfigHelper($this);$configHelper->configInputfield($inputfield, $inputfields);}/*** Get fields allowed for field/template context configuration** @param HookEvent $event**/public function hookAfterInputfieldGetConfigAllowContext(HookEvent $event) {$names = $event->return;$names[] = '_adminTheme';$names[] = 'themeOffset';$names[] = 'themeBorder';$names[] = 'themeColor';$names[] = 'themeInputSize';$names[] = 'themeInputWidth';$names[] = 'themeBlank';$event->return = $names;}/*** Hook after MarkupAdminDataTable::render** This is primarily to add support for Uikit horizontal scrolling responsive tables,* which is used instead of the default MarkupAdminDataTable responsive table.** @param HookEvent $event**/public function hookAfterTableRender(HookEvent $event) {/** @var MarkupAdminDataTable $table */$table = $event->object;$classes = array();if($table->responsive) {$classes[] = 'pw-table-responsive';if(!wireInstanceOf($this->wire()->process, array('ProcessPageLister', 'ProcessUser'))) {$classes[] = 'uk-overflow-auto';}}if($table->sortable) {$classes[] = 'pw-table-sortable';}if($table->resizable) {$classes[] = 'pw-table-resizable';}if(count($classes)) {$class = implode(' ', $classes);$event->return = "<div class='$class'>$event->return</div>";}}/*** Event called when a page is saved or modules refreshed to clear caches** @param HookEvent $event**/public function hookClearCaches(HookEvent $event) {/** @var Page|User|null $page */$page = $event->arguments(0);/** @var array $changes */$changes = $event->arguments(1);/** @var User $user */$user = $this->wire('user');if($page !== null && !($page instanceof Page)) return;if(!is_array($changes)) $changes = array();if($page === null || $page->template == 'admin' || ($page->id === $user->id && in_array('language', $changes))) {/** @var Session $session */$session = $this->wire('session');$session->removeFor($this, 'prnav');$session->removeFor($this, 'sidenav');$session->message("Cleared the admin theme navigation cache (primary nav)", Notice::debug);}}/*** Hook to ProcessLogin::afterLoginURL()** @param HookEvent $event**/public function hookAfterLoginURL(HookEvent $event) {$layout = $this->layout;if(!$layout) return;$url = $event->return;$url .= (strpos($url, '?') !== false ? '&' : '?') . "layout=$this->layout-init";$event->return = $url;}/******************************************************************************************** MARKUP RENDERING METHODS**//*** Render a list of breadcrumbs (list items), excluding the containing <ul>** @return string**/public function ___renderBreadcrumbs() {if(!$this->isLoggedIn || $this->isModal) return '';$process = $this->wire('page')->process;if($process == 'ProcessPageList') return '';$breadcrumbs = $this->wire('breadcrumbs');$out = '';// don't show breadcrumbs if only one of them (subjective)if(count($breadcrumbs) < 2 && $process != 'ProcessPageEdit') return '';if(strpos($this->layout, 'sidenav') === false) {$out = "<li>" . $this->renderQuickTreeLink() . "</li>";}foreach($breadcrumbs as $breadcrumb) {$title = $breadcrumb->get('titleMarkup');if(!$title) $title = $this->wire('sanitizer')->entities1($this->_($breadcrumb->title));$out .= "<li><a href='$breadcrumb->url'>$title</a></li>";}if($out) $out = "<ul class='uk-breadcrumb'>$out</ul>";return $out;}/*** Render the populated “Add New” head button, or blank when not applicable** @return string**/public function renderAddNewButton() {$items = array();foreach($this->getAddNewActions() as $item) {$icon = $this->renderNavIcon($item['icon']);$items[] = "<li><a href='$item[url]'>$icon$item[label]</a></li>";}if(!count($items)) return '';$out = implode('', $items);$label = $this->getAddNewLabel();$icon = $this->renderIcon('angle-down');$out ="<button class='ui-button pw-dropdown-toggle'>$label $icon</button>" ."<ul class='pw-dropdown-menu' data-at='right bottom+1'>$out</ul>";return $out;}/*** Render runtime notices div#notices** @param Notices|bool $notices* @param array $options See defaults in method* @return string|array**/public function renderNotices($notices, array $options = array()) {$defaults = array('groupByType' => $this->groupNotices ? true : false,'messageClass' => 'NoticeMessage uk-alert uk-alert-primary', // class for messages'messageIcon' => 'check-square', // default icon to show with notices'warningClass' => 'NoticeWarning uk-alert uk-alert-warning', // class for warnings'warningIcon' => 'exclamation-circle', // icon for warnings'errorClass' => 'NoticeError uk-alert uk-alert-danger', // class for errors'errorIcon' => 'exclamation-triangle', // icon for errors'debugClass' => 'NoticeDebug uk-alert', // class for debug items (appended)'debugIcon' => 'bug', // icon for debug notices'closeClass' => 'pw-notice-remove notice-remove', // class for close notices link <a>'closeIcon' => 'times', // icon for close notices link'listMarkup' => "<ul class='pw-notices' id='notices'>{out}</ul><!--/notices-->",'itemMarkup' =>"<li class='{class}'>" ."<div class='pw-container uk-container uk-container-expand'>{remove}{icon}{text}</div>" ."</li>");$options = array_merge($defaults, $options);return parent::renderNotices($notices, $options);}/*** Render a single top navigation item for the given page** This function designed primarily to be called by the renderPrimaryNavItems() function.** @param array $item* @return string**/protected function renderPrimaryNavItem(array $item) {$title = $item['title'];$out = "<li class='page-$item[id]-'>";if(!count($item['children'])) {$out .= "<a href='$item[url]'>$title</a></li>";return $out;}$out .="<a href='$item[url]' " ."id='prnav-page-$item[id]' " ."data-from='prnav-page-$item[parent_id]' " ."class='pw-dropdown-toggle'>" ."$title</a>";$my = 'left-1 top';if(in_array($item['name'], array('access', 'page', 'module'))) $my = 'left top';$out .="<ul class='pw-dropdown-menu prnav' data-my='$my' data-at='left bottom'>" .$this->renderPrimaryNavItemChildren($item['children']) ."</ul>" ."</li>";return $out;}/*** Renders <li> items navigation from given nav array** @param array $items* @return string**/protected function renderPrimaryNavItemChildren(array $items) {$out = '';foreach($items as $item) {$icon = empty($item['icon']) ? '' : $this->renderNavIcon($item['icon']);$title = $item['title'];$out .= "<li class='page-$item[id]-'>";if(!empty($item['children'])) {$out .="<a class='pw-has-items' data-from='prnav-page-$item[parent_id]' href='$item[url]'>$icon$title</a>" ."<ul>" . $this->renderPrimaryNavItemChildren($item['children']) . "</ul>";} else if(!empty($item['navJSON'])) {$item['navJSON'] = $this->wire('sanitizer')->entities($item['navJSON']);$out .="<a class='pw-has-items pw-has-ajax-items' " ."data-from='prnav-page-$item[parent_id]' " ."data-json='$item[navJSON]' " ."href='$item[url]'>$icon$title" ."</a>" ."<ul></ul>";} else {$out .= "<a href='$item[url]'>$icon$title</a>";}}$out .= "</li>";return $out;}/*** Render all top navigation items, ready to populate in ul#prnav** @return string**/public function renderPrimaryNavItems() {$cache = self::dev ? '' : $this->wire('session')->getFor($this, 'prnav');if($cache) {$this->markCurrentNavItems($cache);return $cache;}$out = '';$items = $this->getPrimaryNavArray();foreach($items as $item) {$out .= $this->renderPrimaryNavItem($item);}if(!self::dev) $this->wire('session')->setFor($this, 'prnav', $out);$this->markCurrentNavItems($out);return $out;}/*** Render sidebar navigation that uses uk-nav** The contents is the same as the Primary nav, except that output is prepared for sidebar.* This method uses a session-cached version. To clear the cache, logout then log back in.** @return string**/public function renderSidebarNavItems() {// see if we can get it from the cache$out = self::dev ? '' : $this->wire('session')->getFor($this, 'sidenav');if(empty($out)) {$out = $this->renderSidebarNavCache();$this->wire('session')->setFor($this, 'sidenav', $out);}$out = str_replace('<!--pw-user-nav-label-->', $this->renderUserNavLabel(), $out);$this->markCurrentNavItems($out);return $out;}/*** Re-renders the sidebar nav to be cached** @return string**/protected function renderSidebarNavCache() {$out = '';$items = $this->getPrimaryNavArray();$ulAttrs = "class='uk-nav-sub uk-nav-default uk-nav-parent-icon' data-uk-nav='animation: false; multiple: true;'";foreach($items as $item) {$class = "page-$item[id]-";$subnav = '';foreach($item['children'] as $child) {$icon = $child['icon'] ? $this->renderNavIcon($child['icon']) : '';$childClass = "page-$child[id]-";$childAttr = "";$childNav = '';if(count($child['children'])) {$childClass .= ' uk-parent';$childNavList = $this->renderPrimaryNavItemChildren($child['children']);$childIcon = $this->renderNavIcon('arrow-circle-right');$childNav ="<ul $ulAttrs>" ."<li class='pw-nav-dup'><a href='$child[url]'>$childIcon$child[title]</a></li>" .$childNavList ."</ul>";} else if($child['navJSON']) {$child['navJSON'] = $this->wire('sanitizer')->entities($child['navJSON']);$childClass .= ' uk-parent';$childAttr = " class='pw-has-items pw-has-ajax-items' data-json='$child[navJSON]'";$childNav = "<ul $ulAttrs></ul>";}$subnav .= "<li class='$childClass'><a$childAttr href='$child[url]'>$icon$child[title]</a>";$subnav .= $childNav . "</li>";}if($subnav) {$icon = $this->renderNavIcon($item['icon']);$class .= " uk-parent";$subnav ="<ul $ulAttrs>" ."<li class='pw-nav-dup'><a href='$item[url]'>$icon$item[title]</a></li>" .$subnav ."</ul>";}$out .="<li class='$class'><a href='$item[url]' id='sidenav-page-$item[id]'>$item[title]</a>" .$subnav ."</li>";}// render user nav$out .="<li class='uk-parent'>" ."<a href='#'><!--pw-user-nav-label--></a>" ."<ul $ulAttrs>" . $this->renderUserNavItems() . "</ul>" ."</li>";return $out;}/*** Identify current items in the primary nav and add appropriate classes to them** This presumes that navigation items in given $out markup use "page-[id]-" classes,* which will be updated consistent with the current $page.** @param $out**/protected function markCurrentNavItems(&$out) {$page = $this->wire('page');foreach($page->parents()->and($page) as $p) {$out = str_replace("page-$p-", "page-$p- uk-active", $out);}}/*** Render label for user masthead dropdown nav item** @return string**/public function renderUserNavLabel() {/** @var User $user */$user = $this->wire('user');$userLabel = $this->get('userLabel');$userAvatar = $this->get('userAvatar');$defaultIcon = 'user-circle';if(strpos($userLabel, '{') !== false) {if(strpos($userLabel, '{Name}') !== false) {$userLabel = str_replace('{Name}', ucfirst($user->name), $userLabel);} else if(strpos($userLabel, '{name}') !== false) {$userLabel = str_replace('{name}', $user->name, $userLabel);}if(strpos($userLabel, '{') !== false) {$userLabel = $user->getText($userLabel, true, true);}} else {$userLabel = $this->wire('sanitizer')->entities($userLabel);}if($userAvatar) {if($userAvatar === 'gravatar') {if($user->email) {$url = "https://www.gravatar.com/avatar/" . md5(strtolower(trim($user->email))) . "?s=80&d=mm&r=g";$userAvatar = "<img class='pw-avatar' src='$url' alt='$user->name' /> ";} else {$userAvatar = $this->renderNavIcon("$defaultIcon fa-lg");}} else if(strpos($userAvatar, 'icon.') === 0) {list(,$icon) = explode('.', $userAvatar);$userAvatar = $this->renderNavIcon("$icon fa-lg");} else if(strpos($userAvatar, ':')) {list($fieldID, $fieldName) = explode(':', $userAvatar);$field = $this->wire('fields')->get($fieldName);if(!$field || !$field->type instanceof FieldtypeImage) {$field = $this->wire('fields')->get((int) $fieldID);}if($field && $field->type instanceof FieldtypeImage) {$value = $user->get($field->name);if($value instanceof Pageimages) $value = $value->first();if($value instanceof Pageimage) {$value = $value->size(60, 60);$userAvatar = "<img class='pw-avatar' src='$value->url' alt='$user->name' /> ";} else {$userAvatar = $this->renderNavIcon("$defaultIcon fa-lg");}} else {$userAvatar = '';}}}if($userAvatar) $userLabel = $userAvatar . $userLabel;return $userLabel;}/*** Render navigation for the “user” menu** @return string**/public function renderUserNavItems() {$items = $this->getUserNavArray();$out = '';foreach($items as $item) {$label = $this->wire('sanitizer')->entities($item['title']);$icon = isset($item['icon']) ? $this->renderNavIcon($item['icon']) : ' ';$target = isset($item['target']) ? " target='$item[target]'" : '';$out .= "<li><a$target href='$item[url]'>$icon$label</a></li>";}return $out;}/*** Render link that opens the quick page-tree panel** @param string $icon Icon to use for link (default=sitemap)* @param string $text Optional text to accompany icon (default=empty)* @return string**/public function renderQuickTreeLink($icon = 'tree', $text = '') {$tree = $this->_('Tree');$url = $this->wire('urls')->admin . 'page/';return"<a class='pw-panel' href='$url' data-tab-text='$tree' data-tab-icon='$icon' title='$tree'>" .$this->renderNavIcon($icon) . $text ."</a>";}/*** Get the URL to the ProcessWire or brand logo (or <img> tag)** @param array $options* - `getURL` (bool): Return only the URL? (default=false)* - `getNative` (bool): Return only the ProcessWire brand logo? (default=false)* - `alt` (string): Alt attribute for <img> tag (default=auto)* - `height` (string): Height style to use for SVG images (default='')* @return string**/public function getLogo(array $options = array()) {/** @var Config $config */$config = $this->wire('config');/** @var Sanitizer $sanitizer */$sanitizer = $this->wire('sanitizer');$defaults = array('getURL' => false,'getNative' => false,'alt' => '','height' => '',);$options = array_merge($defaults, $options);$logoURL = $this->get('logoURL');$logoQS = '';$svg = false;if(empty($logoURL) || $options['getNative'] || strpos($logoURL, '//') !== false) {$native = true;$logoURL = $this->url() . self::logo;} else {if(strpos($logoURL, '?')) list($logoURL, $logoQS) = explode('?', $logoURL, 2);$logoURL = $config->urls->root . ltrim($logoURL, '/');$logoURL = $sanitizer->entities($logoURL);$native = false;$svg = strtolower(pathinfo($logoURL, PATHINFO_EXTENSION)) === 'svg';}$alt = $options['alt'];if(empty($alt) && $this->wire()->user->isLoggedin()) {$alt = "ProcessWire $config->version";}$class = 'pw-logo ' . ($native ? 'pw-logo-native' : 'pw-logo-custom');$attr = "class='$class' alt='$alt' ";if($svg) {if($options['height']) $attr .= "style='height:$options[height]' ";if(strpos($logoQS, 'uk-svg') === 0) {// if logo has "?uk-svg" query string, add uk-svg attribute which makes it styleable via CSS/LESS (PR#77)$attr .= 'uk-svg ';$logoQS = str_replace(array('uk-svg&', 'uk-svg'), '', $logoQS);}}if($logoQS) $logoURL .= '?' . $sanitizer->entities($logoQS);$img = "<img src='$logoURL' $attr/>";return $options['getURL'] ? $logoURL : $img;}/*** Get the URL to the ProcessWire or brand logo** @return string**/public function getLogoURL() {return $this->getLogo(array('getURL' => true));}/*** Get URL to this admin theme** @return string* @since 3.0.171**/public function url() {return $this->wire()->config->urls->modules . 'AdminTheme/AdminThemeUikit/';}/*** Get disk path to this admin theme** @return string* @since 3.0.171**/public function path() {return __DIR__ . '/';}/*** Get the primary Uikit CSS URL to use** @return string* @since 3.0.178 Was not hookable in prior versions**/public function ___getUikitCSS() {$config = $this->wire()->config;$cssUrl = $this->get('cssURL');if($cssUrl) { // a custom css URL was set in the theme configif(strpos($cssUrl, '//') === false) $cssUrl = $config->urls->root . ltrim($cssUrl, '/');return $this->wire()->sanitizer->entities($cssUrl);}require_once(__DIR__ . '/AdminThemeUikitCss.php');$settings = $config->AdminThemeUikit;if(!is_array($settings)) $settings = array();$settings['requireCssVersion'] = self::requireCssVersion;if(self::upgrade) {$settings['upgrade'] = true;$settings['replacements'] = array('../pw/images/' => 'images/');} else {if(empty($settings['customCssFile'])) $settings['customCssFile'] = '/site/assets/admin.css';$path = 'wire/modules/AdminTheme/AdminThemeUikit/uikit-pw/images/';$back = str_repeat('../', substr_count(trim($settings['customCssFile'], '/'), '/'));$settings['replacements'] = array('url(../pw/images/' => "url($back$path",'url("../pw/images/' => "url(\"$back$path",'url(pw/images/' => "url($back$path",'url("pw/images/' => "url(\"$back$path");}$css = new AdminThemeUikitCss($this, $settings);return $css->getCssFile();}/*** Get Javascript that must be present in the document <head>** @return string**/public function getHeadJS() {$config = $this->wire()->config;$data = $config->js('adminTheme');if(!is_array($data)) $data = array();$data['logoAction'] = (int) $this->logoAction;$data['toggleBehavior'] = (int) $this->toggleBehavior;$config->js('adminTheme', $data);return parent::getHeadJS();}/*** Module configuration** @param InputfieldWrapper $inputfields**/public function getModuleConfigInputfields(InputfieldWrapper $inputfields) {parent::getModuleConfigInputfields($inputfields);include_once(__DIR__ . '/config.php');$configHelper = new AdminThemeUikitConfigHelper($this);$configHelper->configModule($inputfields);}}