MediaWiki:Gadget-WatchlistNotice.core.js
Jump to navigation
Jump to search
Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
![]() | This user script seems to have a documentation page at MediaWiki:Gadget-WatchlistNotice.core and an accompanying .css page at MediaWiki:Gadget-WatchlistNotice.core.css. |
/***********************************************************************
************************ WatchlistNotice ******************************
***********************************************************************
***********************************************************************
* @description
* This is part of the Commons Notice system, formerly known as
* Watchlist Notice. The system provides support for targeting messages
* to users who share a common property like living in the same country
* or being administrators and allows them to dismiss messages.
*
* It also empowers the user to choose which messages should be displayed
* in which manner.
*
* More information at [[Help:Watchlist messages]].
*
*
* Porting the gadget to other Wikis:
* You need everything listed at [[Help:Watchlist messages]] +
* all ext.gadget-dependencies. You can see all the dependencies at
* [[MediaWiki:Gadgets-definition]] and [[Special:Gadgets]]
*
* You have to register all the dependencies at [[MediaWiki:Gadgets-definition]].
* You have to register the loader-gadget and the core at [[MediaWiki:Gadgets-definition]].
*
*
* @author
* Original Authors: [[:en:w:User:Ruud Koot|Ruud Koot]], [[commons:User:Dschwen]]
* New jQuery version by: [[User:Whym]]
* A lot of addidional featurs by: [[User:Rillke]]
*
* @jsvalidate
* Please make sure keeping this script jshint valid
*
* @terminology
* The term notice or note covers the whole section or mechanism
* One unit containing information is referenced as "message"
*
* @documentation
* [[Help:Watchlist messages]]
*
* <nowiki>
*/
/*global jQuery:false, mediaWiki:false, Geo:false*/
/*jshint curly:false, undef:true, unused:true */
(function(mw, $) {
'use strict';
var $notice = $('#watchlist-message');
if ($notice.length === 0) return;
// These messages will be overwritten by localized versions
// The loacalized versions are shiped with the watchlist
// They are inlcuded in a hidden div and make use of [[Help:Autotranslate]]
var i18n = {
'dwn-dlg-title': "{{SITENAME}} notice configuration",
'dwn-dlg-save': "Save",
'dwn-dlg-cancel': "Cancel",
'dwn-dlg-saving': "Saving your preferences",
'dwn-dlg-save-success': "{{SITENAME}} notice configuration saved and will be applied after page was reloaded",
'dwn-dlg-save-err': "Options could not be saved",
'dwn-type-h': "Message topics and types",
'dwn-type-desc': "Select the topics you would like to get notifications about",
'dwn-type-summary': "You will receive messages about $1",
'dwn-display-h': "Display",
'dwn-display-desc': "Adjust how the messages are displayed",
'dwn-display-fade': "Show only one message at once",
'dwn-display-collapse': "Fold notice section if it is empty",
'dwn-display-disable': "Disable {{SITENAME}} notices",
'dwn-display-disabled-info': "{{SITENAME}} notices disabled. The gadget can be reactivated in your preferences.",
'dwn-display-not-enabled': "The {{SITENAME}} notice gadget is already disabled. The effect will be visible after reloading this page.",
'dwn-display-cycle': "Step through the messages automatically",
'dwn-colon': ":",
'dwn-cfg': "Config notices",
'dwn-go-msg': "Go to message $1",
'dwn-mark-as-read': "read",
'dwn-mark-as-read-details': "Hide this message",
'dwn-no-msg': "There are no new {{SITENAME}} messages."
};
mw.messages.set(i18n);
// The CSS that will be used for blocking the dialog
// when options were changed and we are waiting for an AJAX request
var blockCSS = {
border: 'none',
padding: '15px',
backgroundColor: '#000',
borderRadius: '10px',
opacity: 0.5
};
// Small helper functions returning the message text for a given key
// The _msgp parses the message (so e.g. wikilinks are resolved)
var _msg = function(params) {
/*jshint unused:false */
var args = Array.prototype.slice.call(arguments);
args[0] = 'dwn-' + args[0];
return mw.msg.apply(mw, args);
},
_msgp = function(params) {
/*jshint unused:false */
var args = Array.prototype.slice.call(arguments);
args[0] = 'dwn-' + args[0];
var msg = mw.message.apply(mw, args);
return msg.parse();
};
// dwn stands for Dismiss Watchlist Note
var dwn = {
// Always bump this number when making changes
version: '1.0.9.0',
pefKey: 'dwn',
dismissKey: 'dwnd',
$notice: $notice,
$noticeInner: $('#watchlist-message-inner'),
$types: $('#wln-types'),
$msgs: $('.watchlist-message'),
storageTTL: 14, // in days (1 day = 24 hours)
defaultconfig: {
types: {}, // Types included here will be _un_subscribed-by-default
fade: true, // Whether to use a fading effect or whether to show all messages at once
collapse: true, // Whether to close the watchlist message section if it is empty
cycle: true, // Whether to step through the messages automatically
duration: 10 // (currently no UI option for this): Duration to show a message.
// The duration is automatically adapted to the text's length.
},
/**
* Loads the required modules and calls the method to show the configuration dialog
* This function may be added added as an jquery observer for a click-event
*
* @example
* $('#myButton').click(dwn.configScreen);
*
* @param e {Object} A jQuery Event object.
* @context {Object} May be a jQuery objcet but is not required
* @return {undefined}
*/
configScreen: function(e) {
if (e && $.isFunction(e.preventDefault)) e.preventDefault();
if (dwn.$prefLink.hasClass('ui-state-disabled')) return;
dwn.$prefLink.addClass('ui-state-disabled');
var toLoad = ['jquery.ui', 'ext.gadget.libJQuery', 'ext.gadget.jquery.blockUI'];
mw.loader.using(toLoad, function() {
mw.libs.settingsManager.fetchGadgetSetting(dwn.pefKey, ['option']).done(function(prefName, settingValue) {
dwn.config = $.extend(true, dwn.defaultconfig, settingValue);
dwn._configScreen();
});
});
},
/**
* Shows the configuration dialog as depicted in
* [[File:2013-05-23-Gadget-WatchlistNotice-Config-Dlg.png]]
*
* @context {any} May be called in and from all contexts.
* @return {any} Don't assume anything. It's a UI method.
*/
_configScreen: function() {
var $dlg,
$dlgContent1, $typeWrap1, $typeInfoAjax, $typeInfoList,
$dlgContent2, $typeWrap2, $typeInfoSummary, $displayPrefWrap,
$displayPrefFadeW, $displayPrefFade, $displayPrefCollapseW, $displayPrefCollapse,
$displayPrefCycleW, $displayPrefCycle, $displayDisable,
$clonedTypes,
$types = $(),
selected = [],
dlgButtons = {};
$dlg = $('<div>').css('padding', 0);
$('<div id="wln-version"></div>').text(dwn.version).appendTo($dlg);
$dlgContent1 = $('<div class="wln-dlg-content"></div>').appendTo($dlg);
$('<h3>').text(_msg('type-h')).appendTo($dlgContent1);
$typeWrap1 = $('<div>').appendTo($dlgContent1);
$('<p>').text(_msg('type-desc')).appendTo($typeWrap1);
$typeInfoAjax = $('<div class="wln-prefscreen-dynamic"></div>').css('max-height', Math.max(Math.min($(window).height() - 500, 400), 150)).appendTo($dlg);
$typeInfoList = $('<ol>').appendTo($typeInfoAjax);
$dlgContent2 = $('<div class="wln-dlg-content"></div>').appendTo($dlg);
$typeWrap2 = $('<div>').appendTo($dlgContent2);
$typeInfoSummary = $('<p class="wln-dlg-summary"></p>').appendTo($typeWrap2);
$('<h3>').text(_msg('display-h')).appendTo($dlgContent2);
$displayPrefWrap = $('<div>').appendTo($dlgContent2);
$('<p>').text(_msg('display-desc')).appendTo($displayPrefWrap);
$displayPrefFadeW = $('<div>').appendTo($displayPrefWrap);
$displayPrefFade = $('<input id="wln_pref_disp_fade" type="checkbox"/>').appendTo($displayPrefFadeW);
$('<label for="wln_pref_disp_fade"></label>').text(_msg('display-fade')).appendTo($displayPrefFadeW);
$displayPrefCollapseW = $('<div>').appendTo($displayPrefWrap);
$displayPrefCollapse = $('<input id="wln_pref_disp_collapse" type="checkbox"/>').appendTo($displayPrefCollapseW);
$('<label for="wln_pref_disp_collapse"></label>').text(_msg('display-collapse')).appendTo($displayPrefCollapseW);
$displayPrefCycleW = $('<div>').appendTo($displayPrefWrap);
$displayPrefCycle = $('<input id="wln_pref_disp_cycle" type="checkbox"/>').appendTo($displayPrefCycleW);
$('<label for="wln_pref_disp_cycle"></label>').text(_msg('display-cycle')).appendTo($displayPrefCycleW);
$displayDisable = $('<button type="button" role="button"></button>').text(_msgp('display-disable')).button().appendTo($displayPrefWrap);
/*
Save settings
*/
dlgButtons[_msg('dlg-save')] = function() {
dwn.config.types = {};
$clonedTypes.not('.ui-selected').each(function(i, el) {
dwn.config.types[$(el).data('id')] = 1;
});
dwn.config.fade = $displayPrefFade[0].checked;
dwn.config.collapse = $displayPrefCollapse[0].checked;
dwn.config.cycle = $displayPrefCycle[0].checked;
$dlg.closest('.ui-dialog').block({
css: blockCSS,
message: '<h3 style="color:#fff">' + mw.html.escape(_msg('dlg-saving')) + '</h3>'
});
mw.libs.settingsManager.switchGadgetPref(dwn.pefKey, dwn.config).done(function() {
$dlg.closest('.ui-dialog').unblock().fadeOut(function() {
mw.notify($('<div class="wln-ok-sign"></div>').text(_msgp('dlg-save-success')));
$dlg.dialog('close');
});
}).fail(function() {
mw.notify($('<div class="wln-err-sign"></div>').text(_msg('dlg-save-err')));
$dlg.closest('.ui-dialog').unblock();
});
};
dlgButtons[_msg('dlg-cancel')] = function() {
$dlg.dialog('close');
};
$displayDisable.click(function() {
var g = mw.libs.settingsManager.gadget( 'WatchlistNotice' );
if (g.isEnabled()) {
g.disable(function() {
$dlg.closest('.ui-dialog').unblock().fadeOut(function() {
// Security hint: You must parse this message when setting HTML!
mw.notify($('<div class="wln-ok-sign"></div>').html(_msgp('display-disabled-info')));
$dlg.dialog('close');
});
}, function() {
mw.notify($('<div class="wln-err-sign"></div>').text(_msg('dlg-save-err')));
$dlg.closest('.ui-dialog').unblock();
});
$dlg.closest('.ui-dialog').block({
css: blockCSS,
message: '<h3 style="color:#fff">' + mw.html.escape(_msg('dlg-saving')) + '</h3>'
});
} else {
mw.notify($('<div class="wln-ok-sign"></div>').text(_msgp('display-not-enabled')));
}
});
dwn.$types.find('tr').each(function(i, el) {
if (!i) return; // Skip first row, which is the header row
var $tr = $(el),
$tds = $tr.find('td'),
id = $.trim($tds.eq(0).text()),
ts = $.trim($tds.eq(1).text()),
c = dwn.config,
$type = $('<li>').data({
id: id,
ts: ts
});
if (!c.types[id]) {
$type.addClass('ui-selected');
}
$('<b>').text(ts + _msg('colon')).appendTo($type);
$('<div>').text($tds.eq(2).text()).appendTo($type);
$types = $types.add($type);
});
/*
Apply settings
*/
$clonedTypes = $types.clone(true).addClass('ui-selectee').click(function() {
selected = [];
$(this).toggleClass('ui-selected');
$clonedTypes.filter('.ui-selected').each(function(i, el) {
selected.push($(el).data('ts'));
});
$typeInfoSummary.text(_msg('type-summary', selected.join(', ')));
}).mousedown(function(e) {
if (1 === e.which) $(this).addClass('ui-selecting');
}).mouseup(function() {
$(this).removeClass('ui-selecting');
}).mouseleave(function() {
$(this).removeClass('ui-selecting');
}).appendTo($typeInfoList);
$displayPrefFade[0].checked = dwn.config.fade;
$displayPrefCollapse[0].checked = dwn.config.collapse;
$displayPrefCycle[0].checked = dwn.config.cycle;
/*
Show a dialog
*/
$dlg.dialog({
position: {
my: 'right top',
at: 'right bottom',
of: dwn.$noticeInner
},
title: _msgp('dlg-title'),
dialogClass: 'wln-prefscreen',
resizable: false,
buttons: dlgButtons,
close: function() {
$dlg.remove();
dwn.$prefLink.removeClass('ui-state-disabled');
},
open: function() {
var $buttons = $(this).parent().find('.ui-dialog-buttonpane button');
$buttons.eq(0).specialButton('proceed');
$buttons.eq(1).specialButton('cancel');
}
});
$dlg.closest('.ui-dialog').find('.ui-dialog-titlebar-close')._jqInteraction();
},
/**
* Creates a link that belongs to one message
*
* @example
* var $link = dwn._$msgLink(1, $myMessage);
*
* @param i {number} The ordinal number of the message.
* @param $el {object} Instance of jQuery: The message the link will belong to.
* @context {any} May be called in and from all contexts.
* @return {object} Instance of jQuery: The created link.
*/
_$msgLink: function(i, $el) {
var $b = $('<a>').attr({
href: '#wln' + i,
title: _msg('go-msg', (i + 1)),
'class': 'ui-state-default'
}).css({
display: 'inline-block',
padding: '1px',
'text-align': 'center',
width: '1.4em'
}).text(i + 1).click(dwn._goToMessage);
$el.data('$b', $b);
$b.data('$msg', $el);
return $b._jqInteraction();
},
/**
* Sets a timeout which will, when expired, show the next message
*
* @example
* dwn._rotationTimeout(factor);
*
* @param factor {number} Numbers smaller than 1 (but > 0) will speed-up
* cycling through all messages, numbers bigger than 1 will slow it down.
* @context {any} May be called in and from all contexts.
* @return {any} Don't rely on it.
*/
_rotationTimeout: function(t) {
if (dwn.rotationtimeout) clearTimeout(dwn.rotationtimeout);
var textlen = dwn.$lastShownNote ? dwn.$lastShownNote.text().length : 150;
dwn.rotationtimeout = setTimeout(function() {
dwn.rotation();
}, ((t || 1) * 1000 * dwn.config.duration * (textlen/150) + 1000));
},
/**
* Loads the required modules and calls the method,
* which will show the navigation panel
*
* @example
* dwn.panel();
*
* @context {any} May be called in and from all contexts.
* @return {any} Don't rely on it.
*/
panel: function() {
mw.loader.using('ext.gadget.libJQuery', dwn._panel);
},
/**
* Shows a navigation panel consisting of numbers and a link
* that opens the configuration
*
* @context {Object} May be called in and from all contexts.
* @return {any} Don't rely on it. It's a UI method.
*/
_panel: function() {
if (dwn.$panel) dwn.$panel.remove();
var $panel = dwn.$panel = $('<div class="wln-panel-wrap"></div>'),
$messagePanel = $('<div class="wln-msg-panel"></div>').appendTo($panel),
$prefLink = dwn.$prefLink = $('<a>').text(_msg('cfg')).attr({
href: '#wln_prefs',
'class': 'ui-state-default wln-cfg-link'
}).appendTo($panel);
$.createIcon('ui-icon-gear').prependTo($prefLink);
// There is no $.Widget.prototype._focusable() yet
dwn.$prefLink._jqInteraction().click(dwn.configScreen);
dwn.$notice.css('position', 'relative').append($panel).hover(function() {
$panel.stop(true).fadeTo('fast', 1);
dwn._rotationTimeout(2);
}, function() {
$panel.stop(true).delay(500).fadeTo(1000, 0);
dwn._rotationTimeout(0.5);
});
dwn.$msgs.each(function(i, el) {
dwn._$msgLink(i, $(el)).appendTo($messagePanel);
});
if (dwn.config.cycle) dwn.startRotation();
},
/**
* A callback passed to a jQuery observer which is bound to the
* numbers on the navigation panel. Extracts the ordinal number of
* the message to got to from the href attribute
* and calls goToMessage with that number
*
* @example
* $('#myNumber').click(dwn._goToMessage);
*
* @param e {Object} A jQuery Event object.
* @context {Object} Instance of jQuery.
* The link must contain the ordinal number of the message in the following format: #wln0000
* where 0000 is a number of any digit length.
* @return {undefined}
*/
_goToMessage: function(e) {
if (e && $.isFunction(e.preventDefault)) e.preventDefault();
var m = $(this).attr('href').match(/\#wln(\d+)/);
dwn.goToMessage(Number(m[1]), 50);
},
/**
* Shows a specific message, identified by an ordinal number at the notice section
*
* @example
* dwn.goToMessage(1);
*
* @param i {number} Message number (starting with 0)
* @param duration {number} Transition duration in ms
* @context {any} May be called in and from all contexts.
* @return {any} Don't rely on it.
*/
goToMessage: function(i, duration) {
dwn.lastShownNote = i;
var _onready2Show = function() {
dwn.$lastShownNote = dwn.$msgs.eq(i);
dwn.$lastShownNote.fadeIn(duration, function() {
dwn.$lastShownNote.data('$b').addClass('ui-state-highlight');
});
if (!dwn.config.fade) {
dwn.$noticeInner.clearQueue().animate({ scrollTop: dwn.$lastShownNote.position().top }, duration);
}
dwn._rotationTimeout();
};
if (dwn.$lastShownNote && dwn.$lastShownNote.filter(':visible').length) {
dwn.$lastShownNote.data('$b').removeClass('ui-state-highlight');
if (dwn.config.fade) {
dwn.$lastShownNote.fadeOut(duration, _onready2Show);
} else {
_onready2Show();
}
} else {
_onready2Show();
}
},
/**
* Kicks-on cycling through all messages
*
* @example
* dwn.startRotation();
*
* @context {any} May be called in and from all contexts.
* @return {any} Don't rely on it.
*/
startRotation: function() {
// No need to rotate over just one message
if (dwn.$msgs.length <= 1) {
dwn.$msgs.eq(0).show();
return;
}
// Hide all notices
if (dwn.config.fade) dwn.$msgs.hide();
// A randam message
dwn.goToMessage(Math.round(Math.random() * (dwn.$msgs.length - 1)));
},
/**
* Wrangles with numbers to find the next message in the list of messages
* After it found the correct message to show, it calls goToMessage with its findings
*
* @example
* dwn.rotation();
*
* @param duration {number} Transition duration in ms
* @context {any} May be called in and from all contexts.
* @return {any} Don't rely on it.
*/
rotation: function() {
if (dwn.$msgs.length <= 1 || !dwn.config.cycle) return;
dwn.lastShownNote++;
if (dwn.lastShownNote >= dwn.$msgs.length) dwn.lastShownNote = 0;
dwn.goToMessage(dwn.lastShownNote);
},
/**
* Removes a messages from the list of those
* (Happens when user clicks hide/mark as read)
*
* @example
* dwn.removeMessage();
*
* @param $msg {object} instance of jQuery containing a message node
* @context {any} May be called in and from all contexts.
* @return {any} Don't rely on it.
*/
removeMessage: function($msg) {
dwn.$msgs = dwn.$msgs.not($msg.remove());
dwn.checkIfEmpty();
dwn.panel();
},
/**
* Bunch of functions that determine whether a message
* is addressed to the current user
*
* @example
* dwn.isTarget.geo(data);
*
* @param {Object} d A data object. Data is read from the HTML-DOM. The object given will
* be modified by this script (e.g. `d.countries` converted from string to array).
* @param {string} [d.countries]
* @return {boolean} true, if the message is possibly for the user, false if it is for sure not
*/
isTarget: {
geo: function(d) {
var found = false;
// The object is always set (if the server could not derive geo data from the
// IP address, it is set to an empty object). But this covers the case where
// the request might have failed so the variable would be undefined.
// Below each property access to Geo must take into account that the value may
// be undefined. Comparison like `x === Geo.city` can be done straight up,
// but `Geo.country.toUpperCase` will throw an exception since undefined has
// no method #toUpperCase (only string has).
if (!window.Geo) {
return true;
}
if (d.countries) {
d.countries = d.countries.split(':');
if (Geo.country) {
$.each(d.countries, function(i, c) {
c = $.trim(c.toUpperCase());
if (c === Geo.country.toUpperCase()) {
found = true;
return false;
}
});
if (!found) return false;
}
}
if (d.cities) {
found = false;
d.cities = d.cities.split(':');
$.each(d.cities, function(i, c) {
c = $.trim(c);
if (c === Geo.city) {
found = true;
return false;
}
});
if (!found) return false;
}
if (d.lonFrom && d.lonTo) {
var lon = parseFloat(Geo.lon, 10),
lonFrom = parseFloat(d.lonFrom, 10),
lonTo = parseFloat(d.lonTo, 10),
lonDiff = lonTo - lonFrom;
if (lonDiff < 0) {
// We crossed the International Date Line
if (lonTo > lon || lonFrom > lon) return false;
} else {
if (lonTo < lon || lonFrom > lon) return false;
}
}
if (d.latFrom && d.latTo) {
var lat = parseFloat(Geo.lat, 10),
latFrom = parseFloat(d.latFrom, 10),
latTo = parseFloat(d.latTo, 10);
if (latFrom < lat || latTo > lat) return false;
}
return true;
},
pref: function(d) {
var opt = d.preferences,
cfg = dwn.config,
t = d.type,
found = false;
if (opt) {
opt = opt.split(';');
$.each(opt, function(i, optval) {
var k, v,
m = optval.split('!=');
if (m && m.length === 2) {
k = m[0]; v = m[1];
if ($.trim(v) !== $.trim((mw.user.options.get(k) + ''))) {
found = true;
// break the .each loop
return false;
}
} else if ((m = optval.split('=')) && m.length === 2) {
k = m[0]; v = m[1];
if ($.trim(v) === $.trim((mw.user.options.get(k) + ''))) {
found = true;
// break the .each loop
return false;
}
}
});
} else {
found = true;
}
if (t && cfg && cfg.types) {
found = found && !cfg.types[t];
}
return found;
},
browser: function(d) {
var clnt = $.client.profile(),
clntName = clnt.name.toLowerCase(),
bfound = false,
ok = true;
if (d.browser) {
var bs = d.browser.split(':');
$.each(bs, function(i, b) {
if ($.trim(b.toLowerCase()) === clntName) {
bfound = true;
return false;
}
});
ok = ok && bfound;
}
if (d.browserVerMin) ok = ok && (parseFloat(clnt.version, 10) >= parseFloat(d.browserVerMin, 10));
if (d.browserVerMax) ok = ok && (parseFloat(clnt.version, 10) <= parseFloat(d.browserVerMax, 10));
if (d.browserLang) {
var langs = ['', d.browserLang.toLowerCase(), ''].join(':'),
clntLang = dwn.isTarget.getBrowserLanguage().toLowerCase(),
clntLangFull = ['', clntLang, ''].join(':'),
clntLangBase = ['', clntLangBase.split('-')[0], ''].join(':');
ok = ok && (langs.indexOf(clntLangFull) >= 0 || langs.indexOf(clntLangBase) >= 0);
}
return ok;
},
getBrowserLanguage: function() {
return navigator.userLanguage || navigator.language || navigator.browserLanguage;
},
other: function(d) {
// Dummy function; will be called during verification
// Required because not all data is used for determining whether the message
// should be shown
var m = d.until.match(/\d+/g);
if (m) {
m[1]--;
d.until = new Date(Date.UTC.apply(Date, m));
d.until.setDate(d.until.getDate()+2);
} else {
d.until = null;
}
return true;
}
},
/**
* Collection of data that will be read by the following method
*
* @example
* var data = dwn.readData();
*
* @param $msg {object} instance of jQuery containing a message node
* @context {any} May be called in and from all contexts.
* @return {any} Don't rely on it.
*/
data: {
geo: ['countries', 'cities', 'latFrom', 'latTo', 'lonFrom', 'lonTo'],
pref: ['preferences', 'type'],
browser: ['browser', 'browserVerMin', 'browserVerMax', 'browserLang'],
other: ['until']
},
readData: function($msg) {
var ret = {};
$.each(dwn.data, function(k, arr) {
var dm = ret[k] = {};
$.each(arr, function(i, dataKey) {
dm[dataKey] = $.trim($msg.find('.wln_' + dataKey).text());
});
});
return ret;
},
// Removes expired keys
tidyDismissKeys: function() {
var d = new Date();
$.each(dwn.dkv, function(k, v) {
if (!v || new Date(v) < d) {
delete dwn.dkv[k];
}
});
},
/**
* Whether one message was hidden will be stored here
* Old browsers do not support DOM storage, we'll use bad
* old cookies in this case. More about cookies below
* in function install.
*
* Also, an async process saves this setting to the user options
*
* @example
* var info = dwn.store.load('myKey');
*
* @param key {string} a key for which
* a lookup in the store will be performed
* @context {any} May be called in and from all contexts.
* @return {any} The value found in the storage key.
*/
store: {
load: function(key) {
if (!this.backend) this.init();
var ret;
if (dwn.dkv[key]) return 1;
if ('cookie' === this.backend) {
ret = mw.cookie.get(key);
} else {
var stored = this.readWithTTL()[key];
ret = stored ? stored.val : undefined;
}
if (ret) this.makePersistent(key, ret, dwn.storageTTL);
return ret;
},
readWithTTL: function() {
var stored = mw.storage.getObject(this.storageKey);
if (!stored) {
return {};
}
var now = Date.now();
var out = {};
for (var key in stored) {
if (stored[key].expires > now) {
out[key] = stored[key];
}
}
mw.storage.setObject(this.storageKey, out);
return out;
},
save: function(key, val, TTL) {
if (!this.backend) this.init();
if ('cookie' === this.backend) {
mw.cookie.set(key, val, {
expires: TTL || dwn.storageTTL, // expires in 7 days
path: '/' // entire commonswiki
});
} else {
var stored = this.readWithTTL();
stored[key] = {val: val, expires: Date.now() + ((TTL || dwn.storageTTL) * 86400000)}; // 1000*60*60*24
mw.storage.setObject(this.storageKey, stored);
}
this.makePersistent(key, val, TTL);
},
makePersistent: function(key, val, TTL) {
// WARNING: Val(ue) is ignored!
var d = new Date();
dwn.tidyDismissKeys();
d.setDate(d.getDate() + TTL);
dwn.dkv[key] = d;
if (this.to) clearTimeout(this.to);
this.to = setTimeout(function() {
mw.libs.settingsManager.switchGadgetPref(dwn.dismissKey, dwn.dkv);
}, 5000);
},
init: function() {
this.backend = "localStorage";
this.storageKey = "mwgadget-watchlistnotice";
}
},
/**
* Reads the translation from the HTML-DOM and overwrites the English default
* translation
*
* @example
* dwn.readTranslation();
*
* @context {any} May be called in and from all contexts.
* @return {undefined}
*/
readTranslation: function() {
var $i18n = $('#wln-translation'),
i18nNew = {};
$.each(i18n, function(k) {
var t = $i18n.find('.' + k).text();
if (t) {
i18nNew[k] = t;
}
});
mw.messages.set(i18nNew);
},
/**
* Removes hidden messages from the HTML-DOM and updates
* the message list
*
* @example
* dwn.updateMessageList();
*
* @context {any} May be called in and from all contexts.
* @return {any} Don't rely on it.
*/
updateMessageList: function() {
// We want only messages that are not hidden
dwn.$msgs.not(':visible').remove();
dwn.$msgs = dwn.$msgs.filter(':visible');
},
/**
* Checks whether the message list is empty.
* If so adds a message that there are no messages
* or - dependent on the user's preferences - folds
* the notice section
*
* @example
* dwn.checkIfEmpty();
*
* @context {any} May be called in and from all contexts.
* @return {any} Don't rely on it.
*/
checkIfEmpty: function() {
if (0 === dwn.$msgs.length) {
if (dwn.config.collapse && '#noticenohide' !== location.hash) {
return dwn.$notice.slideUp('fast', function() {
dwn.$notice.remove();
});
}
$('<li class="wln-no-message"></li>').text(_msg('no-msg')).appendTo(dwn.$notice.find('ul'));
}
},
/**
* First checks how many messages are addressed to the user
* and how the user prefers to have them displayed and then
* calls the appropriate methods to build the UI
* This is the only (partially) remaining part of the old script
*
* @example
* dwn.install();
*
* @context {any} May be called in and from all contexts.
* @return {any} Don't rely on it.
*/
install: function() {
var originalHeight = dwn.$noticeInner.height(),
maxMessageHeight,
d = new Date();
dwn.$msgs.each(function(i, el) {
var $msg = $(el),
// mId = message id
mId = Number($msg.attr('class').replace(/.*cookie\-ID\_(\d*).*/ig, '$1')),
// hwm stands for "hide watchlist message" but Cookies are always sent to the server with *each* request
// and often upload bandwidth is very limited.
cKey = 'hwm-' + mId,
dismissed = dwn.store.load(cKey),
// ".*?" is the junk we want to remove
cleanCls = $msg.attr('class').replace(/.*?(watchlist\-message.+)$/, '$1'),
vis = $msg.filter(':visible'),
data = dwn.readData($msg),
TTL;
// Cleaning class attribute
$msg.attr('class', cleanCls);
// Check whether the message has been previously dismissed
if (0 === vis.length || dismissed) {
return $msg.hide();
}
var showMsg = true;
$.each(data, function(k, data) {
showMsg = showMsg && dwn.isTarget[k](data);
});
if (!showMsg) return $msg.hide();
// Ensure that the message is visible after cleaning up the class attribute
// We put it here for performace reasons
$msg.show();
// 1000 * 60 * 60 * 24
TTL = data.other.until ? ((data.other.until - d) / (86400000)) : dwn.storageTTL;
var $ButtonLink = $('<a>').attr({
href: '#hide',
title: _msg('mark-as-read-details', 1)
}).text(_msg('mark-as-read')).click(function(e) {
e.preventDefault();
dwn.store.save(cKey, 1, TTL);
dwn.removeMessage($msg);
});
var $markRead = $('<span class="wln-mark-as-read"></span>').append($('<span>').text('[')).append($ButtonLink).append($('<span>').text(']'));
$msg.append(' ', $markRead);
// Check whether a single message is bigger than the container
maxMessageHeight = $msg.height() + 7;
});
if (dwn.config.fade) {
// Only animate, if one message requires more space
if (maxMessageHeight > originalHeight) {
dwn.$noticeInner.animate({
height: maxMessageHeight
});
}
} else {
// per [[Special:Permalink/96962449]] (Help talk:Watchlist messages)
// let it extend so it fits the content
dwn.$noticeInner.css('min-height', dwn.$noticeInner.height());
dwn.$noticeInner.css('height', 'auto');
}
dwn.updateMessageList();
dwn.checkIfEmpty();
dwn.panel();
// Look for a hashlink, and if it is present, show the config screen - this is documented!
if ('#noticenohide' === location.hash) dwn.configScreen();
}
};
// Expose globally
mw.libs.dismissWatchlistNote = dwn;
mw.loader.using(['mediawiki.util', 'mediawiki.user', 'user.options', 'mediawiki.cookie', 'mediawiki.storage', 'ext.gadget.SettingsManager'], function() {
var fetch = mw.libs.settingsManager.fetchGadgetSetting;
fetch(dwn.pefKey, ['option']).done(function(prefName, settingValue) {
fetch(dwn.dismissKey, ['option']).done(function(dkn, dkv) {
dwn.config = $.extend(true, dwn.defaultconfig, settingValue);
dwn.dkv = dkv || {}; // Dismiss-key-value
dwn.readTranslation();
dwn.install();
});
});
});
})(mediaWiki, jQuery);
// </nowiki>