diff --git a/i18n/en.json b/i18n/en.json index be3fde8..f1d557b 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -11,6 +11,7 @@ "monobook-jumptonavigation": "Jump to navigation", "monobook-jumptosearch": "Jump to search", "monobook-more-actions": "More", + "cactions-mobile": "Page actions", "monobook-cactions-label": "Page actions", "monobook-notifications-link": "Notifications ($1)", "monobook-notifications-link-none": "Notifications" diff --git a/i18n/qqq.json b/i18n/qqq.json index 7c51220..5d2acb5 100644 --- a/i18n/qqq.json +++ b/i18n/qqq.json @@ -21,6 +21,7 @@ "monobook-jumptonavigation": "Accessibility link for jumping to the navigation links. Visually hidden by default.\n\nSee also\n* {{msg-mw|Navigation}}\n\n{{Identical|jumptonavigation}}\n\njay94ks:\nMaybe this translation context is duplicated. :)\nI've found the perfectly same thing even description also same.\n* MediaWiki:Vector-jumptonavigation/ko - This context must be filled out with same content.", "monobook-jumptosearch": "Accessibility link for jumping to the site search. Visually hidden by default.\n\nSee also\n* {{msg-mw|Search}}\n\n{{Identical|jumptosearch}}", "monobook-more-actions": "Label for the less-important or rarer actions that are hidden from the usual tabs on mobile interfaces (like moving the page, or for sysops deleting or protecting the page). {{Identical|More}}", + "cactions-mobile": "Header for the content actions menu (tabs on the top of the page)", "monobook-cactions-label": "Header for the content actions menu (tabs on the top of the page)", "monobook-notifications-link": "Label for Extension:Notifications link in mobile personal toolbar\n\nParameters:\n* $1 - number of current alerts/notifications", "monobook-notifications-link-none": "Label for Extension:Notifications link in mobile personal toolbar when no notifications present\n{{Identical|Notification}}" diff --git a/includes/Hooks.php b/includes/Hooks.php index 9e4c5f6..284e56e 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -24,6 +24,7 @@ namespace MonoBook; use OutputPage; use Skin; +use SkinTemplate; class Hooks { /** @@ -48,4 +49,51 @@ class Hooks { $bodyAttrs['class'] .= ' monobook-capitalize-all-nouns'; } } + + /** + * SkinTemplateNavigationUniversal hook handler + * + * @param SkinTemplate $skin + * @param array &$content_navigation + */ + public static function onSkinTemplateNavigationUniversal( SkinTemplate $skin, array &$content_navigation ) { + $title = $skin->getTitle(); + if ( $skin->getSkinName() === 'monobook' ) { + $tabs = []; + $namespaces = $content_navigation['namespaces']; + foreach ( $namespaces as $nsid => $attribs ) { + $id = $nsid . '-mobile'; + $tabs[$id] = [] + $attribs; + $tabs[$id]['title'] = $attribs['text']; + $tabs[$id]['id'] = $id; + } + + if ( !$title->isSpecialPage() ) { + $tabs['more'] = [ + 'text' => $skin->msg( 'monobook-more-actions' )->text(), + 'href' => '#p-cactions', + 'id' => 'ca-more' + ]; + } + + $tabs['toolbox'] = [ + 'text' => $skin->msg( 'toolbox' )->text(), + 'href' => '#p-tb', + 'id' => 'ca-tools', + 'title' => $skin->msg( 'toolbox' )->text() + ]; + + $languages = $skin->getLanguages(); + if ( count( $languages ) > 0 ) { + $tabs['languages'] = [ + 'text' => $skin->msg( 'otherlanguages' )->text(), + 'href' => '#p-lang', + 'id' => 'ca-languages', + 'title' => $skin->msg( 'otherlanguages' )->text() + ]; + } + + $content_navigation['cactions-mobile'] = $tabs; + } + } } diff --git a/includes/MonoBookTemplate.php b/includes/MonoBookTemplate.php deleted file mode 100644 index dc52b45..0000000 --- a/includes/MonoBookTemplate.php +++ /dev/null @@ -1,583 +0,0 @@ -get( 'headelement' ); - $html .= Html::openElement( 'div', [ 'id' => 'globalWrapper' ] ); - - $html .= Html::openElement( 'div', [ 'id' => 'column-content' ] ); - $html .= Html::rawElement( 'div', [ 'id' => 'content', 'class' => 'mw-body', 'role' => 'main' ], - Html::element( 'a', [ 'id' => 'top' ] ) . - $this->getIfExists( 'sitenotice', [ - 'wrapper' => 'div', - 'parameters' => [ 'id' => 'siteNotice' ] - ] ) . - $this->getIndicators() . - $this->getIfExists( 'title', [ - 'loose' => true, - 'wrapper' => 'h1', - 'parameters' => [ - 'id' => 'firstHeading', - 'class' => 'firstHeading', - 'lang' => $this->getSkin()->getTitle()->getPageViewLanguage()->getHtmlCode() - ] - ] ) . - Html::rawElement( 'div', [ 'id' => 'bodyContent', 'class' => 'monobook-body' ], - Html::rawElement( 'div', [ 'id' => 'siteSub' ], $this->getMsg( 'tagline' )->parse() ) . - Html::rawElement( - 'div', - [ 'id' => 'contentSub', 'lang' => $this->get( 'userlang' ), 'dir' => $this->get( 'dir' ) ], - $this->get( 'subtitle' ) - ) . - $this->getIfExists( 'undelete', [ 'wrapper' => 'div', 'parameters' => [ - 'id' => 'contentSub2' - ] ] ) . - $this->getIfExists( 'newtalk', [ 'wrapper' => 'div', 'parameters' => [ - 'class' => 'usermessage' - ] ] ) . - Html::element( 'div', [ 'id' => 'jump-to-nav' ] ) . - Html::element( 'a', [ 'href' => '#column-one', 'class' => 'mw-jump-link' ], - $this->getMsg( 'monobook-jumptonavigation' )->text() - ) . - Html::element( 'a', [ 'href' => '#searchInput', 'class' => 'mw-jump-link' ], - $this->getMsg( 'monobook-jumptosearch' )->text() - ) . - '' . - - $this->get( 'bodytext' ) . - $this->getIfExists( 'catlinks' ) . - - '' . - $this->getClear() - ) - ); - $html .= $this->getIfExists( 'dataAfterContent' ) . $this->getClear(); - $html .= Html::closeElement( 'div' ); - - $html .= Html::rawElement( 'div', - [ - 'id' => 'column-one', - 'lang' => $this->get( 'userlang' ), - 'dir' => $this->get( 'dir' ) - ], - Html::element( 'h2', [], $this->getMsg( 'navigation-heading' )->text() ) . - $this->getCactions() . - $this->getBox( 'personal', $this->getPersonalTools(), 'personaltools' ) . - Html::rawElement( 'div', [ 'class' => 'portlet', 'id' => 'p-logo', 'role' => 'banner' ], - Html::element( 'a', - [ - 'href' => $this->data['nav_urls']['mainpage']['href'], - 'class' => 'mw-wiki-logo', - ] - + Linker::tooltipAndAccesskeyAttribs( 'p-logo' ) - ) - ) . - Html::rawElement( 'div', [ 'id' => 'sidebar' ], $this->getRenderedSidebar() ) . - $this->getMobileNavigationIcon( - 'sidebar', - $this->getMsg( 'jumptonavigation' )->text() - ) . - $this->getMobileNavigationIcon( - 'p-personal', - $this->getMsg( 'monobook-jumptopersonal' )->text() - ) . - $this->getMobileNavigationIcon( - 'globalWrapper', - $this->getMsg( 'monobook-jumptotop' )->text() - ) - ); - $html .= ''; - - $html .= $this->getClear(); - $html .= $this->getSimpleFooter(); - $html .= Html::closeElement( 'div' ); - - $html .= $this->getTrail(); - - $html .= Html::closeElement( 'body' ); - $html .= Html::closeElement( 'html' ); - - // The unholy echo - echo $html; - } - - /** - * Create a wrapped link to create a mobile toggle/jump icon - * Needs to be an on-page link (as opposed to drawing something on the fly for an - * onclick event) for no-js support. - * - * @param string $target link target - * @param string $title icon title - * - * @return string html empty link block - */ - protected function getMobileNavigationIcon( $target, $title ) { - return Html::element( 'a', [ - 'href' => "#$target", - 'title' => $title, - 'class' => 'menu-toggle', - 'id' => "$target-toggle" - ] ); - } - - /** - * Generate the cactions (content actions) tabs, as well as a second set of spoof tabs for mobile - * - * @return string html - */ - protected function getCactions() { - $html = ''; - $allTabs = $this->data['content_actions']; - $tabCount = count( $allTabs ); - - // Normal cactions - if ( $tabCount > 2 ) { - $html .= $this->getBox( 'cactions', $allTabs, 'monobook-cactions-label' ); - } else { - // Is redundant with spoof, hide normal cactions entirely in mobile - $html .= $this->getBox( 'cactions', $allTabs, 'monobook-cactions-label', - [ 'extra-classes' => 'nomobile' ] - ); - } - - // Mobile cactions tabs - $tabs = $this->data['content_navigation']['namespaces']; - foreach ( $tabs as $tab => $attribs ) { - $tabs[$tab]['id'] = $attribs['id'] . '-mobile'; - $tabs[$tab]['title'] = $attribs['text']; - } - - if ( $tabCount !== 1 ) { - // Is not special page or stuff, append a 'more' - $tabs['more'] = [ - 'text' => $this->getMsg( 'monobook-more-actions' )->text(), - 'href' => '#p-cactions', - 'id' => 'ca-more' - ]; - } - $tabs['toolbox'] = [ - 'text' => $this->getMsg( 'toolbox' )->text(), - 'href' => '#p-tb', - 'id' => 'ca-tools', - 'title' => $this->getMsg( 'toolbox' )->text() - ]; - - $languages = $this->data['sidebar']['LANGUAGES']; - if ( $languages !== false ) { - $tabs['languages'] = [ - 'text' => $this->getMsg( 'otherlanguages' )->text(), - 'href' => '#p-lang', - 'id' => 'ca-languages', - 'title' => $this->getMsg( 'otherlanguages' )->text() - ]; - } - - $html .= $this->getBox( 'cactions-mobile', $tabs, 'monobook-cactions-label' ); - - return $html; - } - - /** - * Generate the full sidebar - * - * @return string html - * @suppress PhanTypeMismatchArgument $content is an array - * even though we are comparing it to boolean - */ - protected function getRenderedSidebar() { - $sidebar = $this->data['sidebar']; - $html = ''; - $languagesHTML = ''; - - if ( !isset( $sidebar['SEARCH'] ) ) { - $sidebar['SEARCH'] = true; - } - - foreach ( $sidebar as $boxName => $content ) { - if ( $content === false ) { - continue; - } - - // Numeric strings gets an integer when set as key, cast back - T73639 - $boxName = (string)$boxName; - - if ( $boxName == 'SEARCH' ) { - $html .= $this->getSearchBox(); - } elseif ( $boxName == 'TOOLBOX' ) { - $html .= $this->getToolboxBox( $content ); - } elseif ( $boxName == 'LANGUAGES' ) { - $languagesHTML = $this->getLanguageBox( $content ); - } else { - $html .= $this->getBox( - $boxName, - $content, - null, - [ 'extra-classes' => 'generated-sidebar' ] - ); - } - } - - // Output language portal last given it can be long - // on articles which support multiple languages (T254546) - return $html . $languagesHTML; - } - - /** - * Generate the search button - * - * @return string html - */ - protected function getSearchBox() { - $html = ''; - - $optionButtons = "\u{00A0} " . $this->makeSearchButton( - 'fulltext', - [ 'id' => 'mw-searchButton', 'class' => 'searchButton' ] - ); - $searchInputId = 'searchInput'; - $searchForm = Html::rawElement( 'form', [ - 'action' => $this->get( 'wgScript' ), - 'id' => 'searchform' - ], - Html::hidden( 'title', $this->get( 'searchtitle' ) ) . - $this->makeSearchInput( [ 'id' => $searchInputId ] ) . - $this->makeSearchButton( 'go', [ 'id' => 'searchButton', 'class' => 'searchButton' ] ) . - $optionButtons - ); - - $html .= $this->getBox( 'search', $searchForm, null, [ - 'search-input-id' => $searchInputId, - 'role' => 'search', - 'body-id' => 'searchBody' - ] ); - - return $html; - } - - /** - * Generate the toolbox, complete with all three old hooks - * - * @param array $toolboxItems - * @return string html - */ - protected function getToolboxBox( $toolboxItems ) { - $html = ''; - - $html .= $this->getBox( 'tb', $toolboxItems, 'toolbox' ); - - return $html; - } - - /** - * Generate the languages box - * - * @param array $languages Interwiki language links - * @return string html - */ - protected function getLanguageBox( $languages ) { - $html = ''; - $name = 'lang'; - - if ( - $languages !== [] || - // Check getAfterPortlet to make sure the languages are shown - // when empty but something has been injected in the portal. (T252841) - $this->getAfterPortletHTML( $name ) - ) { - $html .= $this->getBox( $name, $languages, 'otherlanguages' ); - } - - return $html; - } - - /** - * Generate a sidebar box using getPortlet(); prefill some common stuff - * - * @param string $name - * @param array|string $contents - * @param-taint $contents escapes_htmlnoent - * @param null|string|array|bool $msg - * @param array $setOptions - * - * @return string html - */ - protected function getBox( $name, $contents, $msg = null, $setOptions = [] ) { - $options = array_merge( [ - 'class' => 'portlet', - 'body-class' => 'pBody', - 'text-wrapper' => '' - ], $setOptions ); - - return $this->getPortlet( $name, $contents, $msg, $options ); - } - - /** - * 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 - * @suppress PhanTypeMismatchArgumentNullable Many false positives - */ - protected function getPortlet( $name, $content, $msg = null, $setOptions = [] ) { - // random stuff to override with any provided options - $options = array_merge( [ - // handle role=search a little differently - 'role' => 'navigation', - 'search-input-id' => 'searchInput', - // extra classes/ids - 'id' => 'p-' . $name, - 'class' => 'mw-portlet', - 'extra-classes' => '', - 'body-id' => null, - 'body-class' => 'mw-portlet-body', - 'body-extra-classes' => '', - // wrapper for individual list items - 'text-wrapper' => [ 'tag' => 'span' ], - ], $setOptions ); - - // Handle the different $msg possibilities - if ( $msg === null ) { - $msg = $name; - $msgParams = []; - } elseif ( is_array( $msg ) ) { - $msgString = array_shift( $msg ); - $msgParams = $msg; - $msg = $msgString; - } else { - $msgParams = []; - } - $msgObj = $this->getMsg( $msg, $msgParams ); - if ( $msgObj->exists() ) { - $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' ) ] - ); - foreach ( $content as $key => $item ) { - if ( is_array( $options['text-wrapper'] ) ) { - $contentText .= $this->makeListItem( - $key, - $item, - [ 'text-wrapper' => $options['text-wrapper'] ] - ); - } else { - $contentText .= $this->makeListItem( - $key, - $item - ); - } - } - $contentText .= Html::closeElement( 'ul' ); - } else { - $contentText = $content; - } - - // Special handling for role=search - $divOptions = [ - 'role' => $options['role'], - 'class' => $this->mergeClasses( $options['class'], $options['extra-classes'] ), - 'id' => Sanitizer::escapeIdForAttribute( $options['id'] ), - 'title' => Linker::titleAttrib( $options['id'] ) - ]; - if ( $options['role'] !== 'search' ) { - $divOptions['aria-labelledby'] = $labelId; - } - $labelOptions = [ - 'id' => $labelId, - 'lang' => $this->get( 'userlang' ), - 'dir' => $this->get( 'dir' ) - ]; - if ( $options['role'] == 'search' ) { - $msgString = Html::rawElement( 'label', [ 'for' => $options['search-input-id'] ], $msgString ); - } - - $bodyDivOptions = [ - 'class' => $this->mergeClasses( $options['body-class'], $options['body-extra-classes'] ) - ]; - if ( is_string( $options['body-id'] ) ) { - $bodyDivOptions['id'] = $options['body-id']; - } - - $html = Html::rawElement( 'div', $divOptions, - Html::rawElement( 'h3', $labelOptions, $msgString ) . - Html::rawElement( 'div', $bodyDivOptions, - $contentText . $this->getAfterPortletHTML( $name ) - ) - ); - - return $html; - } - - /** - * Helper function for getPortlet - * - * Merge all provided css classes into a single array - * Account for possible different input methods matching what Html::element stuff takes - * - * @param string|array $class base portlet/body class - * @param string|array $extraClasses any extra classes to also include - * - * @return array all classes to apply - */ - protected function mergeClasses( $class, $extraClasses ) { - if ( !is_array( $class ) ) { - $class = [ $class ]; - } - if ( !is_array( $extraClasses ) ) { - $extraClasses = [ $extraClasses ]; - } - - return array_merge( $class, $extraClasses ); - } - - /** - * Simple wrapper for random if-statement-wrapped $this->data things - * - * @param string $object name of thing - * @param array $setOptions - * - * @return string html - */ - protected function getIfExists( $object, $setOptions = [] ) { - $options = [ - 'loose' => false, - 'wrapper' => 'none', - 'parameters' => [] - ]; - foreach ( $setOptions as $key => $value ) { - $options[$key] = $value; - } - - $html = ''; - - if ( ( $options['loose'] && $this->data[$object] != '' ) || - ( !$options['loose'] && $this->data[$object] ) ) { - if ( $options['wrapper'] == 'none' ) { - $html .= $this->get( $object ); - } else { - $html .= Html::rawElement( - $options['wrapper'], - $options['parameters'], - $this->get( $object ) - ); - } - } - - return $html; - } - - /** - * Renderer for getFooterIcons and getFooterLinks as a generic footer block - * - * @return string html - */ - protected function getSimpleFooter() { - $validFooterIcons = $this->get( 'footericons' ); - $validFooterLinks = $this->getFooterLinks( 'flat' ); - - $html = ''; - - $html .= Html::openElement( 'div', [ - 'id' => 'footer', - 'class' => 'mw-footer', - 'role' => 'contentinfo', - 'lang' => $this->get( 'userlang' ), - 'dir' => $this->get( 'dir' ) - ] ); - - foreach ( $validFooterIcons as $blockName => $footerIcons ) { - $html .= Html::openElement( 'div', [ - 'id' => Sanitizer::escapeIdForAttribute( "f-{$blockName}ico" ), - 'class' => 'footer-icons' - ] ); - foreach ( $footerIcons as $icon ) { - $html .= $this->getSkin()->makeFooterIcon( $icon ); - } - $html .= Html::closeElement( 'div' ); - } - if ( count( $validFooterLinks ) > 0 ) { - $html .= Html::openElement( 'ul', [ 'id' => 'f-list' ] ); - foreach ( $validFooterLinks as $aLink ) { - $html .= Html::rawElement( - 'li', - [ 'id' => Sanitizer::escapeIdForAttribute( $aLink ) ], - $this->get( $aLink ) - ); - } - $html .= Html::closeElement( 'ul' ); - } - $html .= Html::closeElement( 'div' ); - - return $html; - } - - /** - * Gets after portal HTML and wraps it with div and class - * - * @param string $name - * @return string html - */ - private function getAfterPortletHTML( $name ) { - $content = $this->getSkin()->getAfterPortlet( $name ); - if ( $content !== '' ) { - return Html::rawElement( - 'div', - [ 'class' => [ 'after-portlet', 'after-portlet-' . $name ] ], - $content - ); - } - return ''; - } -} diff --git a/resources/mobile.js.less b/resources/mobile.js.less index 4deabd2..73a225d 100644 --- a/resources/mobile.js.less +++ b/resources/mobile.js.less @@ -1,7 +1,6 @@ @import 'variables.less'; // remove duplicates we're not using here -#sidebar .generated-sidebar, #p-search-mobilejs, #p-tb-mobilejs, #p-lang-mobilejs, @@ -9,6 +8,12 @@ display: none; } +#sidebar #p-tb, +#sidebar #p-lang, +#sidebar #p-search { + display: block; +} + // popouts #p-cactions, #p-personal, diff --git a/resources/screen-desktop.less b/resources/screen-desktop.less index 3ee02b6..5d88483 100644 --- a/resources/screen-desktop.less +++ b/resources/screen-desktop.less @@ -310,6 +310,8 @@ li#ca-print { margin-left: 1.6em; } +/* Historically not present in Monobook skin */ +#p-cactions li#ca-view, /* ** mobile toggles; not used here */ diff --git a/skin.json b/skin.json index 1c66b55..82c80e8 100644 --- a/skin.json +++ b/skin.json @@ -15,14 +15,28 @@ }, "ValidSkinNames": { "monobook": { - "class": "SkinTemplate", + "class": "SkinMustache", "args": [ { "name": "monobook", + "responsive": true, + "templateDirectory": "templates/", "scripts": [ "skins.monobook.scripts" ], "styles": [ "skins.monobook.styles" ], - "responsive": true, - "template": "MonoBook\\MonoBookTemplate" + "messages": [ + "tagline", + "nstab-main", + "nstab-talk", + "monobook-more-actions", + "otherlanguages", + "toolbox", + "navigation-heading", + "monobook-jumptotop", + "monobook-jumptopersonal", + "monobook-jumptosearch", + "monobook-cactions-label", + "monobook-jumptonavigation" + ] } ] } @@ -34,7 +48,8 @@ "monobook": "resources/mediawiki.less" }, "Hooks": { - "OutputPageBodyAttributes": "MonoBook\\Hooks::onOutputPageBodyAttributes" + "OutputPageBodyAttributes": "MonoBook\\Hooks::onOutputPageBodyAttributes", + "SkinTemplateNavigation::Universal": "MonoBook\\Hooks::onSkinTemplateNavigationUniversal" }, "MessagesDirs": { "MonoBook": [ diff --git a/templates/Portlet.mustache b/templates/Portlet.mustache new file mode 100644 index 0000000..7921d72 --- /dev/null +++ b/templates/Portlet.mustache @@ -0,0 +1,10 @@ +{{! monobook uses a `portlet` class. Standardisation of classes will come later }} +
diff --git a/templates/skin.mustache b/templates/skin.mustache new file mode 100644 index 0000000..ce0105d --- /dev/null +++ b/templates/skin.mustache @@ -0,0 +1,100 @@ +