diff --git a/includes/CitizenTemplate.php b/includes/CitizenTemplate.php index 4ef8212d..11eaffde 100644 --- a/includes/CitizenTemplate.php +++ b/includes/CitizenTemplate.php @@ -45,12 +45,15 @@ class CitizenTemplate extends BaseTemplate { 'msg-citizen-header-menu-toggle' => $this->getMsg( 'citizen-header-menu-toggle' )->text(), 'data-menu' => $this->buildMenu(), 'msg-citizen-header-search-toggle' => $this->getMsg( 'citizen-header-search-toggle' )->text(), - 'data-extratools' => $this->buildExtraTools(), + 'data-extratools' => $this->getExtraTools(), 'data-searchbox' => $this->buildSearchbox(), ], 'html-sitenotice' => $this->get( 'sitenotice', null ), 'html-indicators' => $this->getIndicators(), + + 'data-pagetools' => $this->buildPageTools(), + // From Skin::getNewtalks(). Always returns string, cast to null if empty 'html-newtalk' => $this->get( 'newtalk', '' ) ?: null, 'page-langcode' => $this->getSkin()->getTitle()->getPageViewLanguage()->getHtmlCode(), @@ -74,6 +77,9 @@ class CitizenTemplate extends BaseTemplate { 'html-bodycontent' => $this->get( 'bodycontent' ), 'html-printfooter' => $this->get( 'printfooter', null ), + + 'data-pagelinks' => $this->buildPageLinks(), + 'html-catlinks' => $this->get( 'catlinks', '' ), 'html-dataAfterContent' => $this->get( 'dataAfterContent', '' ), // From MWDebug::getHTMLDebugLog (when $wgShowDebug is enabled) @@ -95,23 +101,6 @@ class CitizenTemplate extends BaseTemplate { 'data-bottombar' => $this->buildBottombar(), ]; - // TODO: Convert to Mustache - ob_start(); - - $html = $this->getPageTools(); - - echo $html; - $params['html-unported-pagetools'] = ob_get_contents(); - ob_end_clean(); - - ob_start(); - - $html = $this->getPageLinks(); - - echo $html; - $params['html-unported-pagelinks'] = ob_get_contents(); - ob_end_clean(); - // Prepare and output the HTML response $templates = new TemplateParser( __DIR__ . '/templates' ); echo $templates->processTemplate( 'skin', $params ); @@ -233,10 +222,10 @@ class CitizenTemplate extends BaseTemplate { } /** - * Render notification badges and ULS button + * Echo notification badges and ULS button * @return array */ - private function buildExtratools() { + private function getExtratools() { $personalTools = $this->getPersonalTools(); // Create the Echo badges and ULS @@ -251,12 +240,12 @@ class CitizenTemplate extends BaseTemplate { $extraTools['uls'] = $personalTools['uls']; } - $extraToolsPortal = $this->getMenuData( 'personal-extra', $extraTools ); + $html = $this->getMenuData( 'personal-extra', $extraTools ); // Hide label for extra tools - $extraToolsPortal[ 'label-class' ] .= 'screen-reader-text'; + $html[ 'label-class' ] .= 'screen-reader-text'; - return $extraToolsPortal; + return $html; } /** @@ -281,6 +270,88 @@ class CitizenTemplate extends BaseTemplate { return $props; } + /** + * Render page-related tools + * Possible visibility conditions: + * * true: always visible (bool) + * * false: never visible (bool) + * * 'login': only visible if logged in (string) + * * 'permission-*': only visible if user has permission + * e.g. permission-edit = only visible if user can edit pages + * @return string html + */ + protected function buildPageTools() { + $config = $this->config; + $skin = $this->getSkin(); + $condition = $config->get( 'CitizenShowPageTools' ); + $contentNavigation = $this->data['content_navigation']; + $props = []; + + // Login-based condition, return true if condition is met + if ( $condition === 'login' ) { + $condition = $skin()->getUser()->isLoggedIn(); + } + + // Permission-based condition, return true if condition is met + if ( is_string( $condition ) && strpos( $condition, 'permission' ) === 0 ) { + $permission = substr( $condition, 11 ); + try { + $condition = MediaWikiServices::getInstance()->getPermissionManager()->userCan( + $permission, $skin->getUser(), $skin->getTitle() ); + } catch ( Exception $e ) { + $condition = false; + } + } + + if ( $condition === true ) { + + $actionhtml = $this->getMenuData( 'views', $contentNavigation[ 'views' ] ?? [] ); + $actionmorehtml = $this->getMenuData( 'actions', $contentNavigation[ 'actions' ] ?? [] ); + + if ( $actionhtml ) { + $actionhtml[ 'label-class' ] .= 'screen-reader-text'; + } + + if ( $actionmorehtml ) { + $actionmorehtml[ 'label-class' ] .= 'screen-reader-text'; + } + + $props = [ + 'data-page-actions' => $actionhtml, + 'data-page-actions-more' => $actionmorehtml, + ]; + } + + return $props; + } + + + /** + * Render page-related links at the bottom + * @return string html + */ + private function buildPageLinks() : array { + $contentNavigation = $this->data['content_navigation']; + + $namespaceshtml = $this->getMenuData( 'namespaces', $contentNavigation[ 'namespaces' ] ?? [] ); + $variantshtml = $this->getMenuData( 'variants', $contentNavigation[ 'variants' ] ?? [] ); + + if ( $namespaceshtml ) { + $namespaceshtml[ 'label-class' ] .= 'screen-reader-text'; + } + + if ( $variantshtml ) { + $variantshtml[ 'label-class' ] .= 'screen-reader-text'; + } + + $props = [ + 'data-namespaces' => $namespaceshtml, + 'data-variants' => $variantshtml, + ]; + + return $props; + } + /** * Render the bottom bar * TODO: Convert button text to i18n message. @@ -301,261 +372,6 @@ class CitizenTemplate extends BaseTemplate { return $props; } - /** - * Language variants. Displays list for converting between different scripts in the same language, - * if using a language where this is applicable (such as latin vs cyric display for serbian). - * - * @return string html - */ - protected function getVariants() { - $html = ''; - if ( count( $this->data['content_navigation']['variants'] ) > 0 ) { - $html .= $this->getPortlet( 'variants', $this->data['content_navigation']['variants'] ); - } - - return $html; - } - - /** - * Generates page-related tools - * Possible visibility conditions: - * * true: always visible (bool) - * * false: never visible (bool) - * * 'login': only visible if logged in (string) - * * 'permission-*': only visible if user has permission - * e.g. permission-edit = only visible if user can edit pages - * @return string html - */ - protected function getPageTools() { - $html = ''; - - try { - $condition = $this->config->get( 'CitizenShowPageTools' ); - } catch ( ConfigException $e ) { - $condition = false; - } - - if ( $condition === 'login' ) { - $condition = $this->getSkin()->getUser()->isLoggedIn(); - } - - if ( is_string( $condition ) && strpos( $condition, 'permission' ) === 0 ) { - $permission = substr( $condition, 11 ); - try { - $condition = MediaWikiServices::getInstance()->getPermissionManager()->userCan( - $permission, $this->getSkin()->getUser(), $this->getSkin()->getTitle() ); - } catch ( Exception $e ) { - $condition = false; - } - } - - // Only display if user is logged in - if ( $condition === true ) { - $html .= Html::openElement( 'div', [ 'class' => 'mw-side', 'id' => 'page-tools' ] ); - // 'View' actions for the page: view, edit, view history, etc - $html .= $this->getPortlet( 'views', $this->data['content_navigation']['views'] ); - // Other actions for the page: move, delete, protect, everything else - $html .= $this->getPortlet( 'actions', $this->data['content_navigation']['actions'] ); - $html .= Html::closeElement( 'div' ); - } - - return $html; - } - - /** - * Generates page-related links at the bottom - * @return string html - */ - protected function getPageLinks() { - // Namespaces: links for 'content' and 'talk' for namespaces with talkpages. - // Otherwise is just the content. - // Usually rendered as tabs on the top of the page. - $html = $this->getPortlet( 'namespaces', $this->data['content_navigation']['namespaces'] ); - - // Language variant options - - return $html . $this->getVariants(); - } - - /** - * Generates a block of navigation links with a header - * - * @param string $name - * @param array|string $content array of links for use with makeListItem, or a block of text - * @param null|string|array $msg - * @param array $setOptions random crap to rename/do/whatever - * - * @return string html - */ - protected function getPortlet( $name, $content, $msg = null, $setOptions = [] ) { - // random stuff to override with any provided options - $options = $setOptions + [ - // extra classes/ids - 'id' => 'p-' . $name, - 'class' => 'mw-portlet', - 'extra-classes' => '', - // what to wrap the body list in, if anything - 'body-wrapper' => 'nav', - 'body-id' => null, - 'body-class' => 'mw-portlet-body', - // makeListItem options - 'list-item' => [ 'text-wrapper' => [ 'tag' => 'span' ] ], - // option to stick arbitrary stuff at the beginning of the ul - 'list-prepend' => '', - // old toolbox hook support (use: [ 'SkinTemplateToolboxEnd' => [ &$skin, true ] ]) - 'hooks' => '', - ]; - - // Handle the different $msg possibilities - if ( $msg === null ) { - $msg = $name; - } elseif ( is_array( $msg ) ) { - $msgString = array_shift( $msg ); - $msgParams = $msg; - $msg = $msgString; - } - $msgObj = $this->getMsg( $msg ); - if ( $msgObj->exists() ) { - if ( isset( $msgParams ) && !empty( $msgParams ) ) { - $msgString = $this->getMsg( $msg, $msgParams )->parse(); - } else { - $msgString = $msgObj->parse(); - } - } else { - $msgString = htmlspecialchars( $msg ); - } - - $labelId = Sanitizer::escapeIdForAttribute( "p-$name-label" ); - - if ( is_array( $content ) ) { - $contentText = - Html::openElement( 'ul', - [ 'lang' => $this->get( 'userlang' ), 'dir' => $this->get( 'dir' ) ] ); - $contentText .= $options['list-prepend']; - foreach ( $content as $key => $item ) { - $contentText .= $this->makeListItem( $key, $item, $options['list-item'] ); - } - // Compatibility with extensions still using SkinTemplateToolboxEnd or similar - if ( is_array( $options['hooks'] ) ) { - foreach ( $options['hooks'] as $hook ) { - if ( is_string( $hook ) ) { - $hookOptions = []; - } else { - // it should only be an array otherwise - $hookOptions = array_values( $hook )[0]; - $hook = array_keys( $hook )[0]; - } - $contentText .= $this->deprecatedHookHack( $hook, $hookOptions ); - } - } - - $contentText .= Html::closeElement( 'ul' ); - } else { - $contentText = $content; - } - - // Special handling for role=search and other weird things - $divOptions = [ - 'role' => 'navigation', - 'id' => Sanitizer::escapeIdForAttribute( $options['id'] ), - 'title' => Linker::titleAttrib( $options['id'] ), - 'aria-labelledby' => $labelId, - ]; - if ( !is_array( $options['class'] ) ) { - $class = [ $options['class'] ]; - } - if ( !is_array( $options['extra-classes'] ) ) { - $extraClasses = [ $options['extra-classes'] ]; - } - $divOptions['class'] = - array_merge( $class ?? $options['class'], $extraClasses ?? $options['extra-classes'] ); - - $labelOptions = [ - 'id' => $labelId, - 'lang' => $this->get( 'userlang' ), - 'dir' => $this->get( 'dir' ), - ]; - - if ( $options['body-wrapper'] !== 'none' ) { - $bodyDivOptions = [ 'class' => $options['body-class'] ]; - if ( is_string( $options['body-id'] ) ) { - $bodyDivOptions['id'] = $options['body-id']; - } - $body = - Html::rawElement( $options['body-wrapper'], $bodyDivOptions, - $contentText . $this->getAfterPortlet( $name ) ); - } else { - $body = $contentText . $this->getAfterPortlet( $name ); - } - - return Html::rawElement( 'div', $divOptions, - Html::rawElement( 'h3', $labelOptions, $msgString ) . $body ); - } - - /** - * Wrapper to catch output of old hooks expecting to write directly to page - * We no longer do things that way. - * - * @param string $hook event - * @param array $hookOptions args - * - * @return string html - */ - protected function deprecatedHookHack( $hook, $hookOptions = [] ) { - ob_start(); - try { - Hooks::run( $hook, $hookOptions ); - } catch ( Exception $e ) { - // Do nothing - } - - $hookContents = ob_get_clean(); - if ( !trim( $hookContents ) ) { - $hookContents = ''; - } - - return $hookContents; - } - - /** - * @param string $label to be used to derive the id and human readable label of the menu - * If the key has an entry in the constant MENU_LABEL_KEYS then that message will be used for the - * human readable text instead. - * @param array $urls to convert to list items stored as string in html-items key - * @param array $options (optional) to be passed to makeListItem - * @return array - */ - private function getMenuData( - string $label, - array $urls = [], - array $options = [] - ) : array { - // For some menu items, there is no language key corresponding with its menu key. - // These inconsitencies are captured in MENU_LABEL_KEYS - $msgObj = $this->getMsg( self::MENU_LABEL_KEYS[ $label ] ?? $label ); - $props = [ - 'id' => "p-$label", - 'label-class' => '', - 'label-id' => "p-{$label}-label", - // If no message exists fallback to plain text (T252727) - 'label' => $msgObj->exists() ? $msgObj->text() : $label, - 'html-items' => '', - 'html-tooltip' => Linker::tooltip( 'p-' . $label ), - ]; - - foreach ( $urls as $key => $item ) { - $props['html-items'] .= $this->makeListItem( $key, $item, $options ); - } - - $props['html-after-portal'] = $this->getAfterPortlet( $label ); - - // Mark the portal as empty if it has no content - $class = ( count( $urls ) == 0 && !$props['html-after-portal'] ) - ? 'mw-portal-empty' : ''; - $props['class'] = $class; - return $props; - } - /** * Get last modified message * @return string html @@ -659,4 +475,43 @@ class CitizenTemplate extends BaseTemplate { return $footerRows; } + + /** + * @param string $label to be used to derive the id and human readable label of the menu + * If the key has an entry in the constant MENU_LABEL_KEYS then that message will be used for the + * human readable text instead. + * @param array $urls to convert to list items stored as string in html-items key + * @param array $options (optional) to be passed to makeListItem + * @return array + */ + private function getMenuData( + string $label, + array $urls = [], + array $options = [] + ) : array { + // For some menu items, there is no language key corresponding with its menu key. + // These inconsitencies are captured in MENU_LABEL_KEYS + $msgObj = $this->getMsg( self::MENU_LABEL_KEYS[ $label ] ?? $label ); + $props = [ + 'id' => "p-$label", + 'label-class' => null, + 'label-id' => "p-{$label}-label", + // If no message exists fallback to plain text (T252727) + 'label' => $msgObj->exists() ? $msgObj->text() : $label, + 'html-items' => '', + 'html-tooltip' => Linker::tooltip( 'p-' . $label ), + ]; + + foreach ( $urls as $key => $item ) { + $props['html-items'] .= $this->makeListItem( $key, $item, $options ); + } + + $props['html-after-portal'] = $this->getAfterPortlet( $label ); + + // Mark the portal as empty if it has no content + $class = ( count( $urls ) == 0 && !$props['html-after-portal'] ) + ? ' mw-portal-empty' : ''; + $props['class'] = $class; + return $props; + } } diff --git a/includes/templates/skin.mustache b/includes/templates/skin.mustache index e78d9ead..2d7471d7 100644 --- a/includes/templates/skin.mustache +++ b/includes/templates/skin.mustache @@ -36,7 +36,12 @@ {{/html-sitenotice}} {{#html-newtalk}}
{{{html-newtalk}}}
{{/html-newtalk}} {{{html-indicators}}} - {{{html-unported-pagetools}}} + {{#data-pagetools}} +
+ {{#data-page-actions}}{{>Portal}}{{/data-page-actions}} + {{#data-page-actions-more}}{{>Portal}}{{/data-page-actions-more}} +
+ {{/data-pagetools}}

{{{html-title}}}

{{msg-tagline}}
{{{html-prebodyhtml}}} @@ -47,7 +52,10 @@ {{#html-printfooter}}
{{{html-printfooter}}}
{{/html-printfooter}} - {{{html-unported-pagelinks}}} + {{#data-pagelinks}} + {{#data-namespaces}}{{>Portal}}{{/data-namespaces}} + {{#data-variants}}{{>Portal}}{{/data-variants}} + {{/data-pagelinks}} {{{html-catlinks}}} {{{html-dataAfterContent}}} {{{html-debuglog}}} diff --git a/resources/components/common.less b/resources/components/common.less index 61477e08..4851eeff 100644 --- a/resources/components/common.less +++ b/resources/components/common.less @@ -741,10 +741,6 @@ a { margin: 0 @negative-margin; padding: @margin-side; - &-label { - .mixin-screen-reader-text; - } - ul { margin: 1.6rem 0 0 0; display: flex; @@ -758,6 +754,7 @@ a { align-items: center; padding: 0.4rem 0.8rem; border: 1px solid @base-80; + color: @base-20; background-color: @base-90; transition: @transition-background-quick, @transition-box-shadow-quick; .boxshadow(1); @@ -767,10 +764,6 @@ a { .boxshadow(2); } - span { - color: @base-20; - } - &:after { order: -1; content: ''; diff --git a/resources/components/darkmode.less b/resources/components/darkmode.less index ad5a3713..8485a05e 100644 --- a/resources/components/darkmode.less +++ b/resources/components/darkmode.less @@ -99,7 +99,7 @@ background: @dark-bg-40; } - #page-tools #p-actions > nav ul { + #page-tools #p-actions ul { background: @dark-bg-50; } @@ -180,7 +180,7 @@ } #page-tools #p-views li > a:after, - #page-tools #p-actions > nav:before, + #page-tools #p-actions:before, .mw-header-menu-drawer-container .mw-nav-links a:after, .mw-header-menu-drawer-container .mw-user-links a:after, .mw-editsection > a:before, diff --git a/resources/components/navigation.less b/resources/components/navigation.less index e4b7b2a5..41518b15 100644 --- a/resources/components/navigation.less +++ b/resources/components/navigation.less @@ -207,10 +207,12 @@ margin-bottom: @margin-side / 2; a { - justify-content: unset; + flex-direction: row-reverse; + justify-content: flex-end; &:after { margin: 0; + margin-right: @margin-side; width: @icon-box-size; height: @icon-box-size; } diff --git a/resources/components/page-tools.less b/resources/components/page-tools.less index 8fb2f6f6..bbe5d114 100644 --- a/resources/components/page-tools.less +++ b/resources/components/page-tools.less @@ -5,7 +5,7 @@ // // Hide selected item -.mw-portlet li.selected { +.mw-portal .selected { .mixin-screen-reader-text; } @@ -16,10 +16,6 @@ display: flex; transform: translateX( ~'calc( (100vw - @{page-width}) / 2 - @{margin-side} * 2 - 100%)' ); // magic - h3 { - .mixin-screen-reader-text; - } - ul { margin: 0; display: flex; @@ -53,74 +49,65 @@ } #p-actions { - > nav { - .resource-loader-icon-link; - padding: 5px; - cursor: pointer; - // transition: @transition-opacity-quick; - Hidden behind the menu anyways + .resource-loader-icon-link; + padding: 5px; + cursor: pointer; + // transition: @transition-opacity-quick; - Hidden behind the menu anyways - // TODO: Need to make value more flexible - ul { - z-index: -1; - pointer-events: none; - .menu-container; - position: absolute; - opacity: 0; - .boxshadow(4); - transition: @transition-opacity-quick, @transition-box-shadow-quick; + // TODO: Need to make value more flexible + ul { + z-index: -1; + pointer-events: none; + .menu-container; + position: absolute; + opacity: 0; + .boxshadow(4); + transition: @transition-opacity-quick, @transition-box-shadow-quick; - a { - .menu-item-link; - justify-content: space-between; - font-size: @ui-menu-text; - padding: @padding-menu-item; + a { + .menu-item-link; + justify-content: space-between; + font-size: @ui-menu-text; + padding: @padding-menu-item; + &:after { + .resource-loader-list-icon; + margin-left: @icon-padding; + opacity: @opacity-icon; + } + + &:hover, + &:active, + &:focus { &:after { - .resource-loader-list-icon; - margin-left: @icon-padding; - opacity: @opacity-icon; - } - - &:hover, - &:active, - &:focus { - &:after { - opacity: @opacity-icon-active; - } - } - - &:hover { - .menu-item-link-hover; - } - - &:active { - .menu-item-link-active; - } - - &:focus { - .menu-item-link-focus; + opacity: @opacity-icon-active; } } - } - &:before { - .resource-loader-menu-icon; - opacity: @opacity-icon; - } + &:hover { + .menu-item-link-hover; + } - /* - * Hidden behind the menu anyways - * &:hover:after { - * opacity: @opacity-icon-active; - * } -*/ + &:active { + .menu-item-link-active; + } - &:hover ul { - z-index: 5; - opacity: 1; - pointer-events: auto; + &:focus { + .menu-item-link-focus; + } } } + + &:before { + .resource-loader-menu-icon; + opacity: @opacity-icon; + } + + &:hover ul { + z-index: 5; + opacity: 1; + pointer-events: auto; + } } } diff --git a/skin.json b/skin.json index b14b0ce0..00889519 100644 --- a/skin.json +++ b/skin.json @@ -1,7 +1,7 @@ { "name": "Citizen", "namemsg": "skinname-citizen", - "version": "0.8.1", + "version": "0.8.2", "author": [ "[https://www.mediawiki.org/wiki/User:Alistair3149 Alistair3149]", "[https://www.mediawiki.org/wiki/User:Octfx Octfx]" @@ -260,7 +260,7 @@ }, "skins.citizen.icons.p": { "class": "ResourceLoaderImageModule", - "selector": "#p-{name} > *:before", + "selector": "#p-{name}:before", "defaultColor": "#000", "useDataURI": false, "images": {