--- /dev/null
+/**
+ * Animate watch/unwatch links to use asynchronous API requests to
+ * watch pages, rather than navigating to a different URI.
+ *
+ * @class mw.page.watch.ajax
+ */
+( function ( mw, $ ) {
+ var watch,
+ // The name of the page to watch or unwatch
+ title = mw.config.get( 'wgRelevantPageName' );
+
+ /**
+ * Update the link text, link href attribute and (if applicable)
+ * "loading" class.
+ *
+ * @param {jQuery} $link Anchor tag of (un)watch link
+ * @param {string} action One of 'watch', 'unwatch'
+ * @param {string} [state="idle"] 'idle' or 'loading'. Default is 'idle'
+ */
+ function updateWatchLink( $link, action, state ) {
+ var msgKey, $li, otherAction;
+
+ // A valid but empty jQuery object shouldn't throw a TypeError
+ if ( !$link.length ) {
+ return;
+ }
+
+ // Invalid actions shouldn't silently turn the page in an unrecoverable state
+ if ( action !== 'watch' && action !== 'unwatch' ) {
+ throw new Error( 'Invalid action' );
+ }
+
+ // message keys 'watch', 'watching', 'unwatch' or 'unwatching'.
+ msgKey = state === 'loading' ? action + 'ing' : action;
+ otherAction = action === 'watch' ? 'unwatch' : 'watch';
+ $li = $link.closest( 'li' );
+
+ // Trigger a 'watchpage' event for this List item.
+ // Announce the otherAction value as the first param.
+ // Used to monitor the state of watch link.
+ // TODO: Revise when system wide hooks are implemented
+ if ( state === undefined ) {
+ $li.trigger( 'watchpage.mw', otherAction );
+ }
+
+ $link
+ .text( mw.msg( msgKey ) )
+ .attr( 'title', mw.msg( 'tooltip-ca-' + action ) )
+ .updateTooltipAccessKeys()
+ .attr( 'href', mw.util.getUrl( title, { action: action } ) );
+
+ // Most common ID style
+ if ( $li.prop( 'id' ) === 'ca-' + otherAction ) {
+ $li.prop( 'id', 'ca-' + action );
+ }
+
+ if ( state === 'loading' ) {
+ $link.addClass( 'loading' );
+ } else {
+ $link.removeClass( 'loading' );
+ }
+ }
+
+ /**
+ * TODO: This should be moved somewhere more accessible.
+ *
+ * @private
+ * @param {string} url
+ * @return {string} The extracted action, defaults to 'view'
+ */
+ function mwUriGetAction( url ) {
+ var action, actionPaths, key, i, m, parts;
+
+ // TODO: Does MediaWiki give action path or query param
+ // precedence? If the former, move this to the bottom
+ action = mw.util.getParamValue( 'action', url );
+ if ( action !== null ) {
+ return action;
+ }
+
+ actionPaths = mw.config.get( 'wgActionPaths' );
+ for ( key in actionPaths ) {
+ if ( actionPaths.hasOwnProperty( key ) ) {
+ parts = actionPaths[ key ].split( '$1' );
+ for ( i = 0; i < parts.length; i++ ) {
+ parts[ i ] = mw.RegExp.escape( parts[ i ] );
+ }
+ m = new RegExp( parts.join( '(.+)' ) ).exec( url );
+ if ( m && m[ 1 ] ) {
+ return key;
+ }
+
+ }
+ }
+
+ return 'view';
+ }
+
+ // Expose public methods
+ watch = {
+ updateWatchLink: updateWatchLink
+ };
+ module.exports = watch;
+
+ // Deprecated since 1.30
+ mw.log.deprecate( mw, 'page',
+ { watch: watch },
+ 'Use require( \'mediawiki.page.watch.ajax\' ) instead.',
+ 'mw.page'
+ );
+
+ $( function () {
+ var $links = $( '.mw-watchlink a[data-mw="interface"], a.mw-watchlink[data-mw="interface"]' );
+ if ( !$links.length ) {
+ // Fallback to the class-based exclusion method for backwards-compatibility
+ $links = $( '.mw-watchlink a, a.mw-watchlink' );
+ // Restrict to core interfaces, ignore user-generated content
+ $links = $links.filter( ':not( #bodyContent *, #content * )' );
+ }
+
+ $links.click( function ( e ) {
+ var mwTitle, action, api, $link;
+
+ mwTitle = mw.Title.newFromText( title );
+ action = mwUriGetAction( this.href );
+
+ if ( !mwTitle || ( action !== 'watch' && action !== 'unwatch' ) ) {
+ // Let native browsing handle the link
+ return true;
+ }
+ e.preventDefault();
+ e.stopPropagation();
+
+ $link = $( this );
+
+ if ( $link.hasClass( 'loading' ) ) {
+ return;
+ }
+
+ updateWatchLink( $link, action, 'loading' );
+
+ // Preload the notification module for mw.notify
+ mw.loader.load( 'mediawiki.notification' );
+
+ api = new mw.Api();
+
+ api[ action ]( title )
+ .done( function ( watchResponse ) {
+ var message, otherAction = action === 'watch' ? 'unwatch' : 'watch';
+
+ if ( mwTitle.getNamespaceId() > 0 && mwTitle.getNamespaceId() % 2 === 1 ) {
+ message = action === 'watch' ? 'addedwatchtext-talk' : 'removedwatchtext-talk';
+ } else {
+ message = action === 'watch' ? 'addedwatchtext' : 'removedwatchtext';
+ }
+
+ mw.notify( mw.message( message, mwTitle.getPrefixedText() ).parseDom(), {
+ tag: 'watch-self'
+ } );
+
+ // Set link to opposite
+ updateWatchLink( $link, otherAction );
+
+ // Update the "Watch this page" checkbox on action=edit when the
+ // page is watched or unwatched via the tab (T14395).
+ $( '#wpWatchthis' ).prop( 'checked', watchResponse.watched === true );
+ } )
+ .fail( function () {
+ var msg, link;
+
+ // Reset link to non-loading mode
+ updateWatchLink( $link, action );
+
+ // Format error message
+ link = mw.html.element(
+ 'a', {
+ href: mw.util.getUrl( title ),
+ title: mwTitle.getPrefixedText()
+ }, mwTitle.getPrefixedText()
+ );
+ msg = mw.message( 'watcherrortext', link );
+
+ // Report to user about the error
+ mw.notify( msg, {
+ tag: 'watch-self',
+ type: 'error'
+ } );
+ } );
+ } );
+ } );
+
+}( mediaWiki, jQuery ) );