Merge "Cleanup page creation in RevisionIntegrationTest"
[lhc/web/wiklou.git] / resources / src / mediawiki / page / watch.js
1 /**
2 * Animate watch/unwatch links to use asynchronous API requests to
3 * watch pages, rather than navigating to a different URI.
4 *
5 * Usage:
6 *
7 * var watch = require( 'mediawiki.page.watch.ajax' );
8 * watch.updateWatchLink(
9 * $node,
10 * 'watch',
11 * 'loading'
12 * );
13 *
14 * @class mw.plugin.page.watch.ajax
15 * @singleton
16 */
17 ( function ( mw, $ ) {
18 var watch,
19 // The name of the page to watch or unwatch
20 title = mw.config.get( 'wgRelevantPageName' );
21
22 /**
23 * Update the link text, link href attribute and (if applicable)
24 * "loading" class.
25 *
26 * @param {jQuery} $link Anchor tag of (un)watch link
27 * @param {string} action One of 'watch', 'unwatch'
28 * @param {string} [state="idle"] 'idle' or 'loading'. Default is 'idle'
29 */
30 function updateWatchLink( $link, action, state ) {
31 var msgKey, $li, otherAction;
32
33 // A valid but empty jQuery object shouldn't throw a TypeError
34 if ( !$link.length ) {
35 return;
36 }
37
38 // Invalid actions shouldn't silently turn the page in an unrecoverable state
39 if ( action !== 'watch' && action !== 'unwatch' ) {
40 throw new Error( 'Invalid action' );
41 }
42
43 // message keys 'watch', 'watching', 'unwatch' or 'unwatching'.
44 msgKey = state === 'loading' ? action + 'ing' : action;
45 otherAction = action === 'watch' ? 'unwatch' : 'watch';
46 $li = $link.closest( 'li' );
47
48 // Trigger a 'watchpage' event for this List item.
49 // Announce the otherAction value as the first param.
50 // Used to monitor the state of watch link.
51 // TODO: Revise when system wide hooks are implemented
52 if ( state === undefined ) {
53 $li.trigger( 'watchpage.mw', otherAction );
54 }
55
56 $link
57 .text( mw.msg( msgKey ) )
58 .attr( 'title', mw.msg( 'tooltip-ca-' + action ) )
59 .updateTooltipAccessKeys()
60 .attr( 'href', mw.util.getUrl( title, { action: action } ) );
61
62 // Most common ID style
63 if ( $li.prop( 'id' ) === 'ca-' + otherAction ) {
64 $li.prop( 'id', 'ca-' + action );
65 }
66
67 if ( state === 'loading' ) {
68 $link.addClass( 'loading' );
69 } else {
70 $link.removeClass( 'loading' );
71 }
72 }
73
74 /**
75 * TODO: This should be moved somewhere more accessible.
76 *
77 * @private
78 * @param {string} url
79 * @return {string} The extracted action, defaults to 'view'
80 */
81 function mwUriGetAction( url ) {
82 var action, actionPaths, key, i, m, parts;
83
84 // TODO: Does MediaWiki give action path or query param
85 // precedence? If the former, move this to the bottom
86 action = mw.util.getParamValue( 'action', url );
87 if ( action !== null ) {
88 return action;
89 }
90
91 actionPaths = mw.config.get( 'wgActionPaths' );
92 for ( key in actionPaths ) {
93 if ( actionPaths.hasOwnProperty( key ) ) {
94 parts = actionPaths[ key ].split( '$1' );
95 for ( i = 0; i < parts.length; i++ ) {
96 parts[ i ] = mw.RegExp.escape( parts[ i ] );
97 }
98 m = new RegExp( parts.join( '(.+)' ) ).exec( url );
99 if ( m && m[ 1 ] ) {
100 return key;
101 }
102
103 }
104 }
105
106 return 'view';
107 }
108
109 // Expose public methods
110 watch = {
111 updateWatchLink: updateWatchLink
112 };
113 module.exports = watch;
114
115 $( function () {
116 var $links = $( '.mw-watchlink a[data-mw="interface"], a.mw-watchlink[data-mw="interface"]' );
117 if ( !$links.length ) {
118 // Fallback to the class-based exclusion method for backwards-compatibility
119 $links = $( '.mw-watchlink a, a.mw-watchlink' );
120 // Restrict to core interfaces, ignore user-generated content
121 $links = $links.filter( ':not( #bodyContent *, #content * )' );
122 }
123
124 $links.click( function ( e ) {
125 var mwTitle, action, api, $link;
126
127 mwTitle = mw.Title.newFromText( title );
128 action = mwUriGetAction( this.href );
129
130 if ( !mwTitle || ( action !== 'watch' && action !== 'unwatch' ) ) {
131 // Let native browsing handle the link
132 return true;
133 }
134 e.preventDefault();
135 e.stopPropagation();
136
137 $link = $( this );
138
139 if ( $link.hasClass( 'loading' ) ) {
140 return;
141 }
142
143 updateWatchLink( $link, action, 'loading' );
144
145 // Preload the notification module for mw.notify
146 mw.loader.load( 'mediawiki.notification' );
147
148 api = new mw.Api();
149
150 api[ action ]( title )
151 .done( function ( watchResponse ) {
152 var message, otherAction = action === 'watch' ? 'unwatch' : 'watch';
153
154 if ( mwTitle.getNamespaceId() > 0 && mwTitle.getNamespaceId() % 2 === 1 ) {
155 message = action === 'watch' ? 'addedwatchtext-talk' : 'removedwatchtext-talk';
156 } else {
157 message = action === 'watch' ? 'addedwatchtext' : 'removedwatchtext';
158 }
159
160 mw.notify( mw.message( message, mwTitle.getPrefixedText() ).parseDom(), {
161 tag: 'watch-self'
162 } );
163
164 // Set link to opposite
165 updateWatchLink( $link, otherAction );
166
167 // Update the "Watch this page" checkbox on action=edit when the
168 // page is watched or unwatched via the tab (T14395).
169 $( '#wpWatchthis' ).prop( 'checked', watchResponse.watched === true );
170 } )
171 .fail( function () {
172 var msg, link;
173
174 // Reset link to non-loading mode
175 updateWatchLink( $link, action );
176
177 // Format error message
178 link = mw.html.element(
179 'a', {
180 href: mw.util.getUrl( title ),
181 title: mwTitle.getPrefixedText()
182 }, mwTitle.getPrefixedText()
183 );
184 msg = mw.message( 'watcherrortext', link );
185
186 // Report to user about the error
187 mw.notify( msg, {
188 tag: 'watch-self',
189 type: 'error'
190 } );
191 } );
192 } );
193 } );
194
195 }( mediaWiki, jQuery ) );