2 * JavaScript for Special:Watchlist
5 function trimStart( s ) {
6 return s.replace( /^ /, '' );
9 function trimEnd( s ) {
10 return s.endsWith( ' ' ) ? s.slice( 0, -1 ) : s;
14 const api = new mw.Api();
15 const $resetForm = $( '#mw-watchlist-resetbutton' );
18 // If the user wants to reset their watchlist, use an API call to do so (no reload required)
19 // Adapted from a user script by User:NQ of English Wikipedia
20 // (User:NQ/WatchlistResetConfirm.js)
21 $resetForm.on( 'submit', ( event ) => {
22 const $button = $resetForm.find( 'input[name=mw-watchlist-reset-submit]' );
24 event.preventDefault();
26 // Disable reset button to prevent multiple concurrent requests
27 $button.prop( 'disabled', true );
29 if ( !$progressBar ) {
30 $progressBar = new OO.ui.ProgressBarWidget( { progress: false } ).$element;
32 position: 'absolute', width: '100%'
36 $resetForm.append( $progressBar );
38 // Use action=setnotificationtimestamp to mark all as visited,
39 // then set all watchlist lines accordingly
40 api.postWithToken( 'csrf', {
41 formatversion: 2, action: 'setnotificationtimestamp', entirewatchlist: true
43 // Enable button again
44 $button.prop( 'disabled', false );
45 // Hide the button because further clicks can not generate any visual changes
46 $button.css( 'visibility', 'hidden' );
47 $progressBar.detach();
48 $( '.mw-changeslist-line-watched' )
49 .removeClass( 'mw-changeslist-line-watched' )
50 .addClass( 'mw-changeslist-line-not-watched' );
52 // On error, fall back to server-side reset
53 // First remove this submit listener and then re-submit the form
54 $resetForm.off( 'submit' ).trigger( 'submit' );
58 // if the user wishes to reload the watchlist whenever a filter changes
59 if ( mw.user.options.get( 'watchlistreloadautomatically' ) ) {
60 // add a listener on all form elements in the header form
61 $( '#mw-watchlist-form input, #mw-watchlist-form select' ).on( 'change', () => {
62 // submit the form when one of the input fields is modified
63 $( '#mw-watchlist-form' ).trigger( 'submit' );
67 if ( mw.user.options.get( 'watchlistunwatchlinks' ) ) {
68 // Watch/unwatch toggle link:
69 // If a page is on the watchlist, a '×' is shown which, when clicked, removes the page from the watchlist.
70 // After unwatching a page, the '×' becomes a '+', which if clicked re-watches the page.
71 // Unwatched page entries are struck through and have lowered opacity.
72 $( '.mw-changeslist' ).on( 'click', '.mw-unwatch-link, .mw-watch-link', function ( event ) {
73 const $unwatchLink = $( this ), // EnhancedChangesList uses <table> for each row, while OldChangesList uses <li> for each row
74 $watchlistLine = $unwatchLink.closest( 'li, table' )
75 .find( '[data-target-page]' ),
76 pageTitle = String( $watchlistLine.data( 'targetPage' ) ),
77 isTalk = mw.Title.newFromText( pageTitle ).isTalkPage();
79 // Utility function for looping through each watchlist line that matches
80 // a certain page or its associated page (e.g. Talk)
81 function forEachMatchingTitle( title, callback ) {
83 const titleObj = mw.Title.newFromText( title ),
84 associatedTitleObj = titleObj.isTalkPage() ? titleObj.getSubjectPage() : titleObj.getTalkPage(),
85 associatedTitle = associatedTitleObj.getPrefixedText();
86 $( '.mw-changeslist-line' ).each( function () {
87 const $line = $( this );
89 $line.find( '[data-target-page]' ).each( function () {
90 const $this = $( this ), rowTitle = String( $this.data( 'targetPage' ) );
91 if ( rowTitle === title || rowTitle === associatedTitle ) {
93 // EnhancedChangesList groups log entries by performer rather than target page. Therefore...
94 // * If using OldChangesList, use the <li>
95 // * If using EnhancedChangesList and $this is part of a grouped log entry, use the <td> sub-entry
96 // * If using EnhancedChangesList and $this is not part of a grouped log entry, use the <table> grouped entry
99 'li, .mw-enhancedchanges-checkbox + table.mw-changeslist-log td[data-target-page], table' );
100 const $link = $row.find( '.mw-unwatch-link, .mw-watch-link' );
102 callback( rowTitle, $row, $link );
108 // Preload the notification module for mw.notify
109 mw.loader.load( 'mediawiki.notification' );
111 // Depending on whether we are watching or unwatching, for each entry of the page (and its associated page i.e. Talk),
112 // change the text, tooltip, and non-JS href of the (un)watch button, and update the styling of the watchlist entry.
113 // eslint-disable-next-line no-jquery/no-class-state
114 if ( $unwatchLink.hasClass( 'mw-unwatch-link' ) ) {
115 api.unwatch( pageTitle )
117 forEachMatchingTitle( pageTitle,
118 ( rowPageTitle, $row, $rowUnwatchLink ) => {
120 .text( mw.msg( 'watchlist-unwatch-undo' ) )
121 .attr( 'title', mw.msg( 'tooltip-ca-watch' ) )
123 mw.util.getUrl( rowPageTitle, { action: 'watch' } ) )
124 .removeClass( 'mw-unwatch-link loading' )
125 .addClass( 'mw-watch-link' );
127 '.mw-changeslist-line-inner, .mw-enhanced-rc-nested' )
128 .addBack( '.mw-enhanced-rc-nested' ) // For matching log sub-entry
129 .addClass( 'mw-changelist-line-inner-unwatched' );
133 mw.message( isTalk ? 'removedwatchtext-talk' : 'removedwatchtext',
134 pageTitle ), { tag: 'watch-self' } );
137 api.watch( pageTitle )
139 forEachMatchingTitle( pageTitle,
140 ( rowPageTitle, $row, $rowUnwatchLink ) => {
142 .text( mw.msg( 'watchlist-unwatch' ) )
143 .attr( 'title', mw.msg( 'tooltip-ca-unwatch' ) )
145 mw.util.getUrl( rowPageTitle, { action: 'unwatch' } ) )
146 .removeClass( 'mw-watch-link loading' )
147 .addClass( 'mw-unwatch-link' );
148 $row.find( '.mw-changelist-line-inner-unwatched' )
149 .addBack( '.mw-enhanced-rc-nested' )
150 .removeClass( 'mw-changelist-line-inner-unwatched' );
151 $row.find( '.mw-changesList-watchlistExpiry' ).each( function () {
152 // Add the missing semicolon (T266747)
153 const $expiry = $( this );
154 $expiry.next( '.mw-changeslist-separator' )
155 .addClass( 'mw-changeslist-separator--semicolon' )
156 .removeClass( 'mw-changeslist-separator' );
157 // Remove the spaces before and after the expiry icon
158 this.nextSibling.nodeValue = trimStart( this.nextSibling.nodeValue );
159 this.previousSibling.nodeValue = trimEnd( this.previousSibling.nodeValue );
166 mw.message( isTalk ? 'addedwatchtext-talk' : 'addedwatchtext',
167 pageTitle ), { tag: 'watch-self' } );
171 event.preventDefault();
172 event.stopPropagation();
173 $unwatchLink.trigger( 'blur' );