diff --git a/i18n/en.json b/i18n/en.json index 59c931603..c3b88f713 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -63,5 +63,8 @@ "citizen-feature-custom-width-name": "Width", "citizen-feature-custom-width-standard-label": "Standard", "citizen-feature-custom-width-wide-label": "Wide", - "citizen-feature-custom-width-full-label": "Full" + "citizen-feature-custom-width-full-label": "Full", + + "citizen-user-info-text-anon": "Your IP address will be publicly visible if you make any edits.", + "citizen-user-info-text-temp": "This temporary account was created after an edit was made without an account on this browser and device." } diff --git a/i18n/qqq.json b/i18n/qqq.json index 07da90a39..5b1425e08 100644 --- a/i18n/qqq.json +++ b/i18n/qqq.json @@ -62,5 +62,7 @@ "citizen-feature-custom-width-name": "Heading label for page width", "citizen-feature-custom-width-standard-label": "Label for standard page width. An adjective that describes \"text\" ({{msg-mw|Citizen-feature-custom-width-name}}).", "citizen-feature-custom-width-wide-label": "Label for wide page width. An adjective that describes \"text\" ({{msg-mw|Citizen-feature-custom-width-name}}).", - "citizen-feature-custom-width-full-label": "Label for full page width. An adjective that describes \"text\" ({{msg-mw|Citizen-feature-custom-width-name}})." + "citizen-feature-custom-width-full-label": "Label for full page width. An adjective that describes \"text\" ({{msg-mw|Citizen-feature-custom-width-name}}).", + "citizen-user-info-text-anon": "Description in the user menu when user is not logged in.", + "citizen-user-info-text-temp": "Description in the user menu when user is using a temporary account." } diff --git a/includes/Components/CitizenComponent.php b/includes/Components/CitizenComponent.php new file mode 100644 index 000000000..6f0303ac7 --- /dev/null +++ b/includes/Components/CitizenComponent.php @@ -0,0 +1,17 @@ +localizer = $localizer; + $this->footerData = $footerData; + } + + /** + * @inheritDoc + */ + public function getTemplateData(): array { + $localizer = $this->localizer; + $footerData = $this->footerData; + + return $footerData + [ + 'msg-citizen-footer-desc' => $localizer->msg( "citizen-footer-desc" )->inContentLanguage()->parse(), + 'msg-citizen-footer-tagline' => $localizer->msg( "citizen-footer-tagline" )->inContentLanguage()->parse() + ]; + } +} diff --git a/includes/Components/CitizenComponentLink.php b/includes/Components/CitizenComponentLink.php new file mode 100644 index 000000000..f99a78472 --- /dev/null +++ b/includes/Components/CitizenComponentLink.php @@ -0,0 +1,80 @@ +href = $href; + $this->text = $text; + $this->icon = $icon; + $this->localizer = $localizer; + $this->accessKeyHint = $accessKeyHint; + } + + /** + * @inheritDoc + */ + public function getTemplateData(): array { + $localizer = $this->localizer; + $accessKeyHint = $this->accessKeyHint; + $additionalAttributes = []; + if ( $localizer ) { + $msg = $localizer->msg( $accessKeyHint . '-label' ); + if ( $msg->exists() ) { + $additionalAttributes[ 'aria-label' ] = $msg->text(); + } + } + return [ + 'href' => $this->href, + 'icon' => $this->icon, + 'text' => $this->text, + 'array-attributes' => [ + [ + 'key' => 'href', + 'value' => $this->href + ] + ], + 'html-attributes' => $localizer && $accessKeyHint ? Html::expandAttributes( + Linker::tooltipAndAccesskeyAttribs( + $accessKeyHint, + [], + [], + $localizer + ) + $additionalAttributes + ) : '', + ]; + } +} diff --git a/includes/Components/CitizenComponentMainMenu.php b/includes/Components/CitizenComponentMainMenu.php new file mode 100644 index 000000000..e680bcebb --- /dev/null +++ b/includes/Components/CitizenComponentMainMenu.php @@ -0,0 +1,43 @@ +sidebarData = $sidebarData; + } + + /** + * @inheritDoc + */ + public function getTemplateData(): array { + $portletsRest = []; + foreach ( $this->sidebarData[ 'array-portlets-rest' ] as $data ) { + /** + * Remove toolbox from main menu as we moved it to article tools + * TODO: Move handling to SkinCitizen.php after we convert pagetools to component + */ + if ( $data['id'] === 'p-tb' ) { + continue; + } + $portletsRest[] = ( new CitizenComponentMenu( $data ) )->getTemplateData(); + } + $firstPortlet = new CitizenComponentMenu( $this->sidebarData['data-portlets-first'] ); + + return [ + 'data-portlets-first' => $firstPortlet->getTemplateData(), + 'array-portlets-rest' => $portletsRest + ]; + } +} diff --git a/includes/Components/CitizenComponentMenu.php b/includes/Components/CitizenComponentMenu.php new file mode 100644 index 000000000..ba9bb0413 --- /dev/null +++ b/includes/Components/CitizenComponentMenu.php @@ -0,0 +1,52 @@ +data = $data; + } + + /** + * Counts how many items the menu has. + * + * @return int + */ + public function count(): int { + $items = $this->data['array-list-items'] ?? null; + if ( $items ) { + return count( $items ); + } + $htmlItems = $this->data['html-items'] ?? ''; + return substr_count( $htmlItems, 'data + [ + 'class' => '', + 'label' => '', + 'html-tooltip' => '', + 'label-class' => '', + 'html-before-portal' => '', + 'html-items' => '', + 'html-after-portal' => '', + 'array-list-items' => null, + ]; + } +} diff --git a/includes/Components/CitizenComponentMenuListItem.php b/includes/Components/CitizenComponentMenuListItem.php new file mode 100644 index 000000000..649ce3882 --- /dev/null +++ b/includes/Components/CitizenComponentMenuListItem.php @@ -0,0 +1,39 @@ +link = $link; + $this->class = $class; + $this->id = $id; + } + + /** + * @inheritDoc + */ + public function getTemplateData(): array { + return [ + 'array-links' => $this->link->getTemplateData(), + 'item-class' => $this->class, + 'item-id' => $this->id, + ]; + } +} diff --git a/includes/Components/CitizenComponentPageFooter.php b/includes/Components/CitizenComponentPageFooter.php new file mode 100644 index 000000000..9d7dbd138 --- /dev/null +++ b/includes/Components/CitizenComponentPageFooter.php @@ -0,0 +1,46 @@ +localizer = $localizer; + $this->footerData = $footerData; + } + + /** + * @inheritDoc + */ + public function getTemplateData(): array { + $footerData = $this->footerData; + + // Add label to footer-info to use in PageFooter + foreach ( $footerData['array-items'] as &$item ) { + $msgKey = 'citizen-page-info-' . $item['name']; + $item['label'] = $this->localizer->msg( $msgKey )->text(); + } + + return $footerData; + } +} diff --git a/includes/Components/CitizenComponentPageHeading.php b/includes/Components/CitizenComponentPageHeading.php new file mode 100644 index 000000000..9916099eb --- /dev/null +++ b/includes/Components/CitizenComponentPageHeading.php @@ -0,0 +1,226 @@ +localizer = $localizer; + $this->out = $out; + $this->pageLang = $pageLang; + $this->title = $title; + $this->titleData = $titleData; + $this->user = $user; + } + + /** + * Check if the current page is in the content namespace + * + * @return bool + */ + private function shouldAddParenthesis(): bool { + $ns = $this->title->getNamespace(); + $contentNs = MediaWikiServices::getInstance()->getNamespaceInfo()->getContentNamespaces(); + return in_array( $ns, $contentNs ); + } + + /** + * Return new User object based on username or IP address. + * Based on MinervaNeue + * + * @return UserIdentity|null + */ + private function buildPageUserObject() { + $titleText = $this->title->getText(); + $user = $this->user; + + if ( IPUtils::isIPAddress( $titleText ) ) { + return $user->newFromAnyId( null, $titleText, null ); + } + + $userIdentity = MediaWikiServices::getInstance()->getUserIdentityLookup()->getUserIdentityByName( $titleText ); + if ( $userIdentity && $userIdentity->isRegistered() ) { + return $user->newFromId( $userIdentity->getId() ); + } + + return null; + } + + /** + * Return user tagline message + * + * @return string|null + */ + private function buildUserTagline() { + $localizer = $this->localizer; + + $user = $this->buildPageUserObject(); + if ( $user ) { + $tagline = '
'; + $editCount = $user->getEditCount(); + $regDate = $user->getRegistration(); + $gender = MediaWikiServices::getInstance()->getGenderCache()->getGenderOf( $user, __METHOD__ ); + + if ( $gender === 'male' ) { + $msgGender = '♂'; + } elseif ( $gender === 'female' ) { + $msgGender = '♀'; + } + if ( isset( $msgGender ) ) { + $tagline .= "$msgGender"; + } + + if ( $editCount ) { + $msgEditCount = $localizer->msg( 'usereditcount' )->numParams( sprintf( '%s', number_format( $editCount, 0 ) ) ); + $editCountHref = SpecialPage::getTitleFor( 'Contributions', $user )->getLocalURL(); + $tagline .= "$msgEditCount"; + } + + if ( is_string( $regDate ) ) { + $regDateTs = wfTimestamp( TS_UNIX, $regDate ); + $msgRegDate = $localizer->msg( 'citizen-tagline-user-regdate', $this->pageLang->userDate( new MWTimestamp( $regDate ), $this->user ), $user ); + $tagline .= "$msgRegDate"; + } + + $tagline .= '
'; + return $tagline; + } + return null; + } + + /** + * Return the modified page heading HTML + * + * @return string + */ + private function getPageHeading(): string { + $titleHtml = $this->titleData; + if ( $this->shouldAddParenthesis() ) { + // Look for the to ensure that it is the last parenthesis of the title + $pattern = '/\s(\(.+\))<\/span>/'; + $replacement = ' $1'; + $titleHtml = preg_replace( $pattern, $replacement, $this->titleData ); + } + return $titleHtml; + } + + /** + * Return the page tagline based on the current page + * + * @return string + */ + private function getTagline(): string { + $localizer = $this->localizer; + $title = $this->title; + + $tagline = ''; + + if ( $title ) { + // Use short description if there is any + // from Extension:ShortDescription + $shortdesc = $this->out->getProperty( 'shortdesc' ); + if ( $shortdesc ) { + $tagline = $shortdesc; + } else { + $namespaceText = $title->getNsText(); + // Check if namespaceText exists + // Return null if main namespace or not defined + if ( $namespaceText ) { + $msg = $localizer->msg( 'citizen-tagline-ns-' . strtolower( $namespaceText ) ); + // Use custom message if exists + if ( !$msg->isDisabled() ) { + $tagline = $msg->parse(); + } else { + if ( $title->isSpecialPage() ) { + // No tagline if special page + $tagline = ''; + } elseif ( $title->isTalkPage() ) { + // Use generic talk page message if talk page + $tagline = $localizer->msg( 'citizen-tagline-ns-talk' )->parse(); + } elseif ( ( $title->inNamespace( NS_USER ) || ( defined( 'NS_USER_WIKI' ) && $title->inNamespace( NS_USER_WIKI ) ) || ( defined( 'NS_USER_WIKI' ) && $title->inNamespace( NS_USER_PROFILE ) ) ) && !$title->isSubpage() ) { + // Build user tagline if it is a top-level user page + $tagline = $this->buildUserTagline( $title ); + } elseif ( !$localizer->msg( 'citizen-tagline' )->isDisabled() ) { + $tagline = $localizer->msg( 'citizen-tagline' )->parse(); + } else { + // Fallback to site tagline + $tagline = $localizer->msg( 'tagline' )->text(); + } + } + } elseif ( !$localizer->msg( 'citizen-tagline' )->isDisabled() ) { + $tagline = $localizer->msg( 'citizen-tagline' )->parse(); + } else { + $tagline = $localizer->msg( 'tagline' )->text(); + } + } + } + + // Apply language variant conversion + if ( !empty( $tagline ) ) { + $services = MediaWikiServices::getInstance(); + $langConv = $services + ->getLanguageConverterFactory() + ->getLanguageConverter( $services->getContentLanguage() ); + $tagline = $langConv->convert( $tagline ); + } + + return $tagline; + } + + /** + * @inheritDoc + */ + public function getTemplateData(): array { + return [ + 'html-tagline' => $this->getTagline(), + 'html-title-heading' => $this->getPageHeading() + ]; + } +} diff --git a/includes/Components/CitizenComponentPageSidebar.php b/includes/Components/CitizenComponentPageSidebar.php new file mode 100644 index 000000000..3a51a619b --- /dev/null +++ b/includes/Components/CitizenComponentPageSidebar.php @@ -0,0 +1,123 @@ +localizer = $localizer; + $this->out = $out; + $this->pageLang = $pageLang; + $this->title = $title; + $this->user = $user; + } + + /** + * Get the last modified data + * TODO: Use core instead when update to MW 1.43 + * @return array + */ + private function getLastModData() { + $timestamp = $this->out->getRevisionTimestamp(); + + if ( !$timestamp ) { + return []; + } + + $localizer = $this->localizer; + $pageLang = $this->pageLang; + $title = $this->title; + $user = $this->user; + + $d = $pageLang->userDate( $timestamp, $user ); + $t = $pageLang->userTime( $timestamp, $user ); + $s = $localizer->msg( 'lastmodifiedat', $d, $t ); + + // FIXME: Use CitizenComponentMenuListItem + $items = [ + 'item-id' => 'lm-time', + 'item-class' => 'mw-list-item', + 'array-links' => [ + 'array-attributes' => [ + [ + 'key' => 'id', + 'value' => 'citizen-lastmod-relative' + ], + [ + 'key' => 'href', + 'value' => $title->getLocalURL( [ 'diff' => '' ] ) + ], + [ + 'key' => 'title', + 'value' => $s + ], + [ + 'key' => 'data-timestamp', + 'value' => wfTimestamp( TS_UNIX, $timestamp ) + ] + ], + 'icon' => 'history', + 'text' => $d + ] + ]; + + $menu = new CitizenComponentMenu( + [ + 'id' => 'citizen-sidebar-lastmod', + 'label' => $localizer->msg( 'citizen-page-info-lastmod' ), + 'array-list-items' => $items + ] + ); + + return $menu->getTemplateData(); + } + + /** + * @inheritDoc + */ + public function getTemplateData(): array { + return [ + 'data-page-sidebar-lastmod' => $this->getLastModData() + ]; + } +} diff --git a/includes/Components/CitizenComponentSearchBox.php b/includes/Components/CitizenComponentSearchBox.php new file mode 100644 index 000000000..6a84278e8 --- /dev/null +++ b/includes/Components/CitizenComponentSearchBox.php @@ -0,0 +1,41 @@ +searchBoxData = $searchBoxData; + $this->skin = $skin; + } + + /** + * @inheritDoc + */ + public function getTemplateData(): array { + $searchBoxData = $this->searchBoxData; + + return $searchBoxData += [ + 'msg-citizen-search-toggle-shortcut' => '[/]', + 'html-random-href' => $this->skin->makeSpecialUrl( 'Randompage' ), + ]; + } +} diff --git a/includes/Components/CitizenComponentSiteStats.php b/includes/Components/CitizenComponentSiteStats.php new file mode 100644 index 000000000..7a037a44e --- /dev/null +++ b/includes/Components/CitizenComponentSiteStats.php @@ -0,0 +1,115 @@ + 'article', + 'images' => 'image', + 'users' => 'userAvatar', + 'edits' => 'edit' + ]; + + /** + * @return Config + */ + private function getConfig(): Config { + return $this->config; + } + + /** + * @param MessageLocalizer $localizer + * @param Language|StubUserLang $pageLang + */ + public function __construct( + Config $config, + MessageLocalizer $localizer, + $pageLang + ) { + $this->config = $config; + $this->localizer = $localizer; + $this->pageLang = $pageLang; + } + + /** + * Get and format sitestat value + * + * @param string $key + * @param NumberFormatter|null $fmt + * @return string + */ + private function getSiteStatValue( $key, $fmt ): string { + $value = SiteStats::$key() ?? ''; + + if ( $fmt ) { + return $fmt->format( $value ); + } else { + return number_format( $value ); + } + } + + /** + * @inheritDoc + */ + public function getTemplateData(): array { + $config = $this->getConfig(); + if ( !$config->get( 'CitizenEnableDrawerSiteStats' ) ) { + return []; + } + + $items = []; + $fmt = null; + + // Get NumberFormatter here so that we don't have to call it for every stats + if ( $config->get( 'CitizenUseNumberFormatter' ) && class_exists( NumberFormatter::class ) ) { + $locale = $this->pageLang->getHtmlCode() ?? 'en_US'; + try { + $fmt = new NumberFormatter( $locale, NumberFormatter::PADDING_POSITION ); + $fmt->setAttribute( NumberFormatter::ROUNDING_MODE, NumberFormatter::ROUND_DOWN ); + $fmt->setAttribute( NumberFormatter::MAX_FRACTION_DIGITS, 1 ); + } catch ( IntlException $exception ) { + /* + * FIXME: Put a proper log or error message here? + * For some unknown reason, NumberFormatter can throw an IntlException: Constructor failed + * This should allow Citizen to run as usual even if such exception is encountered. + */ + } + } + + foreach ( self::SITESTATS_ICON_MAP as $key => $icon ) { + $items[] = [ + 'id' => $key, + 'icon' => $icon, + 'value' => $this->getSiteStatValue( $key, $fmt ), + 'label' => $this->localizer->msg( "citizen-sitestats-$key-label" )->text(), + ]; + } + + return [ + 'array-sitestats-items' => $items + ]; + } +} diff --git a/includes/Components/CitizenComponentUserInfo.php b/includes/Components/CitizenComponentUserInfo.php new file mode 100644 index 000000000..f09511ba7 --- /dev/null +++ b/includes/Components/CitizenComponentUserInfo.php @@ -0,0 +1,180 @@ +isRegistered = $isRegistered; + $this->isTemp = $isTemp; + $this->localizer = $localizer; + $this->title = $title; + $this->user = $user; + $this->userPageData = $userPageData; + } + + /** + * Get the user edit count + * + * @return array|null + */ + private function getUserEditCount(): ?array { + // Return user edits + $edits = MediaWikiServices::getInstance()->getUserEditTracker()->getUserEditCount( $this->user ); + + if ( empty( $edits ) ) { + return null; + } + + $label = $this->localizer->msg( 'usereditcount' )->numParams( $edits ); + $label = str_replace( $edits, '', $label ); + + return [ + 'count' => number_format( $edits, 0 ), + 'label' => $label + ]; + } + + /** + * Build the template data for the user groups + * + * @return array|null + */ + private function getUserGroups(): ?array { + $groups = MediaWikiServices::getInstance()->getUserGroupManager()->getUserGroups( $this->user ); + + if ( empty( $groups ) ) { + return null; + } + + $listItems = []; + $msgKey = 'group-%s-member'; + foreach ( $groups as $group ) { + $id = sprintf( $msgKey, $group ); + $text = $this->localizer->msg( $id )->text(); + $title = $this->title->newFromText( $text, NS_PROJECT ); + + if ( !$text || !$title ) { + continue; + } + + $link = new CitizenComponentLink( + $title->getLinkURL(), + ucfirst( $text ) + ); + + $listItem = new CitizenComponentMenuListItem( $link, 'citizen-userInfo-usergroup', $id ); + + $listItems[] = $listItem->getTemplateData(); + } + + return [ + 'array-list-items' => $listItems + ]; + } + + /** + * Build the template data for the user page menu + * + * @return array + */ + private function getUserPage(): array { + $user = $this->user; + $userPageData = $this->userPageData; + + $htmlItems = $userPageData['html-items']; + $realname = $user->getRealName(); + if ( !empty( $realname ) ) { + $username = $user->getName(); + $innerHtml = <<$realname + $username + HTML; + // Dirty but it works + $htmlItems = str_replace( + ">" . $username . "<", + ">" . $innerHtml . "<", + $userPageData['html-items'] + ); + } + + $menu = new CitizenComponentMenu( [ + 'id' => 'citizen-user-menu-userpage', + 'class' => null, + 'label' => null, + 'html-items' => $htmlItems + ] ); + + return $menu->getTemplateData(); + } + + /** + * @inheritDoc + */ + public function getTemplateData(): array { + $localizer = $this->localizer; + $data = []; + + if ( $this->isRegistered ) { + $data = [ + 'data-user-page' => $this->getUserPage(), + 'data-user-edit' => $this->getUserEditCount() + ]; + + if ( $this->isTemp ) { + $data['text'] = $localizer->msg( 'citizen-user-info-text-temp' ); + } else { + $data['data-user-groups'] = $this->getUserGroups(); + } + } else { + $data = [ + 'title' => $localizer->msg( 'notloggedin' ), + 'text' => $localizer->msg( 'citizen-user-info-text-anon' ) + ]; + } + + return $data; + } +} diff --git a/includes/Partials/Drawer.php b/includes/Partials/Drawer.php deleted file mode 100644 index ea7fbf2c9..000000000 --- a/includes/Partials/Drawer.php +++ /dev/null @@ -1,127 +0,0 @@ -. - * - * @file - * @ingroup Skins - */ - -declare( strict_types=1 ); - -namespace MediaWiki\Skins\Citizen\Partials; - -use IntlException; -use NumberFormatter; - -/** - * Drawer partial of Skin Citizen - * Generates the following partials: - * - Logo - * - Drawer - * + Special Pages Link - * + Upload Link - */ -final class Drawer extends Partial { - /** - * Decorate main menu template data - * - * @return array - */ - public function decorateMainMenuData( $mainMenuData ) { - for ( $i = 0; $i < count( $mainMenuData['array-portlets-rest'] ); $i++ ) { - if ( $mainMenuData['array-portlets-rest'][$i]['id'] === 'p-tb' ) { - // Remove toolbox since it is handled by page tools - unset( $mainMenuData['array-portlets-rest'][$i] ); - break; - } - } - - // Reset index after unsetting toolbox - $mainMenuData['array-portlets-rest'] = array_values( $mainMenuData['array-portlets-rest'] ); - - return $mainMenuData; - } - - /** - * Get messages used for site stats in the drawer - * - * @return array for use in Mustache template. - */ - public function getSiteStatsData(): array { - if ( !$this->getConfigValue( 'CitizenEnableDrawerSiteStats' ) ) { - return []; - } - - $skin = $this->skin; - // Key => Icon - $map = [ - 'articles' => 'article', - 'images' => 'image', - 'users' => 'userAvatar', - 'edits' => 'edit' - ]; - $items = []; - $fmt = null; - - // Get NumberFormatter here so that we don't have to call it for every stats - if ( $this->getConfigValue( 'CitizenUseNumberFormatter' ) && class_exists( NumberFormatter::class ) ) { - $locale = $skin->getLanguage()->getHtmlCode() ?? 'en_US'; - try { - $fmt = new NumberFormatter( $locale, NumberFormatter::PADDING_POSITION ); - $fmt->setAttribute( NumberFormatter::ROUNDING_MODE, NumberFormatter::ROUND_DOWN ); - $fmt->setAttribute( NumberFormatter::MAX_FRACTION_DIGITS, 1 ); - } catch ( IntlException $exception ) { - /* - * FIXME: Put a proper log or error message here? - * For some unknown reason, NumberFormatter can throw an IntlException: Constructor failed - * This should allow Citizen to run as usual even if such exception is encountered. - */ - } - } - - foreach ( $map as $key => $icon ) { - $items[] = [ - 'id' => $key, - 'icon' => $icon, - 'value' => $this->getSiteStatValue( $key, $fmt ), - 'label' => $skin->msg( "citizen-sitestats-$key-label" )->text(), - ]; - } - - return [ - 'array-drawer-sitestats-item' => $items - ]; - } - - /** - * Get and format sitestat value - * - * @param string $key - * @param NumberFormatter|null $fmt - * @return string - */ - private function getSiteStatValue( $key, $fmt ): string { - $value = call_user_func( 'SiteStats::' . $key ) ?? ''; - - if ( $fmt ) { - return $fmt->format( $value ); - } else { - return number_format( $value ); - } - } -} diff --git a/includes/Partials/Footer.php b/includes/Partials/Footer.php deleted file mode 100644 index 0e055dda3..000000000 --- a/includes/Partials/Footer.php +++ /dev/null @@ -1,48 +0,0 @@ -. - * - * @file - * @ingroup Skins - */ - -declare( strict_types=1 ); - -namespace MediaWiki\Skins\Citizen\Partials; - -/** - * Footer partial of Skin Citizen - */ -final class Footer extends Partial { - - /** - * Decorate footer template data - * - * @param array $footerData original data-footer - * @return array for use in Mustache template describing the footer elements. - */ - public function decorateFooterData( $footerData ): array { - // Add label to footer-info to use in ContentFooter - foreach ( $footerData['data-info']['array-items'] as &$item ) { - $msgKey = 'citizen-page-info-' . $item['name']; - $item['label'] = $this->skin->msg( $msgKey )->text(); - } - - return $footerData; - } -} diff --git a/includes/Partials/Header.php b/includes/Partials/Header.php deleted file mode 100644 index a2af02cfd..000000000 --- a/includes/Partials/Header.php +++ /dev/null @@ -1,178 +0,0 @@ -. - * - * @file - * @ingroup Skins - */ - -declare( strict_types=1 ); - -namespace MediaWiki\Skins\Citizen\Partials; - -use MediaWiki\MediaWikiServices; -use Skin; - -/** - * Header partial of Skin Citizen - * Generates the following partials: - * - User Menu - * - Search - */ -final class Header extends Partial { - /** - * Decorate search box template data - * - * @param array $searchBoxData original data-search-box - * @return array - */ - public function decorateSearchBoxData( $searchBoxData ): array { - return $searchBoxData += [ - 'msg-citizen-search-toggle-shortcut' => '[/]', - 'html-random-href' => Skin::makeSpecialUrl( 'Randompage' ), - ]; - } - - /** - * Get the user info template data for user menu - * - * TODO: Consider dropping Menu.mustache since the DOM doesn't make much sense - * - * @param array $userPageData data-portlets.data-user-page - * @return array - */ - public function getUserInfoData( $userPageData ): array { - $isRegistered = $this->user->isRegistered(); - - $html = $this->getUserPageHTML( $isRegistered, $userPageData ); - - if ( $isRegistered ) { - $html .= $this->getUserGroupsHTML(); - $html .= $this->getUserContributionsHTML(); - } - - return [ - 'id' => 'p-user-info', - 'html-items' => $html, - ]; - } - - /** - * Get the user page HTML - * - * @param bool $isRegistered - * @param array $userPageData data-portlets.data-user-page - * @return string - */ - private function getUserPageHTML( $isRegistered, $userPageData ): ?string { - if ( $isRegistered ) { - $realname = $this->user->getRealName(); - if ( !empty( $realname ) ) { - $username = $this->user->getName(); - $innerHtml = <<$realname -
$username
- HTML; - // Dirty but it works - $html = str_replace( - ">" . $username . "<", - ">" . $innerHtml . "<", - $userPageData['html-items'] - ); - } else { - $html = $userPageData['html-items']; - } - } else { - // There must be a cleaner way to do this - $msg = $this->skin->msg( 'notloggedin' )->text(); - $tooltip = $this->skin->msg( 'tooltip-pt-anonuserpage' )->text(); - $html = << - $msg - - HTML; - } - - return $html; - } - - /** - * Get the user groups HTML - * - * @return string|null - */ - private function getUserGroupsHTML(): ?string { - // This does not return implicit groups - $groups = MediaWikiServices::getInstance()->getUserGroupManager()->getUserGroups( $this->user ); - - if ( empty( $groups ) ) { - return null; - } - - $html = ''; - $msgName = 'group-%s-member'; - - // There must be a cleaner way - foreach ( $groups as $group ) { - $id = sprintf( $msgName, $group ); - $msg = $this->skin->msg( $id )->text(); - $title = $this->title->newFromText( - $msg, - NS_PROJECT - ); - if ( $msg ) { - // Member names are in lowercase - $msg = ucfirst( $msg ); - } - if ( $title ) { - $href = $title->getLinkURL(); - $html .= <<< HTML -
  • - $msg -
  • - HTML; - } - } - - $html = sprintf( '
  • ', $html ); - - return $html; - } - - /** - * Get the user contributions HTML - * - * @return string|null - */ - private function getUserContributionsHTML(): ?string { - // Return user edits - $edits = MediaWikiServices::getInstance()->getUserEditTracker()->getUserEditCount( $this->user ); - - if ( empty( $edits ) ) { - return null; - } - - $editsText = $this->skin->msg( 'usereditcount' ) - ->numParams( sprintf( '%s', number_format( $edits, 0 ) ) ); - - // There must be a cleaner way - $html = '
  • ' . $editsText . '
  • '; - - return $html; - } -} diff --git a/includes/Partials/PageTitle.php b/includes/Partials/PageTitle.php deleted file mode 100644 index f19af32aa..000000000 --- a/includes/Partials/PageTitle.php +++ /dev/null @@ -1,60 +0,0 @@ -. - * - * @file - * @ingroup Skins - */ - -declare( strict_types=1 ); - -namespace MediaWiki\Skins\Citizen\Partials; - -use MediaWiki\MediaWikiServices; - -/** - * Title partial of Skin Citizen - */ -final class PageTitle extends Partial { - /** - * Wrap text within parenthesis with a span tag - * - * @param string $data HTML of the title of the page - * @return string - */ - public function decorateTitle( $data ) { - if ( $this->shouldAddParenthesis() ) { - // Look for the to ensure that it is the last parenthesis of the title - $pattern = '/\s(\(.+\))<\/span>/'; - $replacement = ' $1'; - return preg_replace( $pattern, $replacement, $data ); - } - return $data; - } - - /** - * Check if the current page is in the content namespace - * - * @return bool - */ - private function shouldAddParenthesis(): bool { - $ns = $this->title->getNamespace(); - $contentNs = MediaWikiServices::getInstance()->getNamespaceInfo()->getContentNamespaces(); - return in_array( $ns, $contentNs ); - } -} diff --git a/includes/Partials/Sidebar.php b/includes/Partials/Sidebar.php deleted file mode 100644 index d139d5011..000000000 --- a/includes/Partials/Sidebar.php +++ /dev/null @@ -1,107 +0,0 @@ -. - * - * @file - * @ingroup Skins - */ - -declare( strict_types=1 ); - -namespace MediaWiki\Skins\Citizen\Partials; - -/** - * Based on SkinComponentLastModified.php in MediaWiki core - * TODO: Use core instead when update to MW 1.43 - */ -final class Sidebar extends Partial { - /** @var Language */ - private $language; - - /** @var string|null|false */ - private $timestamp; - - /** - * Get sidebar template data - * - * @param array $parentData - * @return array html - */ - public function getSidebarData( $parentData ): array { - $data = [ - 'data-sidebar-lastmod' => $this->getLastModData() - ]; - - return $data; - } - - /** - * Build last modified template data - * - * @return array|null - */ - private function getLastModData() { - $skin = $this->skin; - $out = $this->out; - $user = $this->user; - $title = $this->title; - $language = $skin->getLanguage(); - $timestamp = $out->getRevisionTimestamp(); - - if ( $timestamp ) { - $d = $language->userDate( $timestamp, $user ); - $t = $language->userTime( $timestamp, $user ); - $s = ' ' . $skin->msg( 'lastmodifiedat', $d, $t )->parse(); - } else { - return; - } - - $items = [ - 'id' => 'lm-time', - 'class' => 'mw-list-item', - 'array-links' => [ - 'array-attributes' => [ - [ - 'key' => 'id', - 'value' => 'citizen-lastmod-relative' - ], - [ - 'key' => 'href', - 'value' => $title->getLocalURL( [ 'diff' => '' ] ) - ], - [ - 'key' => 'title', - 'value' => $s - ], - [ - 'key' => 'data-timestamp', - 'value' => wfTimestamp( TS_UNIX, $timestamp ) - ] - ], - 'icon' => 'history', - 'text' => $d - ] - ]; - - return [ - 'id' => 'citizen-sidebar-lastmod', - 'label' => $skin->msg( 'citizen-page-info-lastmod' ), - 'array-list-items' => $items - ]; - } -} diff --git a/includes/Partials/Tagline.php b/includes/Partials/Tagline.php deleted file mode 100644 index 1c7ecf7f4..000000000 --- a/includes/Partials/Tagline.php +++ /dev/null @@ -1,168 +0,0 @@ -. - * - * @file - * @ingroup Skins - */ - -declare( strict_types=1 ); - -namespace MediaWiki\Skins\Citizen\Partials; - -use MediaWiki\MediaWikiServices; -use MediaWiki\Title\Title; -use MWTimestamp; -use SpecialPage; -use User; -use Wikimedia\IPUtils; - -/** - * Tagline partial of Skin Citizen - */ -final class Tagline extends Partial { - - /** - * Get tagline message - * - * @return string - */ - public function getTagline() { - $skin = $this->skin; - $out = $this->out; - $title = $this->title; - - $shortdesc = $out->getProperty( 'shortdesc' ); - $tagline = ''; - - if ( $title ) { - // Use short description if there is any - // from Extension:ShortDescription - if ( $shortdesc ) { - $tagline = $shortdesc; - } else { - $namespaceText = $title->getNsText(); - // Check if namespaceText exists - // Return null if main namespace or not defined - if ( $namespaceText ) { - $msg = $skin->msg( 'citizen-tagline-ns-' . strtolower( $namespaceText ) ); - // Use custom message if exists - if ( !$msg->isDisabled() ) { - $tagline = $msg->parse(); - } else { - if ( $title->isSpecialPage() ) { - // No tagline if special page - $tagline = ''; - } elseif ( $title->isTalkPage() ) { - // Use generic talk page message if talk page - $tagline = $skin->msg( 'citizen-tagline-ns-talk' )->parse(); - } elseif ( ( $title->inNamespace( NS_USER ) || ( defined( 'NS_USER_WIKI' ) && $title->inNamespace( NS_USER_WIKI ) ) || ( defined( 'NS_USER_WIKI' ) && $title->inNamespace( NS_USER_PROFILE ) ) ) && !$title->isSubpage() ) { - // Build user tagline if it is a top-level user page - $tagline = $this->buildUserTagline( $title ); - } elseif ( !$skin->msg( 'citizen-tagline' )->isDisabled() ) { - $tagline = $skin->msg( 'citizen-tagline' )->parse(); - } else { - // Fallback to site tagline - $tagline = $skin->msg( 'tagline' )->text(); - } - } - } elseif ( !$skin->msg( 'citizen-tagline' )->isDisabled() ) { - $tagline = $skin->msg( 'citizen-tagline' )->parse(); - } else { - $tagline = $skin->msg( 'tagline' )->text(); - } - } - } - - // Apply language variant conversion - if ( !empty( $tagline ) ) { - $services = MediaWikiServices::getInstance(); - $langConv = $services - ->getLanguageConverterFactory() - ->getLanguageConverter( $services->getContentLanguage() ); - $tagline = $langConv->convert( $tagline ); - } - - return $tagline; - } - - /** - * Return user tagline message - * - * @param Title $title - * @return string - */ - private function buildUserTagline( $title ) { - $user = $this->buildPageUserObject( $title ); - if ( $user ) { - $skin = $this->skin; - $tagline = '
    '; - $editCount = $user->getEditCount(); - $regDate = $user->getRegistration(); - $gender = MediaWikiServices::getInstance()->getGenderCache()->getGenderOf( $user, __METHOD__ ); - - if ( $gender === 'male' ) { - $msgGender = '♂'; - } elseif ( $gender === 'female' ) { - $msgGender = '♀'; - } - if ( isset( $msgGender ) ) { - $tagline .= "$msgGender"; - } - - if ( $editCount ) { - $msgEditCount = $skin->msg( 'usereditcount' )->numParams( sprintf( '%s', number_format( $editCount, 0 ) ) ); - $editCountHref = SpecialPage::getTitleFor( 'Contributions', $user )->getLocalURL(); - $tagline .= "$msgEditCount"; - } - - if ( is_string( $regDate ) ) { - $regDateTs = wfTimestamp( TS_UNIX, $regDate ); - $msgRegDate = $skin->msg( 'citizen-tagline-user-regdate', $skin->getLanguage()->userDate( new MWTimestamp( $regDate ), $skin->getUser() ), $user ); - $tagline .= "$msgRegDate"; - } - - $tagline .= '
    '; - return $tagline; - } - return null; - } - - /** - * Return new User object based on username or IP address. - * Based on MinervaNeue - * - * @param Title $title - * @return User|null - */ - private function buildPageUserObject( $title ) { - $titleText = $title->getText(); - $user = $this->user; - - if ( IPUtils::isIPAddress( $titleText ) ) { - return $user->newFromAnyId( null, $titleText, null ); - } - - $userIdentity = MediaWikiServices::getInstance()->getUserIdentityLookup()->getUserIdentityByName( $titleText ); - if ( $userIdentity && $userIdentity->isRegistered() ) { - return $user->newFromId( $userIdentity->getId() ); - } - - return null; - } -} diff --git a/includes/SkinCitizen.php b/includes/SkinCitizen.php index 05a3b0c43..86ccf804b 100644 --- a/includes/SkinCitizen.php +++ b/includes/SkinCitizen.php @@ -23,15 +23,17 @@ namespace MediaWiki\Skins\Citizen; +use MediaWiki\Skins\Citizen\Components\CitizenComponentFooter; +use MediaWiki\Skins\Citizen\Components\CitizenComponentMainMenu; +use MediaWiki\Skins\Citizen\Components\CitizenComponentPageFooter; +use MediaWiki\Skins\Citizen\Components\CitizenComponentPageHeading; +use MediaWiki\Skins\Citizen\Components\CitizenComponentPageSidebar; +use MediaWiki\Skins\Citizen\Components\CitizenComponentSearchBox; +use MediaWiki\Skins\Citizen\Components\CitizenComponentSiteStats; +use MediaWiki\Skins\Citizen\Components\CitizenComponentUserInfo; use MediaWiki\Skins\Citizen\Partials\BodyContent; -use MediaWiki\Skins\Citizen\Partials\Drawer; -use MediaWiki\Skins\Citizen\Partials\Footer; -use MediaWiki\Skins\Citizen\Partials\Header; use MediaWiki\Skins\Citizen\Partials\Metadata; -use MediaWiki\Skins\Citizen\Partials\PageTitle; use MediaWiki\Skins\Citizen\Partials\PageTools; -use MediaWiki\Skins\Citizen\Partials\Sidebar; -use MediaWiki\Skins\Citizen\Partials\Tagline; use MediaWiki\Skins\Citizen\Partials\Theme; use SkinMustache; use SkinTemplate; @@ -70,60 +72,76 @@ protected function runOnSkinTemplateNavigationHooks( SkinTemplate $skin, &$conte * @inheritDoc */ public function getTemplateData(): array { - $data = []; $parentData = parent::getTemplateData(); - $header = new Header( $this ); - $drawer = new Drawer( $this ); - $pageTitle = new PageTitle( $this ); - $tagline = new Tagline( $this ); + $config = $this->getConfig(); + $localizer = $this->getContext(); + $out = $this->getOutput(); + $title = $this->getTitle(); + $user = $this->getUser(); + $pageLang = $title->getPageLanguage(); + $isRegistered = $user->isRegistered(); + $isTemp = $user->isTemp(); + $bodycontent = new BodyContent( $this ); - $sidebar = new Sidebar( $this ); - $footer = new Footer( $this ); $tools = new PageTools( $this ); - // Naming conventions for Mustache parameters. - // - // Value type (first segment): - // - Prefix "is" or "has" for boolean values. - // - Prefix "msg-" for interface message text. - // - Prefix "html-" for raw HTML. - // - Prefix "data-" for an array of template parameters that should be passed directly - // to a template partial. - // - Prefix "array-" for lists of any values. - // - // Source of value (first or second segment) - // - Segment "page-" for data relating to the current page (e.g. Title, WikiPage, or OutputPage). - // - Segment "hook-" for any thing generated from a hook. - // It should be followed by the name of the hook in hyphenated lowercase. - // - // Conditionally used values must use null to indicate absence (not false or ''). - - $data += [ - // Booleans - 'toc-enabled' => !empty( $parentData['data-toc'] ), - // Data objects - 'data-sitestats' => $drawer->getSiteStatsData(), - 'data-user-info' => $header->getUserInfoData( $parentData['data-portlets']['data-user-page'] ), - // HTML strings - 'html-title-heading--formatted' => $pageTitle->decorateTitle( $parentData['html-title-heading'] ), - 'html-citizen-jumptotop' => $parentData['msg-citizen-jumptotop'] . ' [home]', - 'html-body-content--formatted' => $bodycontent->decorateBodyContent( $parentData['html-body-content'] ), - 'html-tagline' => $tagline->getTagline(), - // Messages - // Needed to be parsed here as it should be wikitext - 'msg-citizen-footer-desc' => $this->msg( "citizen-footer-desc" )->inContentLanguage()->parse(), - 'msg-citizen-footer-tagline' => $this->msg( "citizen-footer-tagline" )->inContentLanguage()->parse(), - // Decorate data provided by core - 'data-search-box' => $header->decorateSearchBoxData( $parentData['data-search-box'] ), - 'data-main-menu' => $drawer->decorateMainMenuData( $parentData['data-portlets-sidebar'] ), - 'data-footer' => $footer->decorateFooterData( $parentData['data-footer'] ), + $components = [ + 'data-footer' => new CitizenComponentFooter( + $localizer, + $parentData['data-footer'] + ), + 'data-main-menu' => new CitizenComponentMainMenu( $parentData['data-portlets-sidebar'] ), + 'data-page-footer' => new CitizenComponentPageFooter( + $localizer, + $parentData['data-footer']['data-info'] + ), + 'data-page-heading' => new CitizenComponentPageHeading( + $localizer, + $out, + $pageLang, + $title, + $parentData['html-title-heading'], + $user + ), + 'data-page-sidebar' => new CitizenComponentPageSidebar( + $localizer, + $out, + $pageLang, + $title, + $user + ), + 'data-search-box' => new CitizenComponentSearchBox( + $parentData['data-search-box'], + $this + ), + 'data-site-stats' => new CitizenComponentSiteStats( + $config, + $localizer, + $pageLang + ), + 'data-user-info' => new CitizenComponentUserInfo( + $isRegistered, + $isTemp, + $localizer, + $title, + $user, + $parentData['data-portlets']['data-user-page'] + ) ]; - $data += $sidebar->getSidebarData( $parentData ); - $data += $tools->getPageToolsData( $parentData ); + foreach ( $components as $key => $component ) { + // Array of components or null values. + if ( $component ) { + $parentData[$key] = $component->getTemplateData(); + } + } - return array_merge( $parentData, $data ); + return array_merge( $parentData, [ + // Booleans + 'toc-enabled' => !empty( $parentData['data-toc'] ), + 'html-body-content--formatted' => $bodycontent->decorateBodyContent( $parentData['html-body-content'] ) + ], $tools->getPageToolsData( $parentData ) ); } /** diff --git a/resources/skins.citizen.scripts/skin.js b/resources/skins.citizen.scripts/skin.js index 933f189d8..857a75100 100644 --- a/resources/skins.citizen.scripts/skin.js +++ b/resources/skins.citizen.scripts/skin.js @@ -31,17 +31,17 @@ function initStickyHeader( document ) { 10 ); - const sentinel = document.getElementById( 'citizen-body-header-sticky-sentinel' ); + const sentinel = document.getElementById( 'citizen-page-header-sticky-sentinel' ); // In some pages we use display:none to disable the sticky header // Do not start observer if it is set to display:none if ( sentinel && getComputedStyle( sentinel ).getPropertyValue( 'display' ) !== 'none' ) { const observer = scrollObserver.initIntersectionObserver( () => { - document.body.classList.add( 'citizen-body-header--sticky' ); + document.body.classList.add( 'citizen-page-header--sticky' ); }, () => { - document.body.classList.remove( 'citizen-body-header--sticky' ); + document.body.classList.remove( 'citizen-page-header--sticky' ); } ); observer.observe( sentinel ); diff --git a/resources/skins.citizen.styles/common/print.less b/resources/skins.citizen.styles/common/print.less index 4aca2a845..c6d3afe99 100644 --- a/resources/skins.citizen.styles/common/print.less +++ b/resources/skins.citizen.styles/common/print.less @@ -6,7 +6,7 @@ .page-actions, .section-toggle, .mw-editsection, -.mw-body-footer, +.citizen-page-footer, #mw-data-after-content, #footer-desc, #footer-places, diff --git a/resources/skins.citizen.styles/components/ContentFooter.less b/resources/skins.citizen.styles/components/PageFooter.less similarity index 95% rename from resources/skins.citizen.styles/components/ContentFooter.less rename to resources/skins.citizen.styles/components/PageFooter.less index 998b87165..caaad2df0 100644 --- a/resources/skins.citizen.styles/components/ContentFooter.less +++ b/resources/skins.citizen.styles/components/PageFooter.less @@ -1,4 +1,4 @@ -.mw-body-footer { +.citizen-page-footer { display: flex; flex-direction: column; grid-area: footer; diff --git a/resources/skins.citizen.styles/components/ContentHeader.less b/resources/skins.citizen.styles/components/PageHeader.less similarity index 100% rename from resources/skins.citizen.styles/components/ContentHeader.less rename to resources/skins.citizen.styles/components/PageHeader.less diff --git a/resources/skins.citizen.styles/components/ContentSidebar.less b/resources/skins.citizen.styles/components/PageSidebar.less similarity index 96% rename from resources/skins.citizen.styles/components/ContentSidebar.less rename to resources/skins.citizen.styles/components/PageSidebar.less index bf538f405..c89588cab 100644 --- a/resources/skins.citizen.styles/components/ContentSidebar.less +++ b/resources/skins.citizen.styles/components/PageSidebar.less @@ -1,4 +1,4 @@ -.citizen-body-sidebar { +.citizen-page-sidebar { --size-icon: 1rem; @media ( min-width: @min-width-breakpoint-desktop ) { diff --git a/resources/skins.citizen.styles/components/StickyHeader.less b/resources/skins.citizen.styles/components/StickyHeader.less index ad5f44bc3..8063a6eec 100644 --- a/resources/skins.citizen.styles/components/StickyHeader.less +++ b/resources/skins.citizen.styles/components/StickyHeader.less @@ -1,4 +1,4 @@ -#citizen-body-header-sticky-sentinel { +#citizen-page-header-sticky-sentinel { right: 0; left: 0; grid-area: content; // align right above content @@ -10,13 +10,13 @@ .ve-activated, .action-edit { // HACK: So sticky header will never trigger in edit action - #citizen-body-header-sticky-sentinel { + #citizen-page-header-sticky-sentinel { display: none; } } -.citizen-body-header--sticky { - .mw-body-header { +.citizen-page-header--sticky { + .citizen-page-header { flex-wrap: nowrap; padding-bottom: var( --space-md ); white-space: nowrap; @@ -27,7 +27,7 @@ } } - .page-heading { + .citizen-page-heading { position: relative; min-width: 0; } @@ -56,14 +56,14 @@ // Hide sticky header on scroll down on smaller screens @media ( max-width: @max-width-breakpoint-tablet ) { - .citizen-body-header--sticky { - .mw-body-header { + .citizen-page-header--sticky { + .citizen-page-header { transition: var( --transition-menu ); transition-property: transform; } &.citizen-scroll--down { - .mw-body-header { + .citizen-page-header { transform: translateY( -150% ); } } @@ -72,8 +72,8 @@ // Make sticky header more compact if there are less screen estate @media ( max-height: 800px ) { - .citizen-body-header--sticky { - .mw-body-header { + .citizen-page-header--sticky { + .citizen-page-header { padding-top: var( --space-sm ); padding-bottom: var( --space-sm ); } diff --git a/resources/skins.citizen.styles/components/TableOfContents.less b/resources/skins.citizen.styles/components/TableOfContents.less index 7179ebced..dfb94a74f 100644 --- a/resources/skins.citizen.styles/components/TableOfContents.less +++ b/resources/skins.citizen.styles/components/TableOfContents.less @@ -89,7 +89,7 @@ } // Sticky header styles -.citizen-body-header--sticky { +.citizen-page-header--sticky { .citizen-toc__top.citizen-toc__link { height: 2rem; // 1rem text + 1rem padding padding-top: var( --space-xs ); @@ -200,7 +200,7 @@ overscroll-behavior: contain; // Sticky header styles - .citizen-body-header--sticky & { + .citizen-page-header--sticky & { --toc-margin-top: ~'calc( var( --header-size ) + var( --space-xxl ) )'; // Sticky header is shorter without buttons diff --git a/resources/skins.citizen.styles/components/UserInfo.less b/resources/skins.citizen.styles/components/UserInfo.less new file mode 100644 index 000000000..045e3fabe --- /dev/null +++ b/resources/skins.citizen.styles/components/UserInfo.less @@ -0,0 +1,77 @@ +.citizen-userInfo { + padding: var( --space-sm ) var( --space-md ) var( --space-md ); + margin-bottom: var( --space-xs ); + border-bottom: 1px solid var( --border-color-base ); + + &-title { + font-weight: var( --font-weight-medium ); + } + + &-title > div, + #pt-userpage-2 > a, + #pt-tmpuserpage-2 > span { + padding-top: var( --space-xxs ); + padding-bottom: var( --space-xxs ); + font-size: var( --font-size-medium ); + color: var( --color-base--emphasized ); + } + + #pt-userpage-2 > a > span { + display: flex; + flex-grow: 1; + flex-wrap: wrap; + gap: var( --space-xs ); + align-items: baseline; + } + + #pt-tmpuserpage-2 > span { + display: block; + } + + #pt-userpage-username { + font-size: var( --font-size-small ); + color: var( --color-base--subtle ); + } + + &-text { + font-size: var( --font-size-small ); + line-height: var( --line-height-sm ); + color: var( --color-base--subtle ); + + + .citizen-userInfo-edits { + margin-top: var( --space-sm ); + } + } + + &-usergroups { + display: flex; + flex-wrap: wrap; + gap: 0 var( --space-xs ); + margin: 0; + list-style: none; + + a { + color: var( --color-base--subtle ); + } + } + + &-edits { + &-count { + font-size: 1.75rem; + font-weight: var( --font-weight-medium ); + color: var( --color-base--emphasized ); + } + + &-label { + font-size: var( --font-size-small ); + color: var( --color-base--subtle ); + letter-spacing: 0.05em; + } + } +} + +#pt-userpage-2 > a { + padding-right: 0; + padding-left: 0; + border-radius: var( --border-radius--small ); +} diff --git a/resources/skins.citizen.styles/components/Usermenu.less b/resources/skins.citizen.styles/components/Usermenu.less index 4e7778ff3..eba8c4ef4 100644 --- a/resources/skins.citizen.styles/components/Usermenu.less +++ b/resources/skins.citizen.styles/components/Usermenu.less @@ -6,79 +6,6 @@ .mixin-screen-reader-text; } } - - &__header { - margin-bottom: var( --space-xs ); - border-bottom: 1px solid var( --border-color-base ); - } -} - -#p-user-info { - padding: var( --space-md ); - - > ul > li { - margin-right: var( --space-md ); - margin-left: var( --space-md ); - } -} - -#pt { - &-anonuserpage span, - &-tmpuserpage-2, - &-userpage-2 a { - padding: var( --space-xxs ) 0; - font-size: 1rem; - font-weight: var( --font-weight-semibold ); - color: var( --color-base--emphasized ); - word-break: break-word; - } - - &-user { - &groups { - margin-bottom: var( --space-xs ); - - ul { - display: flex; - flex-wrap: wrap; - gap: var( --space-xxs ); - margin: 0; - list-style: none; - } - - a { - color: var( --color-base--subtle ); - } - } - - &page-2, - &groups { - a { - display: block; - - &:hover { - color: var( --color-primary--hover ); - } - - &:active { - color: var( --color-primary--active ); - } - } - } - - &page { - &-username { - margin-top: var( --space-xxs ); - margin-bottom: var( --space-sm ); - font-size: var( --font-size-x-small ); - font-weight: var( --font-weight-normal ); - color: var( --color-base--subtle ); - } - } - - &contris { - font-weight: var( --font-weight-medium ); - } - } } #pt-createaccount, diff --git a/resources/skins.citizen.styles/layout.less b/resources/skins.citizen.styles/layout.less index 645b3a7a8..7cf55aa6d 100644 --- a/resources/skins.citizen.styles/layout.less +++ b/resources/skins.citizen.styles/layout.less @@ -24,7 +24,7 @@ margin: var( --space-xl ) 0; // don't collide with other components (e.g. notice) } -.mw-body-header { +.citizen-page-header { position: -webkit-sticky; position: sticky; // So that the modal are above custom sticky headers @@ -37,7 +37,7 @@ padding-top: var( --space-md ); } -.page-heading { +.citizen-page-heading { flex-grow: 1; } diff --git a/resources/skins.citizen.styles/skin.less b/resources/skins.citizen.styles/skin.less index 8a1654c73..791e33b50 100644 --- a/resources/skins.citizen.styles/skin.less +++ b/resources/skins.citizen.styles/skin.less @@ -27,20 +27,21 @@ @import 'components/Header.less'; @import 'components/Drawer.less'; @import 'components/Drawer__button.less'; - @import 'components/Usermenu.less'; + @import 'components/UserMenu.less'; @import 'components/Search.less'; @import 'components/Search__button.less'; @import 'components/Pagetools.less'; @import 'components/Menu.less'; - @import 'components/ContentHeader.less'; - @import 'components/ContentSidebar.less'; - @import 'components/ContentFooter.less'; + @import 'components/PageHeader.less'; + @import 'components/PageSidebar.less'; + @import 'components/PageFooter.less'; @import 'components/Footer.less'; @import 'components/TableOfContents.less'; @import 'components/StickyHeader.less'; @import 'components/Sitestats.less'; @import 'components/Sections.less'; @import 'components/Tables.less'; + @import 'components/UserInfo.less'; // Mediawiki.skinning // This get loaded regardless so we don't have to use skinStyles to target them diff --git a/skin.json b/skin.json index 7234723a8..66f776852 100644 --- a/skin.json +++ b/skin.json @@ -59,8 +59,8 @@ "citizen-drawer-toggle", "citizen-jumptotop", "citizen-languages-toggle", - "citizen-usermenu-toggle", "citizen-search-toggle", + "citizen-usermenu-toggle", "randompage", "search", "sitetitle", diff --git a/skinStyles/extensions/Cargo/ext.cargo.pagevalues.less b/skinStyles/extensions/Cargo/ext.cargo.pagevalues.less index 44b183bf9..5afb73b1e 100644 --- a/skinStyles/extensions/Cargo/ext.cargo.pagevalues.less +++ b/skinStyles/extensions/Cargo/ext.cargo.pagevalues.less @@ -11,7 +11,7 @@ @import '../../../resources/mixins.less'; // Disable sticky header since it collides with cargo sticky header -#citizen-body-header-sticky-sentinel { +#citizen-page-header-sticky-sentinel { display: none; } diff --git a/skinStyles/extensions/Echo/ext.echo.special.less b/skinStyles/extensions/Echo/ext.echo.special.less index 97f9e6d10..73a6d199a 100644 --- a/skinStyles/extensions/Echo/ext.echo.special.less +++ b/skinStyles/extensions/Echo/ext.echo.special.less @@ -9,7 +9,7 @@ */ // Disable sticky header since it collides with Echo sticky header -#citizen-body-header-sticky-sentinel { +#citizen-page-header-sticky-sentinel { display: none; } diff --git a/skinStyles/extensions/MediaSearch/mediasearch.styles.less b/skinStyles/extensions/MediaSearch/mediasearch.styles.less index 468f4db6b..2ff8e8ed2 100644 --- a/skinStyles/extensions/MediaSearch/mediasearch.styles.less +++ b/skinStyles/extensions/MediaSearch/mediasearch.styles.less @@ -11,7 +11,7 @@ @import '../../../resources/variables.less'; // Disable sticky header -#citizen-body-header-sticky-sentinel { +#citizen-page-header-sticky-sentinel { display: none; } diff --git a/skinStyles/extensions/SemanticMediaWiki/ext.smw.ask.styles.less b/skinStyles/extensions/SemanticMediaWiki/ext.smw.ask.styles.less index 730db34d7..65ae05cb2 100644 --- a/skinStyles/extensions/SemanticMediaWiki/ext.smw.ask.styles.less +++ b/skinStyles/extensions/SemanticMediaWiki/ext.smw.ask.styles.less @@ -9,7 +9,7 @@ */ // Disable sticky header since it collides with anchor and adds no value -#citizen-body-header-sticky-sentinel { +#citizen-page-header-sticky-sentinel { display: none; } diff --git a/skinStyles/extensions/SemanticMediaWiki/ext.smw.browse.styles.less b/skinStyles/extensions/SemanticMediaWiki/ext.smw.browse.styles.less index 95311b85b..865a3e6ff 100644 --- a/skinStyles/extensions/SemanticMediaWiki/ext.smw.browse.styles.less +++ b/skinStyles/extensions/SemanticMediaWiki/ext.smw.browse.styles.less @@ -12,7 +12,7 @@ //@import '../../../resources/variables.less'; // Disable sticky header since it collides with anchor and adds no value -#citizen-body-header-sticky-sentinel { +#citizen-page-header-sticky-sentinel { display: none; } diff --git a/skinStyles/extensions/SemanticMediaWiki/ext.smw.page.styles.less b/skinStyles/extensions/SemanticMediaWiki/ext.smw.page.styles.less index 4fd3a7b36..4f7ec1706 100644 --- a/skinStyles/extensions/SemanticMediaWiki/ext.smw.page.styles.less +++ b/skinStyles/extensions/SemanticMediaWiki/ext.smw.page.styles.less @@ -11,7 +11,7 @@ @import '../../../resources/mixins.less'; // Disable sticky header since it collides with anchor and adds no value -#citizen-body-header-sticky-sentinel { +#citizen-page-header-sticky-sentinel { display: none; } diff --git a/skinStyles/extensions/SemanticMediaWiki/smw.jsonview.less b/skinStyles/extensions/SemanticMediaWiki/smw.jsonview.less index 87504e4a0..afae911fd 100644 --- a/skinStyles/extensions/SemanticMediaWiki/smw.jsonview.less +++ b/skinStyles/extensions/SemanticMediaWiki/smw.jsonview.less @@ -9,7 +9,7 @@ */ // Disable sticky header since it collides with anchor and adds no value -#citizen-body-header-sticky-sentinel { +#citizen-page-header-sticky-sentinel { display: none; } diff --git a/skinStyles/extensions/UploadWizard/ext.uploadWizard.less b/skinStyles/extensions/UploadWizard/ext.uploadWizard.less index bb9e5e199..b4bd120e5 100644 --- a/skinStyles/extensions/UploadWizard/ext.uploadWizard.less +++ b/skinStyles/extensions/UploadWizard/ext.uploadWizard.less @@ -12,7 +12,7 @@ @import '../../../resources/mixins.less'; // Disable sticky header -#citizen-body-header-sticky-sentinel { +#citizen-page-header-sticky-sentinel { display: none; } diff --git a/skinStyles/extensions/VisualEditor/ext.visualEditor.desktopArticleTarget.init.less b/skinStyles/extensions/VisualEditor/ext.visualEditor.desktopArticleTarget.init.less index 8d0709f62..f5bce5b5b 100644 --- a/skinStyles/extensions/VisualEditor/ext.visualEditor.desktopArticleTarget.init.less +++ b/skinStyles/extensions/VisualEditor/ext.visualEditor.desktopArticleTarget.init.less @@ -21,7 +21,7 @@ } // Body header should have a lower z-index than VE elements - .mw-body-header { + .citizen-page-header { z-index: 1; } } diff --git a/skinStyles/mediawiki/action/mediawiki.action.edit.styles.less b/skinStyles/mediawiki/action/mediawiki.action.edit.styles.less index 9534c330a..ad918e9a3 100644 --- a/skinStyles/mediawiki/action/mediawiki.action.edit.styles.less +++ b/skinStyles/mediawiki/action/mediawiki.action.edit.styles.less @@ -42,6 +42,6 @@ } // Disable sticky header -#citizen-body-header-sticky-sentinel { +#citizen-page-header-sticky-sentinel { display: none; } diff --git a/skinStyles/mediawiki/action/mediawiki.action.view.filepage.less b/skinStyles/mediawiki/action/mediawiki.action.view.filepage.less index 2953e0e14..3f428692b 100644 --- a/skinStyles/mediawiki/action/mediawiki.action.view.filepage.less +++ b/skinStyles/mediawiki/action/mediawiki.action.view.filepage.less @@ -12,7 +12,7 @@ @import '../../../resources/variables.less'; // Custom sticky header handling -#citizen-body-header-sticky-sentinel { +#citizen-page-header-sticky-sentinel { display: none; } diff --git a/skinStyles/mediawiki/special/mediawiki.special.changeslist.less b/skinStyles/mediawiki/special/mediawiki.special.changeslist.less index c6f58c306..213808e8c 100644 --- a/skinStyles/mediawiki/special/mediawiki.special.changeslist.less +++ b/skinStyles/mediawiki/special/mediawiki.special.changeslist.less @@ -123,7 +123,7 @@ body:not( .mw-rcfilters-ui-initialized ) .mw-rcfilters-head { } // Disable sticky header - #citizen-body-header-sticky-sentinel { + #citizen-page-header-sticky-sentinel { display: none; } } diff --git a/skinStyles/mediawiki/special/mediawiki.special.search.styles.less b/skinStyles/mediawiki/special/mediawiki.special.search.styles.less index cea4b224a..d7e05a696 100644 --- a/skinStyles/mediawiki/special/mediawiki.special.search.styles.less +++ b/skinStyles/mediawiki/special/mediawiki.special.search.styles.less @@ -12,7 +12,7 @@ @import '../../../resources/variables.less'; // Disable default sticky header -#citizen-body-header-sticky-sentinel { +#citizen-page-header-sticky-sentinel { display: none; } diff --git a/templates/ContentFooter.mustache b/templates/ContentFooter.mustache deleted file mode 100644 index e92a817ca..000000000 --- a/templates/ContentFooter.mustache +++ /dev/null @@ -1,9 +0,0 @@ -{{! - -}} - \ No newline at end of file diff --git a/templates/ContentHeader.mustache b/templates/ContentHeader.mustache deleted file mode 100644 index ed1e04959..000000000 --- a/templates/ContentHeader.mustache +++ /dev/null @@ -1,19 +0,0 @@ -{{! - Indicator[] array-indicators wiki-defined badges such as "good article", - "featured article". An empty array if none are defined. - string html-title-heading--formatted - string html-tagline - string html-citizen-jumptotop Jump to top title text -}} -
    -
    -
    - {{{html-title-heading--formatted}}} - {{>Indicators}} -
    -
    {{{html-tagline}}}
    - -
    - {{>PageTools}} -
    -
    diff --git a/templates/ContentSidebar.mustache b/templates/ContentSidebar.mustache deleted file mode 100644 index d674eb69d..000000000 --- a/templates/ContentSidebar.mustache +++ /dev/null @@ -1,7 +0,0 @@ -{{! - -}} -
    - {{#data-sidebar-lastmod}}{{>Menu}}{{/data-sidebar-lastmod}} - {{>TableOfContents}} -
    \ No newline at end of file diff --git a/templates/Drawer.mustache b/templates/Drawer.mustache index ad9618c62..12c9d478c 100644 --- a/templates/Drawer.mustache +++ b/templates/Drawer.mustache @@ -16,15 +16,12 @@
    {{>Drawer__logo}}
    - {{#data-sitestats}}{{>Drawer__siteStats}}{{/data-sitestats}} + {{#data-site-stats}}{{>SiteStats}}{{/data-site-stats}}
    {{msg-sitetitle}}
    {{#data-main-menu}} -
    - {{#data-portlets-first}}{{>Menu}}{{/data-portlets-first}} - {{#array-portlets-rest}}{{>Menu}}{{/array-portlets-rest}} -
    + {{>MainMenu}} {{/data-main-menu}} \ No newline at end of file diff --git a/templates/MainMenu.mustache b/templates/MainMenu.mustache new file mode 100644 index 000000000..ad8bef322 --- /dev/null +++ b/templates/MainMenu.mustache @@ -0,0 +1,4 @@ +
    + {{#data-portlets-first}}{{>Menu}}{{/data-portlets-first}} + {{#array-portlets-rest}}{{>Menu}}{{/array-portlets-rest}} +
    \ No newline at end of file diff --git a/templates/MenuListItem.mustache b/templates/MenuListItem.mustache index 5791122f7..850293c77 100644 --- a/templates/MenuListItem.mustache +++ b/templates/MenuListItem.mustache @@ -1,4 +1,4 @@ -
  • {{! +
  • {{! }}{{#array-links}}{{! }}{{>Link}}{{! }}{{/array-links}}{{! diff --git a/templates/PageFooter.mustache b/templates/PageFooter.mustache new file mode 100644 index 000000000..49ceaeda7 --- /dev/null +++ b/templates/PageFooter.mustache @@ -0,0 +1,9 @@ +{{! + +}} +
    + {{{html-categories}}} + {{#data-page-footer}} + {{>PageFooter__item}} + {{/data-page-footer}} +
    \ No newline at end of file diff --git a/templates/ContentFooter__item.mustache b/templates/PageFooter__item.mustache similarity index 100% rename from templates/ContentFooter__item.mustache rename to templates/PageFooter__item.mustache diff --git a/templates/PageHeader.mustache b/templates/PageHeader.mustache new file mode 100644 index 000000000..7daf17aec --- /dev/null +++ b/templates/PageHeader.mustache @@ -0,0 +1,5 @@ +
    + {{#data-page-heading}}{{>PageHeading}}{{/data-page-heading}} + {{>PageTools}} +
    +
    diff --git a/templates/PageHeading.mustache b/templates/PageHeading.mustache new file mode 100644 index 000000000..88648c3ca --- /dev/null +++ b/templates/PageHeading.mustache @@ -0,0 +1,15 @@ +{{! + Indicator[] array-indicators wiki-defined badges such as "good article", + "featured article". An empty array if none are defined. + string html-title-heading--formatted + string html-tagline + string html-citizen-jumptotop Jump to top title text +}} +
    +
    + {{{html-title-heading}}} + {{>Indicators}} +
    +
    {{{html-tagline}}}
    + +
    \ No newline at end of file diff --git a/templates/PageSidebar.mustache b/templates/PageSidebar.mustache new file mode 100644 index 000000000..974c5c77a --- /dev/null +++ b/templates/PageSidebar.mustache @@ -0,0 +1,9 @@ +{{! + +}} +
    + {{#data-page-sidebar}} + {{#data-page-sidebar-lastmod}}{{>Menu}}{{/data-page-sidebar-lastmod}} + {{/data-page-sidebar}} + {{>TableOfContents}} +
    \ No newline at end of file diff --git a/templates/Drawer__siteStats.mustache b/templates/SiteStats.mustache similarity index 78% rename from templates/Drawer__siteStats.mustache rename to templates/SiteStats.mustache index 4886161da..a5dc268a1 100644 --- a/templates/Drawer__siteStats.mustache +++ b/templates/SiteStats.mustache @@ -1,8 +1,8 @@
    - {{#array-drawer-sitestats-item}} + {{#array-sitestats-items}}
    {{value}}
    - {{/array-drawer-sitestats-item}} + {{/array-sitestats-items}}
    \ No newline at end of file diff --git a/templates/UserInfo.mustache b/templates/UserInfo.mustache new file mode 100644 index 000000000..1289d60ad --- /dev/null +++ b/templates/UserInfo.mustache @@ -0,0 +1,20 @@ +
    +
    + {{#title}}
    {{.}}
    {{/title}} + {{#data-user-page}}{{>Menu}}{{/data-user-page}} +
    +
    + {{#text}}
    {{.}}
    {{/text}} + {{#data-user-groups}} +
      + {{#array-list-items}}{{>MenuListItem}}{{/array-list-items}} +
    + {{/data-user-groups}} +
    + {{#data-user-edit}} +
    +
    {{count}}
    +
    {{label}}
    +
    + {{/data-user-edit}} +
    \ No newline at end of file diff --git a/templates/UserMenu.mustache b/templates/UserMenu.mustache index e731c94fe..1df22a3f1 100644 --- a/templates/UserMenu.mustache +++ b/templates/UserMenu.mustache @@ -19,9 +19,7 @@ {{msg-citizen-usermenu-toggle}} diff --git a/templates/skin.mustache b/templates/skin.mustache index 5982ca4eb..182c12b31 100644 --- a/templates/skin.mustache +++ b/templates/skin.mustache @@ -12,18 +12,20 @@ {{>Header}}
    -
    {{{html-site-notice}}}
    +
    +
    {{{html-site-notice}}}
    +
    - {{>ContentHeader}} + {{>PageHeader}}
    {{{html-subtitle}}}
    {{#html-undelete-link}}
    {{{.}}}
    {{/html-undelete-link}} {{{html-user-message}}} {{{html-body-content--formatted}}}
    - {{#toc-enabled}}{{>ContentSidebar}}{{/toc-enabled}} - {{>ContentFooter}} + {{#toc-enabled}}{{>PageSidebar}}{{/toc-enabled}} + {{>PageFooter}}
    {{{html-after-content}}} diff --git a/tests/phpunit/Partials/DrawerTest.php b/tests/phpunit/Partials/DrawerTest.php deleted file mode 100644 index 1d637aeee..000000000 --- a/tests/phpunit/Partials/DrawerTest.php +++ /dev/null @@ -1,85 +0,0 @@ -assertEmpty( $partial->decorateMainMenuData( [ - 'array-portlets-rest' => [], - ] )['array-portlets-rest'] ); - } - - /** - * @covers \MediaWiki\Skins\Citizen\Partials\Drawer::decorateMainMenuData - * @return void - */ - public function testDecorateMainMenuRemovePageTools() { - $partial = new Drawer( new SkinCitizen() ); - - $mainMenuData = [ - 'array-portlets-rest' => [ - [ 'id' => 'foo' ], - [ 'id' => 'p-tb' ], - ], - ]; - - $this->assertNotEmpty( $partial->decorateMainMenuData( $mainMenuData ) ); - $this->assertArrayHasKey( 'array-portlets-rest', $partial->decorateMainMenuData( $mainMenuData ) ); - $this->assertNotContains( [ 'id' => 'pt-tb' ], $partial->decorateMainMenuData( $mainMenuData )['array-portlets-rest'] ); - } - - /** - * @covers \MediaWiki\Skins\Citizen\Partials\Drawer::getSiteStatsData - * @return void - */ - public function testGetSiteStatsDataDisabled() { - $this->overrideConfigValues( [ - 'CitizenEnableDrawerSiteStats' => false, - ] ); - - $partial = new Drawer( new SkinCitizen() ); - $this->assertEmpty( $partial->getSiteStatsData() ); - } - - /** - * @covers \MediaWiki\Skins\Citizen\Partials\Drawer::getSiteStatsData - * @covers \MediaWiki\Skins\Citizen\Partials\Drawer::getSiteStatValue - * @return void - */ - public function testGetSiteStatsDataNoFormat() { - $this->overrideConfigValues( [ - 'CitizenEnableDrawerSiteStats' => true, - 'CitizenUseNumberFormatter' => false, - ] ); - - $partial = new Drawer( new SkinCitizen() ); - $data = $partial->getSiteStatsData(); - - $this->assertArrayHasKey( 'array-drawer-sitestats-item', $data ); - $this->assertCount( 4, $data['array-drawer-sitestats-item'] ); - - foreach ( $data['array-drawer-sitestats-item'] as $stat ) { - $this->assertArrayHasKey( 'id', $stat ); - $this->assertArrayHasKey( 'icon', $stat ); - $this->assertArrayHasKey( 'value', $stat ); - $this->assertArrayHasKey( 'label', $stat ); - - $this->assertContains( $stat['id'], [ 'articles', 'images', 'users', 'edits' ] ); - } - } -} diff --git a/tests/phpunit/Partials/FooterTest.php b/tests/phpunit/Partials/FooterTest.php deleted file mode 100644 index 055a13188..000000000 --- a/tests/phpunit/Partials/FooterTest.php +++ /dev/null @@ -1,43 +0,0 @@ - [ - 'array-items' => [ - [ 'name' => 'copyright' ] - ] - ] - ]; - - $out = $partial->decorateFooterData( $data ); - - $this->assertArraySubmapSame( [ - 'data-info' => [ - 'array-items' => [ - [ - 'name' => 'copyright', - 'label' => wfMessage( 'citizen-page-info-copyright' )->text() - ] - ], - ] - ], $out ); - } -} diff --git a/tests/phpunit/Partials/HeaderTest.php b/tests/phpunit/Partials/HeaderTest.php deleted file mode 100644 index 913bd6873..000000000 --- a/tests/phpunit/Partials/HeaderTest.php +++ /dev/null @@ -1,39 +0,0 @@ -decorateSearchBoxData( [] ); - - $this->assertArrayHasKey( 'msg-citizen-search-toggle-shortcut', $out ); - $this->assertEquals( '[/]', $out['msg-citizen-search-toggle-shortcut'] ); - } - - /** - * @covers \MediaWiki\Skins\Citizen\Partials\Header::getUserInfoData - * @covers \MediaWiki\Skins\Citizen\Partials\Header::getUserPageHTML - * @covers \MediaWiki\Skins\Citizen\Partials\Header::getUserGroupsHTML - * @covers \MediaWiki\Skins\Citizen\Partials\Header::getUserContributionsHTML - * @return void - */ - public function testGetUserInfoData() { - $partial = new Header( new SkinCitizen() ); - $out = $partial->getUserInfoData( [] ); - - $this->assertArrayHasKey( 'id', $out ); - } -} diff --git a/tests/phpunit/Partials/PageTitleTest.php b/tests/phpunit/Partials/PageTitleTest.php deleted file mode 100644 index e4d24a2b5..000000000 --- a/tests/phpunit/Partials/PageTitleTest.php +++ /dev/null @@ -1,57 +0,0 @@ -setTitle( $title ); - $partial = new PageTitle( new SkinCitizen() ); - - $data = sprintf( - '

    %s

    ', - 'Foo Title (paren)' - ); - $text = $partial->decorateTitle( $data ); - - $this->assertStringNotContainsString( 'mw-page-title-parenthesis', $text ); - } - - /** - * @covers \MediaWiki\Skins\Citizen\Partials\PageTitle - * @return void - * @throws MWException - */ - public function testDecorateTitle() { - $title = Title::makeTitle( NS_MAIN, 'Foo' ); - RequestContext::resetMain(); - RequestContext::getMain()->setTitle( $title ); - $partial = new PageTitle( new SkinCitizen() ); - - $data = sprintf( - '

    %s

    ', - 'Foo Title (paren)' - ); - $text = $partial->decorateTitle( $data ); - - $this->assertStringContainsString( 'mw-page-title-parenthesis', $text ); - } -} diff --git a/tests/phpunit/Partials/TaglineTest.php b/tests/phpunit/Partials/TaglineTest.php deleted file mode 100644 index f135ebb23..000000000 --- a/tests/phpunit/Partials/TaglineTest.php +++ /dev/null @@ -1,70 +0,0 @@ -assertEmpty( $partial->getTagline() ); - } - - /** - * @covers \MediaWiki\Skins\Citizen\Partials\Tagline - * @return void - */ - public function testGetTagLineShortDesc() { - $title = Title::makeTitle( NS_MAIN, 'Foo' ); - - RequestContext::resetMain(); - - $out = new OutputPage( RequestContext::getMain() ); - $out->setProperty( 'shortdesc', '' ); - - RequestContext::getMain()->setOutput( $out ); - RequestContext::getMain()->setTitle( $title ); - - $partial = new Tagline( new SkinCitizen() ); - - $this->assertEquals( '', $partial->getTagline() ); - } - - /** - * @covers \MediaWiki\Skins\Citizen\Partials\Tagline - * @return void - */ - public function testGetTagLineNoNSText() { - $title = $this->getMockBuilder( Title::class )->disableOriginalConstructor()->getMock(); - $title->expects( $this->once() )->method( 'getNsText' )->willReturn( false ); - - RequestContext::resetMain(); - - $out = new OutputPage( RequestContext::getMain() ); - $out->setProperty( 'shortdesc', null ); - - RequestContext::getMain()->setOutput( $out ); - RequestContext::getMain()->setTitle( $title ); - - $partial = new Tagline( new SkinCitizen() ); - - $this->assertEquals( - wfMessage( 'tagline' )->text(), - $partial->getTagline() - ); - } -} diff --git a/tests/phpunit/Unit/Components/CitizenComponentLinkTest.php b/tests/phpunit/Unit/Components/CitizenComponentLinkTest.php new file mode 100644 index 000000000..8f10c7126 --- /dev/null +++ b/tests/phpunit/Unit/Components/CitizenComponentLinkTest.php @@ -0,0 +1,59 @@ +createMock( MessageLocalizer::class ); + // Adjusting mock to prevent calling the service container. + $localizer->method( 'msg' ) + ->willReturnCallback( function ( $key ) use ( $accessKeyHint ) { + // Directly create Message object without accessing real message texts + // to avoid 'Premature access to service container' error. + return $this->createConfiguredMock( Message::class, [ + 'exists' => true, + 'text' => $key === $accessKeyHint . '-label' ? 'Mock aria label' : $key, + '__toString' => 'Mock aria label', + ] ); + } ); + + // Create the component + $linkComponent = new CitizenComponentLink( $href, $text, $icon, $localizer, $accessKeyHint ); + $actual = $linkComponent->getTemplateData(); + + // Assert the expected values + $this->assertEquals( $icon, $actual['icon'] ); + $this->assertEquals( $text, $actual['text'] ); + $this->assertEquals( $href, $actual['href'] ); + + // New assertions for HTML attributes + $expectedTitle = "tooltip-sample-accesskeyword-separatorbrackets"; + $expectedAriaLabel = "Mock aria label"; + $attributesString = $actual['html-attributes']; + + // Assert that the expected attributes are present in the string + $this->assertStringContainsString( 'title="' . $expectedTitle . '"', $attributesString ); + $this->assertStringContainsString( 'aria-label="' . $expectedAriaLabel . '"', $attributesString ); + } +} diff --git a/tests/phpunit/Unit/Components/CitizenComponentMainMenuTest.php b/tests/phpunit/Unit/Components/CitizenComponentMainMenuTest.php new file mode 100644 index 000000000..3bc56cbec --- /dev/null +++ b/tests/phpunit/Unit/Components/CitizenComponentMainMenuTest.php @@ -0,0 +1,66 @@ +assertInstanceOf( CitizenComponent::class, $mainMenu ); + } + + /** + * @return array[] + */ + public function provideMainMenuData(): array { + return [ + [ + 'sidebarData' => [ + 'data-portlets-first' => [], + 'array-portlets-rest' => [], + ] + ] + ]; + } + + /** + * @covers ::getTemplateData + * @dataProvider provideMainMenuData + */ + public function testGetTemplateData( array $sidebarData ) { + // Create a new CitizenComponentMainMenu object + $mainMenu = new CitizenComponentMainMenu( $sidebarData ); + + // Call the getTemplateData method + $templateData = $mainMenu->getTemplateData(); + + // Assert the structure and types of expected keys + $this->assertIsArray( $templateData['data-portlets-first'] ); + $this->assertIsArray( $templateData['array-portlets-rest'] ); + + // Assert the structure and types of expected keys + $this->assertArrayHasKey( 'data-portlets-first', $templateData ); + $this->assertArrayHasKey( 'array-portlets-rest', $templateData ); + } +} diff --git a/tests/phpunit/Unit/Components/CitizenComponentMenuListItemTest.php b/tests/phpunit/Unit/Components/CitizenComponentMenuListItemTest.php new file mode 100644 index 000000000..b69cdb641 --- /dev/null +++ b/tests/phpunit/Unit/Components/CitizenComponentMenuListItemTest.php @@ -0,0 +1,38 @@ +link = $link; + $this->class = $class; + $this->id = $id; + } + + /** + * @inheritDoc + */ + public function getTemplateData(): array { + return $this->link->getTemplateData() + [ + 'item-class' => $this->class, + 'item-id' => $this->id, + ]; + } +} diff --git a/tests/phpunit/Unit/Components/CitizenComponentMenuTest.php b/tests/phpunit/Unit/Components/CitizenComponentMenuTest.php new file mode 100644 index 000000000..29c075150 --- /dev/null +++ b/tests/phpunit/Unit/Components/CitizenComponentMenuTest.php @@ -0,0 +1,98 @@ + 'some-class', + 'label' => 'Some label', + 'html-tooltip' => 'Some tooltip', + 'label-class' => 'some-label-class', + 'html-before-portal' => 'Some before portal', + 'html-items' => 'Some items', + 'html-after-portal' => 'Some after portal', + 'array-list-items' => [ 'some-item-one', 'some-item-2', 'some-item-3' ] + ] + ] + ]; + } + + /** + * @return array[] + */ + public function provideCountData(): array { + return [ + [ + [ + 'array-list-items' => [ 'some-item-one', 'some-item-2', 'some-item-3' ] + ], + 3 + ], + [ + [ + 'html-items' => '
  • Some item
  • Some item
  • Some item
  • ' + ], + 3 + ] + ]; + } + + /** + * This test checks if the CitizenComponentMenu class can be instantiated + * @covers ::__construct + */ + public function testConstruct() { + // Create a new CitizenComponentMenu object + $menu = new CitizenComponentMenu( [] ); + + // Check if the object is an instance of CitizenComponent + $this->assertInstanceOf( CitizenComponent::class, $menu ); + } + + /** + * This test checks if the count method returns the correct number of items + * @covers ::count + * @dataProvider provideCountData + */ + public function testCount( array $data, int $expected ) { + // Create a new CitizenComponentMenu object + $menu = new CitizenComponentMenu( $data ); + + // Check if the count method returns the correct number of items + $this->assertSame( $expected, $menu->count() ); + } + + /** + * This test checks if the getTemplateData method returns the correct data + * @covers ::getTemplateData + * @dataProvider provideMenuData + */ + public function testGetTemplateData( array $data ) { + // Create a new CitizenComponentMenu object + $menu = new CitizenComponentMenu( $data ); + + // Call the getTemplateData method + $actualData = $menu->getTemplateData(); + + // Check if the getTemplateData method returns the correct data + $this->assertSame( $data, $actualData ); + } +} diff --git a/tests/phpunit/Hooks/ResourceLoaderHooksTest.php b/tests/phpunit/integration/Hooks/ResourceLoaderHooksTest.php similarity index 95% rename from tests/phpunit/Hooks/ResourceLoaderHooksTest.php rename to tests/phpunit/integration/Hooks/ResourceLoaderHooksTest.php index c281fd41f..f0e10dbfc 100644 --- a/tests/phpunit/Hooks/ResourceLoaderHooksTest.php +++ b/tests/phpunit/integration/Hooks/ResourceLoaderHooksTest.php @@ -2,15 +2,16 @@ declare( strict_types=1 ); -namespace MediaWiki\Skins\Citizen\Tests\Hooks; +namespace MediaWiki\Skins\Citizen\Tests\Integration\Hooks; use MediaWiki\ResourceLoader\Context; use MediaWiki\Skins\Citizen\Hooks\ResourceLoaderHooks; +use MediaWikiIntegrationTestCase; /** * @group Citizen */ -class ResourceLoaderHooksTest extends \MediaWikiIntegrationTestCase { +class ResourceLoaderHooksTest extends MediaWikiIntegrationTestCase { /** * @covers \MediaWiki\Skins\Citizen\Hooks\ResourceLoaderHooks * @return void diff --git a/tests/phpunit/Hooks/SkinHooksTest.php b/tests/phpunit/integration/Hooks/SkinHooksTest.php similarity index 98% rename from tests/phpunit/Hooks/SkinHooksTest.php rename to tests/phpunit/integration/Hooks/SkinHooksTest.php index 58169f26c..740255c29 100644 --- a/tests/phpunit/Hooks/SkinHooksTest.php +++ b/tests/phpunit/integration/Hooks/SkinHooksTest.php @@ -2,12 +2,13 @@ declare( strict_types=1 ); -namespace MediaWiki\Skins\Citizen\Tests\Hooks; +namespace MediaWiki\Skins\Citizen\Tests\Integration\Hooks; use MediaWiki\Request\ContentSecurityPolicy; use MediaWiki\Skins\Citizen\Hooks\SkinHooks; use MediaWiki\Skins\Citizen\SkinCitizen; use MediaWiki\Title\Title; +use MediaWikiIntegrationTestCase; use OutputPage; use RequestContext; use ResourceLoaderContext; @@ -16,7 +17,7 @@ /** * @group Citizen */ -class SkinHooksTest extends \MediaWikiIntegrationTestCase { +class SkinHooksTest extends MediaWikiIntegrationTestCase { /** * @covers \MediaWiki\Skins\Citizen\Hooks\SkinHooks * @return void diff --git a/tests/phpunit/Partials/BodyContentTest.php b/tests/phpunit/integration/Partials/BodyContentTest.php similarity index 97% rename from tests/phpunit/Partials/BodyContentTest.php rename to tests/phpunit/integration/Partials/BodyContentTest.php index aa0aafaa5..35d8b9d12 100644 --- a/tests/phpunit/Partials/BodyContentTest.php +++ b/tests/phpunit/integration/Partials/BodyContentTest.php @@ -2,7 +2,7 @@ declare( strict_types=1 ); -namespace MediaWiki\Skins\Citizen\Tests\Partials; +namespace MediaWiki\Skins\Citizen\Tests\Integration\Partials; use MediaWiki\Skins\Citizen\Partials\BodyContent; use MediaWiki\Skins\Citizen\SkinCitizen; diff --git a/tests/phpunit/Partials/PageToolsTest.php b/tests/phpunit/integration/Partials/PageToolsTest.php similarity index 98% rename from tests/phpunit/Partials/PageToolsTest.php rename to tests/phpunit/integration/Partials/PageToolsTest.php index e31b124cf..4300285fe 100644 --- a/tests/phpunit/Partials/PageToolsTest.php +++ b/tests/phpunit/integration/Partials/PageToolsTest.php @@ -2,7 +2,7 @@ declare( strict_types=1 ); -namespace MediaWiki\Skins\Citizen\Tests\Partials; +namespace MediaWiki\Skins\Citizen\Tests\Integration\Partials; use MediaWiki\Skins\Citizen\Partials\PageTools; use MediaWiki\Skins\Citizen\SkinCitizen; diff --git a/tests/phpunit/SkinCitizenTest.php b/tests/phpunit/integration/SkinCitizenTest.php similarity index 98% rename from tests/phpunit/SkinCitizenTest.php rename to tests/phpunit/integration/SkinCitizenTest.php index ce22e8ab0..2442c17f9 100644 --- a/tests/phpunit/SkinCitizenTest.php +++ b/tests/phpunit/integration/SkinCitizenTest.php @@ -2,7 +2,7 @@ declare( strict_types=1 ); -namespace MediaWiki\Skins\Citizen\Tests; +namespace MediaWiki\Skins\Citizen\Integration\Tests; use Exception; use MediaWiki\Skins\Citizen\SkinCitizen;