Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F1426541
watch-ajax.js
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
15 KB
Referenced Files
None
Subscribers
None
watch-ajax.js
View Options
(
function
()
{
// The name of the page to watch or unwatch
const
pageTitle
=
mw
.
config
.
get
(
'wgRelevantPageName'
),
isWatchlistExpiryEnabled
=
require
(
'./config.json'
).
WatchlistExpiry
,
// Use Object.create( null ) instead of {} to get an Object without predefined properties.
// This avoids problems if the title is 'hasOwnPropery' or similar. Bug: T342137
watchstarsByTitle
=
Object
.
create
(
null
);
/**
* 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'
* @param {string} [expiry='infinity'] The expiry date if a page is being watched temporarily.
* @private
*/
function
updateWatchLinkAttributes
(
$link
,
action
,
state
,
expiry
)
{
// A valid but empty jQuery object shouldn't throw a TypeError
if
(
!
$link
.
length
)
{
return
;
}
expiry
=
expiry
||
'infinity'
;
// Invalid actions shouldn't silently turn the page in an unrecoverable state
if
(
action
!==
'watch'
&&
action
!==
'unwatch'
)
{
throw
new
Error
(
'Invalid action'
);
}
const
otherAction
=
action
===
'watch'
?
'unwatch'
:
'watch'
;
const
$li
=
$link
.
closest
(
'li'
);
if
(
state
!==
'loading'
)
{
// jQuery event, @deprecated in 1.38
// Trigger a 'watchpage' event for this List item.
// NB: A expiry of 'infinity' is cast to null here, but not above
$li
.
trigger
(
'watchpage.mw'
,
[
otherAction
,
mw
.
util
.
isInfinity
(
expiry
)
?
null
:
expiry
]
);
}
let
tooltipAction
=
action
;
let
daysLeftExpiry
=
null
;
let
watchExpiry
=
null
;
// Checking to see what if the expiry is set or indefinite to display the correct message
if
(
isWatchlistExpiryEnabled
&&
action
===
'unwatch'
)
{
if
(
mw
.
util
.
isInfinity
(
expiry
)
)
{
// Resolves to tooltip-ca-unwatch message
tooltipAction
=
'unwatch'
;
}
else
{
const
expiryDate
=
new
Date
(
expiry
);
const
currentDate
=
new
Date
();
// Using the Math.ceil function instead of floor so when, for example, a user selects one week
// the tooltip shows 7 days instead of 6 days (see Phab ticket T253936)
daysLeftExpiry
=
Math
.
ceil
(
(
expiryDate
-
currentDate
)
/
(
1000
*
60
*
60
*
24
)
);
if
(
daysLeftExpiry
>
0
)
{
// Resolves to tooltip-ca-unwatch-expiring message
tooltipAction
=
'unwatch-expiring'
;
}
else
{
// Resolves to tooltip-ca-unwatch-expiring-hours message
tooltipAction
=
'unwatch-expiring-hours'
;
}
watchExpiry
=
expiryDate
.
toISOString
();
}
}
const
msgKey
=
state
===
'loading'
?
action
+
'ing'
:
action
;
// The following messages can be used here:
// * watch
// * watching
// * unwatch
// * unwatching
const
msg
=
mw
.
msg
(
msgKey
);
const
link
=
$link
.
get
(
0
);
if
(
link
.
children
.
length
>
1
&&
link
.
lastElementChild
.
tagName
===
'SPAN'
)
{
// Handle updated button markup,
// where the watchstar contains an icon element and a span element containing the text
link
.
lastElementChild
.
textContent
=
msg
;
}
else
{
link
.
textContent
=
msg
;
}
$link
.
toggleClass
(
'loading'
,
state
===
'loading'
)
// The following messages can be used here:
// * tooltip-ca-watch
// * tooltip-ca-unwatch
// * tooltip-ca-unwatch-expiring
// * tooltip-ca-unwatch-expiring-hours
.
attr
(
'title'
,
mw
.
msg
(
'tooltip-ca-'
+
tooltipAction
,
daysLeftExpiry
)
)
.
updateTooltipAccessKeys
()
.
attr
(
'href'
,
mw
.
util
.
getUrl
(
pageTitle
,
{
action
:
action
}
)
)
.
attr
(
'data-mw-expiry'
,
watchExpiry
);
$li
.
toggleClass
(
'mw-watchlink-temp'
,
expiry
!==
null
&&
expiry
!==
'infinity'
);
// Most common ID style
if
(
state
!==
'loading'
&&
$li
.
prop
(
'id'
)
===
'ca-'
+
otherAction
)
{
$li
.
prop
(
'id'
,
'ca-'
+
action
);
}
}
/**
* Notify hooks listeners of the new page watch status
*
* Watchstars should not need to use this hook, as they are updated via
* callback, and automatically kept in sync if a watchstar with the same
* title is changed.
*
* This hook should by used by other interfaces that care if the watch
* status of the page has changed, e.g. an edit form which wants to
* update a 'watch this page' checkbox.
*
* Users which change the watch status of the page without using a
* watchstar (e.g. edit forms again) should use the updatePageWatchStatus
* method to ensure watchstars are updated and this hook is fired.
*
* @param {boolean} isWatched The page is watched
* @param {string} [expiry='infinity'] The expiry date if a page is being watched temporarily.
* @param {string} [expirySelected='infinite'] The expiry length that was just selected from a dropdown, e.g. '1 week'
* @private
*/
function
notifyPageWatchStatus
(
isWatched
,
expiry
,
expirySelected
)
{
expiry
=
expiry
||
'infinity'
;
expirySelected
=
expirySelected
||
'infinite'
;
/**
* Fires when the page watch status has changed.
*
* @event ~'wikipage.watchlistChange'
* @memberof Hooks
* @param {boolean} isWatched
* @param {string} expiry The expiry date if the page is being watched temporarily.
* @param {string} expirySelected The expiry length that was selected from a dropdown, e.g. '1 week'
* @example
* mw.hook( 'wikipage.watchlistChange' ).add( ( isWatched, expiry, expirySelected ) => {
* // Do things
* } );
*/
mw
.
hook
(
'wikipage.watchlistChange'
).
fire
(
isWatched
,
expiry
,
expirySelected
);
}
/**
* Update the page watch status.
*
* @memberof module:mediawiki.page.watch.ajax
* @param {boolean} isWatched The page is watched
* @param {string} [expiry='infinity'] The expiry date if a page is being watched temporarily.
* @param {string} [expirySelected='infinite'] The expiry length that was just selected from a dropdown, e.g. '1 week'
* @fires Hooks~'wikipage.watchlistChange'
* @stable
*/
function
updatePageWatchStatus
(
isWatched
,
expiry
,
expirySelected
)
{
// Update all watchstars associated with the current page
(
watchstarsByTitle
[
pageTitle
]
||
[]
).
forEach
(
(
w
)
=>
{
w
.
update
(
isWatched
,
expiry
);
}
);
notifyPageWatchStatus
(
isWatched
,
expiry
,
expirySelected
);
}
/**
* Update the link text, link `href` attribute and (if applicable) "loading" class.
*
* For an individual link being set to 'loading', the first
* argument can be a jQuery collection. When updating to an
* "idle" state, an {@link mw.Title} object should be passed to that
* all watchstars associated with that title are updated.
*
* @memberof module:mediawiki.page.watch.ajax
* @param {mw.Title|jQuery} titleOrLink Title of watchlinks to update (when state is idle), or an individual watchlink
* @param {string} action One of 'watch', 'unwatch'
* @param {string} [state="idle"] 'idle' or 'loading'. Default is 'idle'
* @param {string} [expiry='infinity'] The expiry date if a page is being watched temporarily.
* @param {string} [expirySelected='infinite'] The expiry length that was just selected from a dropdown, e.g. '1 week'
* @fires Hooks~'wikipage.watchlistChange'
* @stable
*/
function
updateWatchLink
(
titleOrLink
,
action
,
state
,
expiry
,
expirySelected
)
{
if
(
titleOrLink
instanceof
$
)
{
updateWatchLinkAttributes
(
titleOrLink
,
action
,
state
,
expiry
);
}
else
{
// Assumed state is 'idle' when update a group of watchstars by title
const
isWatched
=
action
===
'unwatch'
;
const
normalizedTitle
=
titleOrLink
.
getPrefixedDb
();
(
watchstarsByTitle
[
normalizedTitle
]
||
[]
).
forEach
(
(
w
)
=>
{
w
.
update
(
isWatched
,
expiry
,
expirySelected
);
}
);
if
(
normalizedTitle
===
pageTitle
)
{
notifyPageWatchStatus
(
isWatched
,
expiry
,
expirySelected
);
}
}
}
/**
* TODO: This should be moved somewhere more accessible.
*
* @param {string} url
* @return {string} The extracted action, defaults to 'view'
* @private
*/
function
mwUriGetAction
(
url
)
{
// TODO: Does MediaWiki give action path or query param
// precedence? If the former, move this to the bottom
const
action
=
mw
.
util
.
getParamValue
(
'action'
,
url
);
if
(
action
!==
null
)
{
return
action
;
}
const
actionPaths
=
mw
.
config
.
get
(
'wgActionPaths'
);
for
(
const
key
in
actionPaths
)
{
let
parts
=
actionPaths
[
key
].
split
(
'$1'
);
parts
=
parts
.
map
(
mw
.
util
.
escapeRegExp
);
const
m
=
new
RegExp
(
parts
.
join
(
'(.+)'
)
).
exec
(
url
);
if
(
m
&&
m
[
1
]
)
{
return
key
;
}
}
return
'view'
;
}
/**
* @private
*/
function
init
()
{
let
$pageWatchLinks
=
$
(
'.mw-watchlink a[data-mw="interface"], a.mw-watchlink[data-mw="interface"]'
);
if
(
!
$pageWatchLinks
.
length
)
{
// Fallback to the class-based exclusion method for backwards-compatibility
$pageWatchLinks
=
$
(
'.mw-watchlink a, a.mw-watchlink'
);
// Restrict to core interfaces, ignore user-generated content
$pageWatchLinks
=
$pageWatchLinks
.
filter
(
':not( #bodyContent *, #content * )'
);
}
if
(
$pageWatchLinks
.
length
)
{
watchstar
(
$pageWatchLinks
,
pageTitle
);
}
}
/**
* Class representing an individual watchstar
*
* @param {jQuery} $link Watch element
* @param {mw.Title} title Title
* @param {module:mediawiki.page.watch.ajax~callback} [callback]
* @private
*/
function
Watchstar
(
$link
,
title
,
callback
)
{
this
.
$link
=
$link
;
this
.
title
=
title
;
this
.
callback
=
callback
;
}
/**
* Update the watchstar
*
* @param {boolean} isWatched The page is watched
* @param {string} [expiry='infinity'] The expiry date if a page is being watched temporarily.
* @private
*/
Watchstar
.
prototype
.
update
=
function
(
isWatched
,
expiry
)
{
expiry
=
expiry
||
'infinity'
;
updateWatchLinkAttributes
(
this
.
$link
,
isWatched
?
'unwatch'
:
'watch'
,
'idle'
,
expiry
);
if
(
this
.
callback
)
{
/**
* @callback module:mediawiki.page.watch.ajax~callback
* @param {jQuery} $link The element being manipulated.
* @param {boolean} isWatched Whether the page is now watched.
* @param {string} expiry The expiry date if the page is being watched temporarily,
* or an 'infinity'-like value (see [mw.util.isIninity()]{@link module:mediawiki.util.isInfinity})
*/
this
.
callback
(
this
.
$link
,
isWatched
,
expiry
);
}
};
/**
* Bind a given watchstar element to make it interactive.
*
* This is meant to allow binding of watchstars for arbitrary page titles,
* especially if different from the currently viewed page. As such, this function
* will *not* synchronise its state with any "Watch this page" checkbox such as
* found on the "Edit page" and "Publish changes" forms. The caller should either make
* "current page" watchstars picked up by init (and not use this function) or sync it manually
* from the callback this function provides.
*
* @memberof module:mediawiki.page.watch.ajax
* @param {jQuery} $links One or more anchor elements that must have an href
* with a URL containing a `action=watch` or `action=unwatch` query parameter,
* from which the current state will be learned (e.g. link to unwatch is currently watched)
* @param {string} title Title of page that this watchstar will affect
* @param {module:mediawiki.page.watch.ajax~callback} [callback] Callback to run after the action has been
* processed and API request completed.
* @stable
*/
function
watchstar
(
$links
,
title
,
callback
)
{
// Set up the ARIA connection between the watch link and the notification.
// This is set outside the click handler so that it's already present when the user clicks.
const
notificationId
=
'mw-watchlink-notification'
;
const
mwTitle
=
mw
.
Title
.
newFromText
(
title
);
if
(
!
mwTitle
)
{
return
;
}
const
normalizedTitle
=
mwTitle
.
getPrefixedDb
();
watchstarsByTitle
[
normalizedTitle
]
=
watchstarsByTitle
[
normalizedTitle
]
||
[];
$links
.
each
(
function
()
{
watchstarsByTitle
[
normalizedTitle
].
push
(
new
Watchstar
(
$
(
this
),
mwTitle
,
callback
)
);
}
);
$links
.
attr
(
'aria-controls'
,
notificationId
);
// Add click handler.
$links
.
on
(
'click'
,
function
(
e
)
{
const
action
=
mwUriGetAction
(
this
.
href
);
if
(
!
mwTitle
||
(
action
!==
'watch'
&&
action
!==
'unwatch'
)
)
{
// Let native browsing handle the link
return
true
;
}
e
.
preventDefault
();
e
.
stopPropagation
();
const
$link
=
$
(
this
);
// eslint-disable-next-line no-jquery/no-class-state
if
(
$link
.
hasClass
(
'loading'
)
)
{
return
;
}
updateWatchLinkAttributes
(
$link
,
action
,
'loading'
);
// Preload the notification module for mw.notify
const
modulesToLoad
=
[
'mediawiki.notification'
];
// Preload watchlist expiry widget so it runs in parallel with the api call
if
(
isWatchlistExpiryEnabled
)
{
modulesToLoad
.
push
(
'mediawiki.watchstar.widgets'
);
}
mw
.
loader
.
load
(
modulesToLoad
);
const
api
=
new
mw
.
Api
();
api
[
action
](
title
)
.
done
(
(
watchResponse
)
=>
{
const
isWatched
=
watchResponse
.
watched
===
true
;
let
message
=
isWatched
?
'addedwatchtext'
:
'removedwatchtext'
;
if
(
mwTitle
.
isTalkPage
()
)
{
message
+=
'-talk'
;
}
let
notifyPromise
;
let
watchlistPopup
;
// @since 1.35 - pop up notification will be loaded with OOUI
// only if Watchlist Expiry is enabled
if
(
isWatchlistExpiryEnabled
)
{
if
(
isWatched
)
{
// The message should include `infinite` watch period
message
=
mwTitle
.
isTalkPage
()
?
'addedwatchindefinitelytext-talk'
:
'addedwatchindefinitelytext'
;
}
notifyPromise
=
mw
.
loader
.
using
(
'mediawiki.watchstar.widgets'
).
then
(
(
require
)
=>
{
const
WatchlistExpiryWidget
=
require
(
'mediawiki.watchstar.widgets'
);
if
(
!
watchlistPopup
)
{
watchlistPopup
=
new
WatchlistExpiryWidget
(
action
,
title
,
updateWatchLink
,
{
// The following messages can be used here:
// * addedwatchindefinitelytext-talk
// * addedwatchindefinitelytext
// * removedwatchtext-talk
// * removedwatchtext
message
:
mw
.
message
(
message
,
mwTitle
.
getPrefixedText
()
).
parseDom
(),
$link
:
$link
}
);
}
mw
.
notify
(
watchlistPopup
.
$element
,
{
tag
:
'watch-self'
,
id
:
notificationId
,
autoHideSeconds
:
'short'
}
);
}
);
}
else
{
// The following messages can be used here:
// * addedwatchtext-talk
// * addedwatchtext
// * removedwatchtext-talk
// * removedwatchtext
notifyPromise
=
mw
.
notify
(
mw
.
message
(
message
,
mwTitle
.
getPrefixedText
()
).
parseDom
(),
{
tag
:
'watch-self'
,
id
:
notificationId
}
);
}
// The notifications are stored as a promise and the watch link is only updated
// once it is resolved. Otherwise, if $wgWatchlistExpiry set, the loading of
// OOUI could cause a race condition and the link is updated before the popup
// actually is shown. See T263135
notifyPromise
.
always
(
()
=>
{
// Update all watchstars associated with this title
watchstarsByTitle
[
normalizedTitle
].
forEach
(
(
w
)
=>
{
w
.
update
(
isWatched
);
}
);
// For the current page, also trigger the hook
if
(
normalizedTitle
===
pageTitle
)
{
notifyPageWatchStatus
(
isWatched
);
}
}
);
}
)
.
fail
(
(
code
,
data
)
=>
{
// Reset link to non-loading mode
updateWatchLinkAttributes
(
$link
,
action
);
// Format error message
const
$msg
=
api
.
getErrorMessage
(
data
);
// Report to user about the error
mw
.
notify
(
$msg
,
{
tag
:
'watch-self'
,
type
:
'error'
,
id
:
notificationId
}
);
}
);
}
);
}
$
(
init
);
/**
* Animate watch/unwatch links to use asynchronous API requests to
* watch pages, rather than navigating to a different URI.
*
* @example
* var watch = require( 'mediawiki.page.watch.ajax' );
* watch.updateWatchLink(
* $node,
* 'watch',
* 'loading'
* );
* // When the watch status of the page has been updated:
* watch.updatePageWatchStatus( true );
*
* @exports mediawiki.page.watch.ajax
*/
module
.
exports
=
{
watchstar
:
watchstar
,
updateWatchLink
:
updateWatchLink
,
updatePageWatchStatus
:
updatePageWatchStatus
};
}()
);
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, May 16, 13:21 (1 d, 13 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
94/7e/2202b42a21a8344e8f4faa73bdfe
Default Alt Text
watch-ajax.js (15 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment