diff --git a/includes/Hooks/HookUtils.php b/includes/Hooks/HookUtils.php index c4379b603..fbfb8a701 100644 --- a/includes/Hooks/HookUtils.php +++ b/includes/Hooks/HookUtils.php @@ -483,19 +483,18 @@ class HookUtils { public static function shouldOpenNewTopicTool( IContextSource $context ): bool { $req = $context->getRequest(); $out = $context->getOutput(); + $hasPreload = $req->getCheck( 'editintro' ) || $req->getCheck( 'preload' ) || + $req->getCheck( 'preloadparams' ) || $req->getCheck( 'preloadtitle' ) || + // Switching or previewing from an external tool (T316333) + $req->getCheck( 'wpTextbox1' ); return ( // ?title=...&action=edit§ion=new // ?title=...&veaction=editsource§ion=new ( $req->getRawVal( 'action' ) === 'edit' || $req->getRawVal( 'veaction' ) === 'editsource' ) && $req->getRawVal( 'section' ) === 'new' && - // Adding a new topic with preloaded text is not supported yet (T269310) - !( - $req->getCheck( 'editintro' ) || $req->getCheck( 'preload' ) || - $req->getCheck( 'preloadparams' ) || $req->getCheck( 'preloadtitle' ) || - // Switching or previewing from an external tool (T316333) - $req->getCheck( 'wpTextbox1' ) - ) && + // Handle new topic with preloaded text only when requested (T269310) + ( $req->getCheck( 'dtpreload' ) || !$hasPreload ) && // User has new topic tool enabled (and not using &dtenable=0) static::isFeatureEnabledForOutput( $out, static::NEWTOPICTOOL ) ); diff --git a/modules/CommentDetails.js b/modules/CommentDetails.js index 7f4e520c4..26ec138b6 100644 --- a/modules/CommentDetails.js +++ b/modules/CommentDetails.js @@ -7,11 +7,15 @@ * @param {number} oldId Revision ID of page at time of editing * @param {Object.} notices Edit notices for the page where the reply is being saved. * Keys are message names; values are HTML to display. + * @param {string} preloadContent Preload content, may be wikitext or HTML depending on `preloadContentMode` + * @param {string} preloadContentMode 'source' or 'visual' */ -function CommentDetails( pageName, oldId, notices ) { +function CommentDetails( pageName, oldId, notices, preloadContent, preloadContentMode ) { this.pageName = pageName; this.oldId = oldId; this.notices = notices; + this.preloadContent = preloadContent; + this.preloadContentMode = preloadContentMode; } OO.initClass( CommentDetails ); diff --git a/modules/NewTopicController.js b/modules/NewTopicController.js index 08fe525b2..ad127d8c6 100644 --- a/modules/NewTopicController.js +++ b/modules/NewTopicController.js @@ -1,5 +1,4 @@ var - utils = require( './utils.js' ), logger = require( './logger.js' ), controller = require( './controller.js' ), CommentController = require( './CommentController.js' ), @@ -9,9 +8,10 @@ var * Handles setup, save and teardown of new topic widget * * @param {jQuery} $pageContainer Page container + * @param {HeadingItem} threadItem * @param {ThreadItemSet} threadItemSet */ -function NewTopicController( $pageContainer, threadItemSet ) { +function NewTopicController( $pageContainer, threadItem, threadItemSet ) { this.container = new OO.ui.PanelLayout( { classes: [ 'ext-discussiontools-ui-newTopic' ], expanded: false, @@ -36,14 +36,10 @@ function NewTopicController( $pageContainer, threadItemSet ) { this.container.$element.append( this.$notices, this.sectionTitleField.$element ); // HeadingItem representing the heading being added, so that we can pretend we're replying to it - var threadItem = new HeadingItem( { - startContainer: this.sectionTitleField.$element[ 0 ], - startOffset: 0, - endContainer: this.sectionTitleField.$element[ 0 ], - endOffset: this.sectionTitleField.$element[ 0 ].childNodes.length - }, 2 ); - threadItem.id = utils.NEW_TOPIC_COMMENT_ID; - threadItem.isNewTopic = true; + threadItem.range.startContainer = this.sectionTitleField.$element[ 0 ]; + threadItem.range.startOffset = 0; + threadItem.range.endContainer = this.sectionTitleField.$element[ 0 ]; + threadItem.range.endOffset = this.sectionTitleField.$element[ 0 ].childNodes.length; NewTopicController.super.call( this, $pageContainer, threadItem, threadItemSet ); } @@ -90,6 +86,10 @@ NewTopicController.prototype.setup = function ( mode ) { NewTopicController.super.prototype.setup.call( this, mode ); + if ( this.threadItem.preloadtitle ) { + this.sectionTitle.setValue( this.threadItem.preloadtitle ); + } + // The section title field is added to the page immediately, we can scroll to the bottom and focus // it while the content field is still loading. rootScrollable.scrollTop = rootScrollable.scrollHeight; @@ -115,7 +115,16 @@ NewTopicController.prototype.setup = function ( mode ) { /** * @inheritdoc */ -NewTopicController.prototype.setupReplyWidget = function ( replyWidget ) { +NewTopicController.prototype.setupReplyWidget = function ( replyWidget, data ) { + if ( replyWidget.commentDetails.preloadContent && ( !data || data.value === undefined ) ) { + if ( replyWidget.commentDetails.preloadContentMode !== replyWidget.getMode() ) { + // This should never happen + throw new Error( 'Preload content was loaded for wrong mode' ); + } + data = $.extend( {}, data, { + value: replyWidget.commentDetails.preloadContent + } ); + } NewTopicController.super.prototype.setupReplyWidget.apply( this, arguments ); this.$notices.empty(); @@ -144,6 +153,7 @@ NewTopicController.prototype.setupReplyWidget = function ( replyWidget ) { this.replyWidget.editSummaryInput.setValue( generatedSummary ); } } + this.replyWidget.storage.set( this.replyWidget.storagePrefix + '/title', this.sectionTitle.getValue() ); if ( this.replyWidget.modeTabSelect ) { // Start with the mode-select widget not-tabbable so focus will go from the title to the body @@ -270,6 +280,11 @@ NewTopicController.prototype.teardown = function ( abandoned ) { url.searchParams.delete( 'action' ); url.searchParams.delete( 'veaction' ); url.searchParams.delete( 'section' ); + url.searchParams.delete( 'dtpreload' ); + url.searchParams.delete( 'editintro' ); + url.searchParams.delete( 'preload' ); + url.searchParams.delete( 'preloadparams[]' ); + url.searchParams.delete( 'preloadtitle' ); history.replaceState( null, '', url ); mw.config.set( 'wgDiscussionToolsStartNewTopicTool', false ); } diff --git a/modules/ReplyLinksController.js b/modules/ReplyLinksController.js index 34f548767..ad383832e 100644 --- a/modules/ReplyLinksController.js +++ b/modules/ReplyLinksController.js @@ -56,6 +56,8 @@ OO.mixinClass( ReplyLinksController, OO.EventEmitter ); * @event link-click * @param {string} id * @param {jQuery} $linkSet + * @param {jQuery} $link + * @param {Object} [data] */ /* Methods */ @@ -104,11 +106,32 @@ ReplyLinksController.prototype.onAnyLinkClick = function ( e ) { if ( !href ) { return; } + + var data = this.parseNewTopicLink( href ); + if ( !data ) { + return; + } + + if ( !this.isActivationEvent( e ) ) { + return; + } + e.preventDefault(); + + this.emit( 'link-click', utils.NEW_TOPIC_COMMENT_ID, $( e.currentTarget ), data ); +}; + +/** + * Check if the given URL is a new topic link, and if so, return parsed parameters. + * + * @param {string} href + * @return {Object|null} `null` if not a new topic link, parameters otherwise + */ +ReplyLinksController.prototype.parseNewTopicLink = function ( href ) { var url = new URL( href ); var title = mw.Title.newFromText( utils.getTitleFromUrl( href ) || '' ); if ( !title ) { - return; + return null; } // Recognize links to add a new topic: @@ -121,7 +144,7 @@ ReplyLinksController.prototype.onAnyLinkClick = function ( e ) { var param = title.getMainText().slice( parserData.specialNewSectionName.length + 1 ); title = mw.Title.newFromText( param ); if ( !title ) { - return; + return null; } } else if ( @@ -135,28 +158,34 @@ ReplyLinksController.prototype.onAnyLinkClick = function ( e ) { } else { // Not a link to add a new topic - return; + return null; } if ( title.getPrefixedDb() !== mw.config.get( 'wgRelevantPageName' ) ) { // Link to add a section on another page, not supported yet (T282205) - return; + return null; } - if ( - url.searchParams.get( 'editintro' ) || url.searchParams.get( 'preload' ) || - url.searchParams.getAll( 'preloadparams[]' ).length || url.searchParams.get( 'preloadtitle' ) - ) { - // Adding a new topic with preloaded text is not supported yet (T269310) - return; + var data = {}; + if ( url.searchParams.get( 'editintro' ) ) { + data.editintro = url.searchParams.get( 'editintro' ); + } + if ( url.searchParams.get( 'preload' ) ) { + data.preload = url.searchParams.get( 'preload' ); + } + if ( url.searchParams.getAll( 'preloadparams[]' ).length ) { + data.preloadparams = url.searchParams.getAll( 'preloadparams[]' ); + } + if ( url.searchParams.get( 'preloadtitle' ) ) { + data.preloadtitle = url.searchParams.get( 'preloadtitle' ); } - if ( !this.isActivationEvent( e ) ) { - return; + // Handle new topic with preloaded text only when requested (T269310) + if ( !url.searchParams.get( 'dtpreload' ) && !$.isEmptyObject( data ) ) { + return null; } - e.preventDefault(); - this.emit( 'link-click', utils.NEW_TOPIC_COMMENT_ID, $( e.currentTarget ) ); + return data; }; ReplyLinksController.prototype.isActivationEvent = function ( e ) { diff --git a/modules/controller.js b/modules/controller.js index 2ef047792..561c028d4 100644 --- a/modules/controller.js +++ b/modules/controller.js @@ -10,6 +10,7 @@ var Parser = require( './Parser.js' ), ThreadItemSet = require( './ThreadItemSet.js' ), CommentDetails = require( './CommentDetails.js' ), + HeadingItem = require( './HeadingItem.js' ), ReplyLinksController = require( './ReplyLinksController.js' ), logger = require( './logger.js' ), utils = require( './utils.js' ), @@ -43,13 +44,15 @@ function getApi() { * * @param {string} pageName Page title * @param {number} oldId Revision ID + * @param {Object} [apiParams] Additional parameters for the API * @return {jQuery.Promise} */ -function getPageData( pageName, oldId ) { +function getPageData( pageName, oldId, apiParams ) { var api = getApi(); + apiParams = apiParams || {}; pageDataCache[ pageName ] = pageDataCache[ pageName ] || {}; - if ( pageDataCache[ pageName ][ oldId ] ) { + if ( pageDataCache[ pageName ][ oldId ] && $.isEmptyObject( apiParams ) ) { return pageDataCache[ pageName ][ oldId ]; } @@ -77,15 +80,15 @@ function getPageData( pageName, oldId ) { transcludedFromPromise = $.Deferred().resolve( {} ).promise(); } - var veMetadataPromise = api.get( { + var veMetadataPromise = api.get( $.extend( { action: 'visualeditor', paction: 'metadata', page: pageName - } ).then( function ( response ) { + }, apiParams ) ).then( function ( response ) { return OO.getProp( response, 'visualeditor' ) || []; } ); - pageDataCache[ pageName ][ oldId ] = $.when( lintPromise, transcludedFromPromise, veMetadataPromise ) + var promise = $.when( lintPromise, transcludedFromPromise, veMetadataPromise ) .then( function ( linterrors, transcludedfrom, metadata ) { return { linterrors: linterrors, @@ -98,7 +101,12 @@ function getPageData( pageName, oldId ) { // Let caller handle the error return $.Deferred().rejectWith( this, arguments ); } ); - return pageDataCache[ pageName ][ oldId ]; + + if ( $.isEmptyObject( apiParams ) ) { + pageDataCache[ pageName ][ oldId ] = promise; + } + + return promise; } /** @@ -112,8 +120,19 @@ function getPageData( pageName, oldId ) { */ function checkThreadItemOnPage( pageName, oldId, threadItem ) { var isNewTopic = threadItem.id === utils.NEW_TOPIC_COMMENT_ID; + var defaultMode = mw.user.options.get( 'discussiontools-editmode' ) || mw.config.get( 'wgDiscussionToolsFallbackEditMode' ); + var apiParams = null; + if ( isNewTopic ) { + apiParams = { + section: 'new', + editintro: threadItem.editintro, + preload: threadItem.preload, + preloadparams: threadItem.preloadparams, + paction: defaultMode === 'source' ? 'wikitext' : 'parse' + }; + } - return getPageData( pageName, oldId ) + return getPageData( pageName, oldId, apiParams ) .then( function ( response ) { var metadata = response.metadata, lintErrors = response.linterrors, @@ -192,7 +211,7 @@ function checkThreadItemOnPage( pageName, oldId, threadItem ) { } ] } ).promise(); } - return new CommentDetails( pageName, oldId, metadata.notices ); + return new CommentDetails( pageName, oldId, metadata.notices, metadata.content, defaultMode ); } ); } @@ -286,26 +305,26 @@ function init( $container, state ) { /** * Setup comment controllers for each comment, and the new topic controller * - * @param {string} commentId Comment ID, or NEW_TOPIC_COMMENT_ID constant + * @param {ThreadItem} comment * @param {jQuery} $link Add section link for new topic controller * @param {string} [mode] Optionally force a mode, 'visual' or 'source' * @param {boolean} [hideErrors] Suppress errors, e.g. when restoring auto-save * @param {boolean} [suppressNotifications] Don't notify the user if recovering auto-save */ - function setupController( commentId, $link, mode, hideErrors, suppressNotifications ) { + function setupController( comment, $link, mode, hideErrors, suppressNotifications ) { var commentController, $addSectionLink; - if ( commentId === utils.NEW_TOPIC_COMMENT_ID ) { + if ( comment.id === utils.NEW_TOPIC_COMMENT_ID ) { // eslint-disable-next-line no-jquery/no-global-selector $addSectionLink = $( '#ca-addsection' ).find( 'a' ); // When opening new topic tool using any link, always activate the link in page tabs too $link = $link.add( $addSectionLink ); - commentController = new NewTopicController( $pageContainer, pageThreads ); + commentController = new NewTopicController( $pageContainer, comment, pageThreads ); } else { - commentController = new CommentController( $pageContainer, pageThreads.findCommentById( commentId ), pageThreads ); + commentController = new CommentController( $pageContainer, comment, pageThreads ); } - activeCommentId = commentId; + activeCommentId = comment.id; activeController = commentController; linksController.setActiveLink( $link ); @@ -345,11 +364,19 @@ function init( $container, state ) { } } + function newTopicComment( data ) { + var comment = new HeadingItem( {}, 2 ); + comment.id = utils.NEW_TOPIC_COMMENT_ID; + comment.isNewTopic = true; + $.extend( comment, data ); + return comment; + } + // Hook up each link to open a reply widget // // TODO: Allow users to use multiple reply widgets simultaneously. // Currently submitting a reply from one widget would also destroy the other ones. - linksController.on( 'link-click', function ( commentId, $link ) { + linksController.on( 'link-click', function ( commentId, $link, data ) { // If the reply widget is already open, activate it. // Reply links are also made unclickable using 'pointer-events' in CSS, but that doesn't happen // for new section links, because we don't have a good way of visually disabling them. @@ -372,7 +399,13 @@ function init( $container, state ) { if ( activeController ) { return; } - setupController( commentId, $link ); + var comment; + if ( commentId !== utils.NEW_TOPIC_COMMENT_ID ) { + comment = parser.findCommentById( comment.id ); + } else { + comment = newTopicComment( data ); + } + setupController( comment, $link ); } ); } ); @@ -389,7 +422,7 @@ function init( $container, state ) { if ( storage.get( 'reply/' + comment.id + '/saveable' ) ) { mode = storage.get( 'reply/' + comment.id + '/mode' ); $link = $( commentNodes[ i ] ); - setupController( comment.id, $link, mode, true, !state.firstLoad ); + setupController( comment, $link, mode, true, !state.firstLoad ); break; } } @@ -398,9 +431,10 @@ function init( $container, state ) { storage.get( 'reply/' + utils.NEW_TOPIC_COMMENT_ID + '/title' ) ) { mode = storage.get( 'reply/' + utils.NEW_TOPIC_COMMENT_ID + '/mode' ); - setupController( utils.NEW_TOPIC_COMMENT_ID, $( [] ), mode, true, !state.firstLoad ); + setupController( newTopicComment(), $( [] ), mode, true, !state.firstLoad ); } else if ( mw.config.get( 'wgDiscussionToolsStartNewTopicTool' ) ) { - setupController( utils.NEW_TOPIC_COMMENT_ID, $( [] ) ); + var data = linksController.parseNewTopicLink( location.href ); + setupController( newTopicComment( data ), $( [] ) ); } }() );