diff --git a/.eslintignore b/.eslintignore index 7a54349e9..49f860025 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,6 +3,7 @@ # Build /vendor/ +/coverage/ # Language files written automatically by TranslateWiki /i18n/**/*.json diff --git a/extension.json b/extension.json index eff942639..28587c887 100644 --- a/extension.json +++ b/extension.json @@ -42,6 +42,7 @@ "highlighter.js", "topicsubscriptions.js", "mobile.js", + "LedeSectionDialog.js", { "name": "controller/contLangMessages.json", "callback": "\\MediaWiki\\Extension\\DiscussionTools\\ResourceLoaderData::getContentLanguageMessages", @@ -118,6 +119,7 @@ "discussiontools-error-noswitchtove-table", "discussiontools-error-noswitchtove-template", "discussiontools-error-noswitchtove-title", + "discussiontools-ledesection-title", "discussiontools-newtopic-legacy-hint", "discussiontools-newtopic-placeholder-title", "discussiontools-newtopic-missing-title", diff --git a/i18n/en.json b/i18n/en.json index 86576dbf3..fe2db8ee7 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -44,6 +44,8 @@ "discussiontools-findcomment-results-notcurrent": "(not in current revision)", "discussiontools-findcomment-results-transcluded": "(transcluded from another page)", "discussiontools-findcomment-title": "Find comment", + "discussiontools-ledesection-button": "Learn more about this page", + "discussiontools-ledesection-title": "About this talk page", "discussiontools-limitreport-errorreqid": "DiscussionTools error request ID", "discussiontools-limitreport-timeusage": "DiscussionTools time usage", "discussiontools-limitreport-timeusage-value": "$1 {{PLURAL:$1|second|seconds}}", diff --git a/i18n/qqq.json b/i18n/qqq.json index 33d005110..ece742e41 100644 --- a/i18n/qqq.json +++ b/i18n/qqq.json @@ -57,6 +57,8 @@ "discussiontools-findcomment-results-notcurrent": "Additional label for a result on [[Special:FindComment]], shown following a link to a page.", "discussiontools-findcomment-results-transcluded": "Additional label for a result on [[Special:FindComment]], shown following a link to a page.", "discussiontools-findcomment-title": "Page title for [[Special:FindComment]], also shown on the list on [[Special:SpecialPages]]", + "discussiontools-ledesection-button": "Label for button to reveal content from the lede section (the section above the first heading).", + "discussiontools-ledesection-title": "Title of dialog showing from the lede section (the section above the first heading).", "discussiontools-limitreport-errorreqid": "Label for the ID of the web request in which a DiscussionTools error has occurred.", "discussiontools-limitreport-timeusage": "Label for the time usage (duration) of DiscussionTools in the parser limit report. Followed by {{msg-mw|discussiontools-limitreport-timeusage-value}}.\n\nSimilar to:\n* {{msg-mw|limitreport-cputime}}\n* {{msg-mw|limitreport-walltime}}\n* {{msg-mw|scribunto-limitreport-timeusage}}", "discussiontools-limitreport-timeusage-value": "Follows {{msg-mw|discussiontools-limitreport-timeusage}}.\n\nParameters:\n* $1 - the usage in seconds\n{{Identical|Second}}", diff --git a/includes/CommentFormatter.php b/includes/CommentFormatter.php index 568ed64d9..bae5e8b04 100644 --- a/includes/CommentFormatter.php +++ b/includes/CommentFormatter.php @@ -306,6 +306,8 @@ class CommentFormatter { $container->appendChild( $newestCommentMarker ); } + $firstHeading = null; + // Enhance other

's which aren't part of a thread $headings = DOMCompat::querySelectorAll( $container, 'h2' ); foreach ( $headings as $headingElement ) { @@ -314,6 +316,18 @@ class CommentFormatter { continue; } static::addTopicContainer( $headingElement ); + if ( !$firstHeading ) { + $firstHeading = $headingElement; + } + } + + if ( + // Page has no headings but some content + ( !$firstHeading && $container->childNodes->length ) || + // Page has content before the first heading + ( $firstHeading && $firstHeading->previousSibling !== null ) + ) { + $container->appendChild( $doc->createComment( '__DTHASLEDECONTENT__' ) ); } if ( count( $threadItems ) === 0 ) { @@ -760,4 +774,14 @@ class CommentFormatter { return str_replace( '', $content, $text ); } + /** + * Check if the talk page has content above the first heading, in the lede section. + * + * @param string $text + * @return bool + */ + public static function hasLedeContent( string $text ): bool { + return strpos( $text, '' ) !== false; + } + } diff --git a/includes/Hooks/PageHooks.php b/includes/Hooks/PageHooks.php index 90529c441..f96fb299f 100644 --- a/includes/Hooks/PageHooks.php +++ b/includes/Hooks/PageHooks.php @@ -197,12 +197,14 @@ class PageHooks implements $text, $lang, $this->subscriptionStore, $output->getUser(), $isMobile ); } + if ( HookUtils::isFeatureEnabledForOutput( $output, HookUtils::REPLYTOOL ) ) { $output->enableOOUI(); $text = CommentFormatter::postprocessReplyTool( $text, $lang, $isMobile ); } + if ( CommentFormatter::isEmptyTalkPage( $text ) && HookUtils::shouldDisplayEmptyState( $output->getContext() ) @@ -213,6 +215,26 @@ class PageHooks implements ); $output->addBodyClasses( 'ext-discussiontools-emptystate-shown' ); } + + if ( + $output->getSkin()->getSkinName() === 'minerva' && + CommentFormatter::hasLedeContent( $text ) + ) { + $output->enableOOUI(); + $output->addHTML( + Html::rawElement( 'div', + [ 'class' => 'ext-discussiontools-init-lede-button-container' ], + ( new ButtonWidget( [ + 'label' => $output->getContext()->msg( 'discussiontools-ledesection-button' )->text(), + 'classes' => [ 'ext-discussiontools-init-lede-button' ], + 'framed' => false, + 'icon' => 'info', + 'infusable' => true, + ] ) ) + ) + ); + } + if ( HookUtils::isFeatureEnabledForOutput( $output, HookUtils::VISUALENHANCEMENTS ) ) { $output->enableOOUI(); if ( HookUtils::isFeatureEnabledForOutput( $output, HookUtils::TOPICSUBSCRIPTION ) ) { @@ -397,28 +419,34 @@ class PageHooks implements * @return bool|void */ public function onArticleViewHeader( $article, &$outputDone, &$pcache ) { - $title = $article->getTitle(); $context = $article->getContext(); $output = $context->getOutput(); - if ( - $output->getSkin()->getSkinName() === 'minerva' && - HookUtils::isFeatureEnabledForOutput( $output, HookUtils::NEWTOPICTOOL ) && - // Only add the button if "New section" tab would be shown in a normal skin. - HookUtils::shouldShowNewSectionTab( $context ) - ) { - // Minerva doesn't show a new topic button by default, unless the MobileFrontend - // talk page feature is enabled, but we shouldn't depend on code from there. - $output->enableOOUI(); - $output->addHTML( - ( new ButtonWidget( [ - 'href' => $title->getLinkURL( [ 'action' => 'edit', 'section' => 'new' ] ), - 'label' => $context->msg( 'skin-action-addsection' )->text(), - 'flags' => [ 'progressive', 'primary' ], - 'classes' => [ 'ext-discussiontools-init-new-topic' ] - ] ) ) - // For compatibility with Minerva click tracking (T295490) - ->setAttributes( [ 'data-event-name' => 'talkpage.add-topic' ] ) - ); + + if ( $output->getSkin()->getSkinName() === 'minerva' ) { + $title = $article->getTitle(); + + if ( + HookUtils::isFeatureEnabledForOutput( $output, HookUtils::NEWTOPICTOOL ) && + // Only add the button if "New section" tab would be shown in a normal skin. + HookUtils::shouldShowNewSectionTab( $context ) + ) { + $output->enableOOUI(); + + // Minerva doesn't show a new topic button by default, unless the MobileFrontend + // talk page feature is enabled, but we shouldn't depend on code from there. + $output->addHTML( + Html::rawElement( 'div', + [ 'class' => 'ext-discussiontools-init-new-topic' ], + ( new ButtonWidget( [ + 'href' => $title->getLinkURL( [ 'action' => 'edit', 'section' => 'new' ] ), + 'label' => $context->msg( 'skin-action-addsection' )->text(), + 'flags' => [ 'progressive', 'primary' ], + ] ) ) + // For compatibility with Minerva click tracking (T295490) + ->setAttributes( [ 'data-event-name' => 'talkpage.add-topic' ] ) + ) + ); + } } } diff --git a/modules/LedeSectionDialog.js b/modules/LedeSectionDialog.js new file mode 100644 index 000000000..945cc840e --- /dev/null +++ b/modules/LedeSectionDialog.js @@ -0,0 +1,43 @@ +function LedeSectionDialog() { + // Parent constructor + LedeSectionDialog.super.apply( this, arguments ); +} + +/* Inheritance */ +OO.inheritClass( LedeSectionDialog, OO.ui.ProcessDialog ); + +LedeSectionDialog.static.name = 'ledeSection'; + +LedeSectionDialog.static.size = 'larger'; + +LedeSectionDialog.static.title = OO.ui.deferMsg( 'discussiontools-ledesection-title' ); + +LedeSectionDialog.static.actions = [ + { + label: OO.ui.deferMsg( 'visualeditor-dialog-action-done' ), + flags: [ 'safe', 'close' ] + } +]; + +LedeSectionDialog.prototype.initialize = function () { + // Parent method + LedeSectionDialog.super.prototype.initialize.call( this ); + + this.contentLayout = new OO.ui.PanelLayout( { + scrollable: true, + padded: true, + expanded: false, + classes: [ 'ext-discussiontools-ui-ledeSectionDialog-content', 'mw-parser-output', 'content' ] + } ); + + this.$body.append( this.contentLayout.$element ); +}; + +LedeSectionDialog.prototype.getSetupProcess = function ( data ) { + return LedeSectionDialog.super.prototype.getSetupProcess.call( this, data ) + .next( function () { + this.contentLayout.$element.empty().append( data.$content ); + }, this ); +}; + +module.exports = LedeSectionDialog; diff --git a/modules/dt.init.less b/modules/dt.init.less index abd5f3ae9..ec1abfabc 100644 --- a/modules/dt.init.less +++ b/modules/dt.init.less @@ -741,6 +741,32 @@ h1, h2, h3, h4, h5, h6 { margin-top: 32px; margin-bottom: -32px; // stylelint-disable-line declaration-block-no-redundant-longhand-properties } + + // Always hide the table of content. This is usually hidden by the mf-section-0 rules, + // but can sometimes appear elsewhere (e.g in the lede section overlay) + // stylelint-disable-next-line selector-class-pattern + .toc { + display: none; + } + + // Override Minerva rule that always hides tmbox. + // stylelint-disable-next-line selector-class-pattern + .ext-discussiontools-ui-ledeSectionDialog-content.content .tmbox { + // stylelint-disable-next-line declaration-no-important + display: block !important; + } +} + +.ext-discussiontools-init-lede-button-container { + margin: 0.5em 0; +} + +.ext-discussiontools-init-lede-button { + opacity: 0.66; + + > .oo-ui-buttonElement-button { + font-weight: normal; + } } // HACK: Fake disabled styles for the .mw-ui-button in Vector sticky header (T307726) diff --git a/modules/mobile.js b/modules/mobile.js index cf4c035d8..45e2a322c 100644 --- a/modules/mobile.js +++ b/modules/mobile.js @@ -1,4 +1,4 @@ -var $readAsWikiPage; +var $readAsWikiPage, ledeSectionDialog; function init( $container ) { // For compatibility with Minerva click tracking (T295490) @@ -31,6 +31,24 @@ function init( $container ) { e.stopPropagation(); } ); } ); + + var $ledeContent = $container.find( '.mf-section-0' ).children( ':not( .ext-discussiontools-emptystate )' ); + var $ledeButton = $container.find( '.ext-discussiontools-init-lede-button' ); + if ( $ledeButton.length ) { + var windowManager = OO.ui.getWindowManager(); + if ( !ledeSectionDialog ) { + var LedeSectionDialog = require( './LedeSectionDialog.js' ); + ledeSectionDialog = new LedeSectionDialog(); + windowManager.addWindows( [ ledeSectionDialog ] ); + } + + // Lede section popup + OO.ui.infuse( $ledeButton ).on( 'click', function () { + mw.loader.using( 'oojs-ui-windows' ).then( function () { + windowManager.openWindow( 'ledeSection', { $content: $ledeContent } ); + } ); + } ); + } if ( !$readAsWikiPage ) { // Read as wiki page button, copied from renderReadAsWikiPageButton in Minerva $readAsWikiPage = $( '