//
/*! Copyright (C) 2012 Lunarity
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*jshint browser:true jquery:true laxbreak:true smarttabs:true multistr:true trailing:true */
/*global mediaWiki */
// Standard cite:
// <sup class="reference"><a href="#cite_note_0">[1]</a></sup>
// WARN: Format can be altered by a MediaWiki message
// Global namespace
window.dev = window.dev || {};
/*global dev */
dev.ReferencePopups = dev.ReferencePopups || {};
// Debugging function
dev.ReferencePopups.unload = dev.ReferencePopups.unload || function () {
"use strict";
delete this.Popup;
delete this.configure;
if (this.unloadCore) {
this.unloadCore();
}
};
// i18n messages
(function ( module, lang ) {
/*jshint forin:false */
"use strict";
var i18n = {
en: {
coreConfigureText: 'Configure Reference Popups',
coreConfigureHover: 'Change settings for Reference Popups'
},
fr: {
coreConfigureText: 'Configurer Popups de Référence',
coreConfigureHover: 'Modifier les préférences pour les Popups de Référence'
},
it: {
coreConfigureText: 'Configura le note a popup',
coreConfigureHover: 'Cambia le impostazioni per le note a popup'
},
pl: {
coreConfigureText: 'Skonfiguruj wyskakujące przypisy',
coreConfigureHover: 'Zmień ustawienia dla wyskakujących przypisów'
},
'pt-br': {
coreConfigureText: 'Configurar Popups de Referência',
coreConfigureHover: 'Mudar configurações para Popups de Referência'
},
vi: {
coreConfigureText: 'Thiết đặt popup tham khảo',
coreConfigureHover: 'Thay đổi thiết đặt cho popup tham khảo'
},
ca: {
coreConfigureText: 'Configura referències emergents',
coreConfigureHover: 'Canviar la configuració de referències emergents'
},
es: {
coreConfigureText: 'Configurar popups de referencias',
coreConfigureHover: 'Modificar la configuración de los popups de referencias'
},
ko: {
coreConfigureText: '주석 말풍선 설정',
coreConfigureHover: '주석 말풍선 설정하기'
}
};
var msg = module.messages = module.messages || {};
for (var m in i18n.en) {
msg[m] = msg[m] || (i18n[lang] && i18n[lang][m]) || i18n.en[m];
}
})(dev.ReferencePopups, mediaWiki.config.get('wgUserLanguage'));
// The popup itself is separated from the core logic which makes it reusable.
// Load dependencies
(function ( window, $, mw ) {
"use strict";
// Double runs
var module = window.dev.ReferencePopups;
if (module.Popup) {
return $.noop;
}
// Deps
var mwReady = $.Deferred(),
mwDeps = ['jquery.ui.position', 'jquery.effects.fold', 'jquery.ui.core', 'jquery.ui.widget'];
mw.loader.load(mwDeps, null, true);
mw.loader.using(mwDeps, mwReady.resolve, mwReady.reject);
var colors = window.dev.colors || $.ajax({
url: mw.config.get('wgLoadScript'),
data: {
mode: 'articles',
only: 'scripts',
articles: 'w:dev:Colors/code.js'
},
dataType: 'script',
cache: true
});
// Support CSS
if (!module.cssLoaded) {
window.importArticle({type: 'style', article: 'w:dev:ReferencePopups/code.css'});
module.cssLoaded = true;
}
var dfd = $.Deferred();
module.Popup = dfd.promise();
return function ( callback ) {
$.when(mwReady, colors).done(function () {
dfd.resolve(module.Popup = callback(module, window, $, mw, window.dev.colors));
}).fail(function () {
delete module.Popup;
dfd.reject();
});
};
}(window, jQuery, mediaWiki))(function ( module, window, $, mw, Colors ) {
"use strict";
// Wikia are using jQuery 1.8 which removed the deprecated $.curCSS...
// but jQuery UI 1.8 needs it and they didn't update THAT to 1.9...
$.curCSS = $.curCSS || $.css;
// Custom CSS. Try to make the popup fit into the skin by adapting to the color scheme
(function ( color, Colors, mw, $ ) {
// Colors interfacing. It doesn't handle various common values.
function ifOk ( val, alt ) {
// Colors chokes on transparent
return (val && val !== 'transparent' && val !== 'rgba(0, 0, 0, 0)') ? val : alt;
}
function tryParse ( val, alt ) {
try {
return Colors.parse(val);
} catch (e) {
if (window.console) {
window.console.warn('Colors Parse Error (' + val + '): ', e, e.stack);
}
return Colors.parse(alt);
}
}
function toRgba ( color, alpha ) {
return 'rgba(' + color.red() + ',' + color.green() + ',' + color.blue() + ',' + alpha + ')';
}
if (mw.config.get('skin') === 'oasis') {
color.page = Colors.parse(Colors.wikia.page);
color.pageBorder = Colors.parse(Colors.wikia.border);
color.accent = Colors.parse(Colors.wikia.menu);
color.popText = Colors.wikia.text;
} else {
var $content = $('#content');
color.page = tryParse(ifOk($('#globalWrapper').css('backgroundColor'), $content.css('backgroundColor')), 'white');
color.pageBorder = tryParse(ifOk($content.css('borderLeftColor'), $content.css('borderRightColor')), '#AAA');
color.accent = tryParse(ifOk($('#footer').css('borderTopColor'), $('body').css('backgroundColor')), '#fabd23');
color.popText = tryParse($('#mw-content-text').css('color'), 'black');
}
// Calculate color variants, we want the popup to stand out from the page slightly
// I do that by calculating a module color like the rail modules.
var mixCol;
if (color.page.isBright()) { // Darken, desaturate and blue shift
color.back = color.page.mix(mixCol = 'black', 95).mix('blue', 97);
} else { // Lighten, desaturate
color.back = color.page.mix(mixCol = 'white', 95);
}
color.popEdge = color.accent.mix(color.pageBorder, 90);
// Background gradient colors, faint glass-like effect
color.backA = toRgba(color.back, 0.95);
color.backB = toRgba(color.back.mix(mixCol, 99), 0.95);
color.backC = toRgba(color.back.mix(mixCol, 97), 0.95);
// Style sheet for the popup itself
Colors.css('\
.refpopups-box {\
background-color: $back;\
color: $popText;\
border-color: $popEdge;\
background: -webkit-linear-gradient(30deg, $backA, $backB 15%, $backB 25%, $backA 40%, $backA 50%, $backB 60%, $backB 70%, $backC 70%, $backC 90%, $backA 92%);\
background: linear-gradient(60deg, $backA, $backB 15%, $backB 25%, $backA 40%, $backA 50%, $backB 60%, $backB 70%, $backC 70%, $backC 90%, $backA 92%);\
}\
.refpopups-chevron-in {\
border-top-color: $back;\
border-top-color: $backA;\
}\
.refpopups-flipped > .refpopups-chevron-in {\
border-bottom-color: $back;\
border-bottom-color: $backA;\
}\
.refpopups-chevron-out {\
border-top-color: $popEdge;\
}\
.refpopups-flipped > .refpopups-chevron-out {\
border-bottom-color: $popEdge;\
}', color);
})(module.colors = module.colors || {}, Colors, mw, $);
// The reference popup itself. [Requires UI 1.8, tries to use parts of 1.9]
// It's implemented as jQuery UI widget for structural and convenience reasons, but this
// isn't really intended to be used by 3rd party code (although it could be)
var widgetId = 0;
$.widget('Lunarity.referencePopup', {
options: {// Default option configuration
activateBy: 'hover',
hoverDelay: 200,
content: '', // primary content
disabled: false, // Disables all events and hides if currently visible
visible: false,
animation: 'fold', // Values: false, or string name
animSpeed: 300,
context: null, // context node to attach the popup to (#mw-content-text) [Default: body, watch the z-index]
escapeCloses: true,
extraClass: '', // Extra CSS classes for repurposing the popups (e.g. width control)
contentBoxClass: '', // Classes applied to the content box (WikiaArticle)
stickyHover: false, // Ignore mouseleave from the element we are attached to.
open: null, // open/close event callbacks (optional)
close: null
},
// Constructor, no parameters but the options member is configured.
_create: function () {
// Requires jQuery UI 1.9 to set this automatically
if (!this.document) {
this.document = $(this.element[0].ownerDocument);
this.window = $(this.document[0].defaultView || this.document[0].parentWindow);
this.eventNamespace = '.' + this.widgetBaseClass + widgetId++;
}
this._timeouts = {};
// The outer box is unstyled which is necessary to avoid borders screwing us with
// offsetParent calculations for the chevrons. (top/left relative to padding-box, offset
// is border-box)
this._$popup = $(
'<div class="refpopups-popup" id="' + this.eventNamespace.substr(1) + '">' +
'<div class="refpopups-chevron-out"></div>' +
'<div class="refpopups-chevron-in"></div>' +
'<div class="refpopups-box">' +
'</div></div>')
.addClass(this.options.extraClass)
.find('> .refpopups-box').addClass(this.options.contentBoxClass).end();
this.options.context = $(this.document.find(this.options.context)[0]);
if (!this.options.context.length) {
this.options.context = $(this.document[0].body);
}
// Sanitise and call _installEvents
this._setOption('activateBy', this.options.activateBy === 'click' ? 'click' : 'hover');
this._updateContent();
if (!this.options.disabled) {
this.options.disabled = true;
this.enable();
}
if (this.options.visible) {
this.options.visible = false;
this.show();
}
},
_clearTimeout: function ( name ) {
/*jshint eqnull:true */
// !=/== null will match x === null || x === undefined
if (this._timeouts[name] !== null) {
window.clearTimeout(this._timeouts[name]);
this._timeouts[name] = null;
}
},
_setTimeout: function ( name, callback, delay ) {
this._clearTimeout(name);
var me = this;
this._timeouts[name] = window.setTimeout(function () {
me._timeouts[name] = null;
callback.call(me);
}, delay || 0);
},
// Installs/updates/replaces events on the various elements
// This should be safe to call at any time although I suspect there may be
// uncloseable and random closing glitches if the popup is visible.
_installEvents: function () {
var self = this, map, ns = this.eventNamespace;
this._$popup.off(ns);
this.element.off(ns);
this._clearTimeout('open');
this._clearTimeout('close');
// If we're disabled then just kill all the events and stop
if (this.options.disabled) {
return;
}
// Install auto hide when mouseout
if (this.options.activateBy === 'hover') {
// NOTE: This is somewhat fiddly as I need to sync leaving the popup
// and entering the base element to avoid glitching.
map = {};
map['mouseenter' + ns] = function () {
self._clearTimeout('close');
};
map['mouseleave' + ns] = function ( ev ) {
self._setTimeout('close', function () {
this.hide(ev);
}, self.options.hoverDelay);
};
this._$popup.on(map);
// Install the event handler on the base element
map = {};
map['mouseenter' + ns] = function ( ev ) {
// Clear the close timeout if a close cycle is happening
self._clearTimeout('close');
self._setTimeout('open', function () {
this.show(ev);
}, self.options.hoverDelay);
};
map['mouseleave' + ns] = function ( ev ) {
self._clearTimeout('open');
// If sticky hovering is off then we need to arm the close
// timeout. Otherwise, we'll just stick open.
if (!self.options.stickyHover) {
self._setTimeout('close', function () {
this.hide(ev);
}, self.options.hoverDelay);
}
};
this.element.on(map);
}
// Click event is always installed in order to deal with touchscreens
// Touchscreens don't have hover, so when touching happens, we enable click
// handling even in hover mode
var lastTouch;
map = {};
map['touchEnd' + ns] = function ( ev ) {
// Fires when touch stops, always fired, even if finger wanders away
lastTouch = ev.originalEvent;
};
map['click' + ns] = function ( ev ) {
// On touch screens, there is no hover so we always process clicks
// defaultPrevented is IE9 or newer. I don't think IE8 has touch events anyway.
if ((self.options.activateBy !== 'click' && (!lastTouch || lastTouch.defaultPrevented)) || self.options.disabled) {
return;
}
lastTouch = null; // for click event generated by touchEnd
if (!self.options.visible) {
ev.preventDefault(); // Block normal interaction
self.show(ev);
} else {
// If it's already visible then we allow the click to pass through
self.hide(ev);
}
};
if (this.options.escapeCloses) {
var onKeyUp = function ( ev ) {
if (ev.which === $.ui.keyCode.ESCAPE) {
self.hide(ev);
}
};
map['keyup' + ns] = onKeyUp;
this._$popup.on('keyup' + ns, onKeyUp);
}
this.element.on(map);
map = null;
},
triggerHover: function ( event ) {
if (this.options.activateBy === 'hover') {
this._setTimeout('open', function () {
this.show(event);
}, this.options.hoverDelay);
}
},
destroy: function () {
this.hide();
this._$popup.remove();
this.element.off(this.eventNamespace); // FIXME: 1.9's destroy does this automatically
return $.Widget.prototype.destroy.call(this);
},
_updateContent: function ( val ) {
val = val || this.options.content || '';
// Canonicalise to either a DOM node or a jQuery
if (!val.jquery && !val.nodeType) {
// The rewrap $() is to discard the prevObject chain
this.options.content = val = $($(this.document[0].createElement('div')).html(val).contents().unwrap());
}
this._$popup.find('> .refpopups-box').empty().append(val);
},
// Adds/removes popup's id from ARIA describedby list for the element
_describeAria: function ( yes ) {
var k = 'aria-describedby',
val = this.element.attr(k),
s = val ? val.split(/\s+/g) : [],
id = this._$popup[0].id,
at = $.inArray(id, s);
if (at !== -1) {
if (!yes) {
s.splice(at, 1);
}
} else if (yes) {
s.push(id);
}
val = s.join(' ');
this.element[val ? 'attr' : 'removeAttr'](k, val);
},
show: function ( event ) {
if (this.options.visible || this.options.disabled) {
return;
}
// Popups are go
this.options.context.append(this._$popup.stop(true, true));
this._positionPopup();
this._describeAria(true);
if (this.options.animation) {
this._$popup.hide().show(this.options.animation, this.options.animSpeed);
}
// Attach the scroll tracking event to the window to keep the popup from leaving
// the visible area for as long as possible. Event handler is throttled for
// performance.
// WARN: This causes noticeable scroll lag when smooth scrolling in Firefox,
// throttling more helps but doesn't prevent it, and becomes annoying with the
// popup reacting noticeably late to the scroll.
var triggered = 0, self = this, ns = this.eventNamespace;
this.window.on('scroll' + ns + ' resize' + ns, function adaptCallback () {
if (++triggered !== 1) {
return;
}
self._positionPopup();
window.setTimeout(function () {
var t = triggered;
triggered = 0;
if (t > 1 && self.options.visible) {
adaptCallback();
}
}, 100);
});
// Install the click-out event to close the popup in click mode
if (this.options.activateBy === 'click') {
this.document.on('click' + ns, function ( ev ) {
// Since we have references, this is kind of awkward
if (!$(ev.target).closest(self._$popup.add(self.element)).length) {
self.hide();
}
});
}
this.options.visible = true;
this._trigger('open', event/*, arbitrary data object*/); // fires referencepopupopen
},
hide: function ( event ) {
if (!this.options.visible) {
return;
}
// Detach the global tracking events
// FIXME: 1.9 provides _on/_off which would make this much easier
this.document.off(this.eventNamespace);
this.window.off(this.eventNamespace);
// Hide the popup itself
if (this.options.animation) {
// The clone is a trick to avoid the completion callback from detaching
// the new popup. The callback fires asynchronously which means it can
// end up detaching after a new session starts despite the .stop()
// This also has the benefit that destroy() won't kill the animation.
var $clone = this._$popup.clone();
this._$popup.after($clone);
$clone.hide(this.options.animation, this.options.animSpeed, function () {
$clone.remove();
});
}
// NOTE: Detaching is actually faster than hiding (display:none) surprisingly
this._$popup.detach();
this._describeAria(false);
this.options.visible = false;
this._trigger('close', event);
},
toggle: function ( yesno ) {
yesno = (yesno !== void 0 ? yesno : !this.options.visible);
this[yesno ? 'show' : 'hide']();
},
_setOption: function ( key, val ) {
switch (key) {
case 'disabled':
if (val && this.options.visible) {
this.hide();
}
this.options.disabled = !!val;
this._installEvents();
return; // Don't add disabled attributes and classes
case 'visible':
this[val ? 'show' : 'hide']();
return;
case 'activateBy':
if (({hover: 1, click: 1})[val] === 1) {
this.options.activateBy = val;
this._installEvents();
}
return;
case 'content':
this.options.content = val;
this._updateContent();
return;
case 'context': // This is construction only.
return;
case 'extraClass':
this._$popup[0].className = 'refpopups-popup';
this._$popup.addClass(val);
break;
case 'extraBoxClass':
this._$popup.find('> .refpopups-box').prop('className', 'refpopups-box').addClass(val);
break;
}
return $.Widget.prototype._setOption.call(this, key, val);
},
_positionPopup: function () {
var $popup = this._$popup.removeClass('refpopups-flipped'),
$this = this.element,
$chevOut = $popup.find('> .refpopups-chevron-out'),
chevOutHeight = 0;
// If custom CSS has hidden the chevrons then the result of this will be bogus
if ($chevOut.is(':visible')) {
// This calculation is trying to deal with the fact that the chevron overlaps the
// popup's border so is not entirely outside of it. [Calculation is only valid when
// unflipped, we're assuming that the chevron is adjacent and the same size both ways]
chevOutHeight = $chevOut.position().top + $chevOut.outerHeight() - $popup.outerHeight();
}
// Position the popup slightly above the reference element, leaving space
// for the chevrons underneath. This may not fit due to scrollTop.
$popup.position({
my: 'bottom',
at: 'top',
of: $this,
collision: 'fit none',
offset: '0 -' + chevOutHeight // FIXME: DEPRECATED in 1.9, but 1.8 doesn't support offsets
});
// Check for not fitting, if it doesn't fit then we need to manually flip it.
// We need to do this the hard way because flip seems to be broken in 1.8
// jQuery UI 1.9's using function may allow me to not have to do this manually but
// that's not likely to be available in MW for a while.
var spaceAtTop = $this.offset().top,
spaceAtBottom = spaceAtTop + $this[0].offsetHeight,
popupHeight = $popup[0].offsetHeight + chevOutHeight,
scroll = this.window.scrollTop();
spaceAtTop = spaceAtTop - scroll;
spaceAtBottom = (scroll + this.window.height()) - spaceAtBottom;
if (spaceAtTop < popupHeight && spaceAtTop < spaceAtBottom) {
$popup.addClass('refpopups-flipped').position({// Flip
my: 'top',
at: 'bottom',
of: $this,
collision: 'fit none',
offset: '0 ' + chevOutHeight
});
}
// If the chevrons are invisible then this calculation is pointless
if (!chevOutHeight) {
return;
}
// Now the pointless gimmickery, position the chevron so it points directly at the
// middle of the host element
var $chevIn = $popup.find('> .refpopups-chevron-in'),
myLoc = $this.offset().left - $popup.offset().left + $this.outerWidth() / 2;
$chevOut.css('left', (myLoc - $chevOut.outerWidth() / 2) + 'px');
$chevIn.css('left', (myLoc - $chevIn.outerWidth() / 2) + 'px');
}
});
return $.Lunarity.referencePopup;
}); // End UI Widget
/**************************************************************************************/
// Reference logic that uses the popup to actual show the things
// This is the core. (In MVC terms, this is the Controller. The popup is the View, and
// the citation nodes in the DOM are the Model)
(function ( window, $ ) {
"use strict";
// Check for loading in noCore mode or double runs and abort.
var module = window.dev.ReferencePopups;
if (module.unloadCore) {
return $.noop;
}
module.unloadCore = $.noop;
return function ( callback ) {
$.when(module.Popup).done(function () {
callback(module, window, $, window.mediaWiki);
}).fail(function () {
delete module.unloadCore;
});
};
}(window, jQuery))(function ( module, window, $, mw ) {
"use strict";
// Local Storage wrapper. I don't know when $.storage becomes available as it isn't
// a standard ResourceLoader module. And I have an allergy to jquery.cookie
// NOTE: IE8 supports localStorage and we don't care about anything more crap than that
var store = {
get: function ( key ) {
try {
return JSON.parse(window.localStorage.getItem(key));
} catch (e) {
return null;
}
},
set: function ( key, data ) {
try {
window.localStorage.setItem(key, JSON.stringify(data));
} catch (e) {
}
}
};
// Configuration (Get settings from storage)
// TODO: We may want to introduce Preferences for global settings.
var userConfig = (function ( config, defaults ) {
if (typeof (config) !== 'object' || config === null) {
config = {};
}
// Must be in range or it becomes inoperable
if (({hover: 1, click: 1})[config.react] !== 1) {
config.react = 'hover';
}
config.hoverDelay = config.hoverDelay || defaults.hoverDelay || 200;
config.animate = (config.animate === void 0 ? defaults.animate === void 0 || defaults.animate : !!config.animate);
config.disabled = !!config.disabled && !module.lockdown;
config.stick = (config.stick === void 0 ? defaults.stick !== void 0 && defaults.stick : !!config.stick);
return config;
})(store.get('RefPopupsJS'), module.defaults || {});
// This code creates and destroys popups as the references are interacted with.
// I do it this way as it minimises memory usage by ensuring only one popup
// will exist at any time.
// Armed is the currently active popup instance, open is the currently OPEN
// popup instance. The difference only matters when hovering, the previously
// open one will be killed and replaced by the Armed instance when armed opens.
var armedPop = null, openPop = null;
function cyclePopup ( newTarget ) {
// Can't arm the open.
if (openPop && openPop.element.is(newTarget)) {
return;
}
if (armedPop) {
// Don't do anything if we are arming already armed
if (armedPop.element.is(newTarget)) {
return;
}
armedPop.destroy();
}
return (armedPop = constructPopup($(newTarget)));
}
function cleanupPopups () {
if (openPop) {
openPop.destroy();
}
if (armedPop) {
armedPop.destroy();
}
openPop = armedPop = null;
}
$(function ( $ ) {
// Click processing functions
var lastTouch;
$('#mw-content-text').on({
'touchEnd.RefPopups': function ( ev ) {
// Fires when touch stops, always fired, even if finger wanders away
lastTouch = ev.originalEvent;
},
'click.RefPopups': function ( ev ) {
// On touch screens, there is no hover so we always process clicks
if (((!lastTouch || lastTouch.defaultPrevented) && userConfig.react !== 'click') || userConfig.disabled) {
return;
}
lastTouch = null;
var pop = cyclePopup(this);
if (pop) {
// The popup missed the event so signal it manually
ev.preventDefault(); // Prevent link nav
pop.show(ev);
}
},
// Central state tracking, this is where the always-only-one happens
// This is important for hover to avoid insta-kills when brushing
// across references
'referencepopupopen.RefPopups': function () {
// Only react to popups created by us, not ones by other scripts
if (!armedPop || !armedPop.element.is(this)) {
return;
}
var oldOpen = openPop;
openPop = armedPop;
armedPop = null;
if (oldOpen) { // Kill existing so only one is open
// IMPORTANT: The destroy MAY trigger a close event which will
// invoke the handler below, that can cause TWO calls to
// destroy() which is bad. We switch the value of openPop
// BEFORE destroying to avoid that.
oldOpen.destroy();
}
},
'referencepopupclose.RefPopups': function () {
if (!openPop || !openPop.element.is(this)) {
return;
}
// If there is an armed popup then we just drop dead, otherwise
// we shift ourself from open to armed.
if (armedPop) {
openPop.destroy();
} else {
armedPop = openPop;
}
openPop = null;
}
}, '.reference');
// When the page is hidden (navigated away but held in cache) then we will
// close any open popups so that when the user hits back, they won't still
// be open.
$(window).on('pagehide.RefPopups', cleanupPopups);
// Install the hover events, if necessary.
applyHoverEvents();
});
// Installs hovering events if we're in hover mode, otherwise doesn't do anything
// It's called from the configuration save callback
function applyHoverEvents () {
var $article = $('#mw-content-text');
$article.off('mouseenter.RefPopups');
if (userConfig.disabled) {
return;
}
if (userConfig.react !== 'hover') {
if (openPop) {
openPop.option('activateBy', 'click');
}
if (armedPop) {
armedPop.option('activateBy', 'click');
}
return;
}
$article.on('mouseenter.RefPopups', '.reference', function () {
var pop = cyclePopup(this);
// Popup missed the hover, so cycle manually
if (pop) {
pop.triggerHover();
}
});
if (openPop) {
openPop.option('activateBy', 'hover');
}
if (armedPop) {
armedPop.option('activateBy', 'hover');
}
}
// Add configuration buttons to the interface.
// Lockdown prevents it at the admin's option (NOT RECOMMENDED)
if (!module.lockdown) {
$(function ( $ ) {
// We insert the configuration link below the categories, above article comments.
// It displays as a float right, cleared block.
$('#WikiaArticleCategories, #catlinks').first().after(
$('<a href="#configure-refpopups" class="refpopups-configure-page" />')
.html('[' + module.messages.coreConfigureText + ']')
.click(onClickConfigure)
.appendTo(this)
);
});
}
// Debugging function to shut everything down.
// Also functions as the double run protection
module.unloadCore = function () {
// Detach events
$('#mw-content-text').off('.RefPopups');
$(window).off('.RefPopups');
// Remove configuration buttons
$('a[href="#configure-refpopups"]').remove();
// Close currently open popups, if any
cleanupPopups();
// The core is unloaded now, it could be reinitialised
delete this.unloadCore;
};
$(window).trigger('dev-ReferencePopups-config', module.settings = $.extend({}, userConfig));
// Interfacing code to load and display the configuration interface.
// The interface is stored in a separate file to reduce the size.
var configureIsPending = false;
function onClickConfigure ( ev ) {
ev.preventDefault();
var closeFunc = function () {
configureIsPending = false;
}, interfaceFunc = function ( confFunc ) {
confFunc(userConfig, function ( newSettings ) {
store.set('RefPopupsJS', newSettings);
// We need to kill any active popups in order to apply the
// new settings to them. This is especially important for disabled
// since they'll be left open otherwise.
cleanupPopups();
applyHoverEvents();
// Signal the new configuration to anyone interested in it
$(window).trigger('dev-ReferencePopups-config', module.settings = $.extend({}, newSettings));
}, closeFunc);
};
// If a lazy-load is running then don't open a second time, it "works" but
// not properly due to duplicate #ids
if (configureIsPending) {
return;
}
configureIsPending = true;
// Already loaded is the best.
if (module.configure) {
// It may or may not be a promise.
$.when(module.configure).done(interfaceFunc).fail(closeFunc);
return;
}
// Do lazy load. This would be a hell of a lot easier if we had an explicit
// dependency system. Then I could just require() and wait for the promise.
$.ajax({
url: mw.config.get('wgLoadScript'),
data: {
mode: 'articles',
only: 'scripts',
articles: 'w:dev:ReferencePopups/code.configure.js'
},
dataType: 'script',
cache: true
}).then(function () {
// WARN: This only works with same origin because browsers suck.
// Cross-origin fires done immediately before the code runs.
// Chain promise
$.when(module.configure).done(interfaceFunc).fail(closeFunc);
}).fail(closeFunc);
}
function constructPopup ( $ref ) {
var frag = $ref.find('a[href^="#cite_note"]').first();
if (!frag.length) {
return null; // Failsafe for crap wiki configurations
}
// The first step is to determine the Element for the reference node
// NOTE: When a reference contains special HTML chars like '&', they get encoded
// to '.26', however the '.' marks the start of a class which is a problem...
// jQuery supports escaping the dot so we can work around this. There are other
// special characters but I think only dot will occur in cite's #ids.
var $cite = $(frag.attr('href').replace(/\./g, '\\.'));
if (!$cite.length) {
// This happens when the ref tag is of the "name=X" form, but X doesn't exist
return null;
}
// Create the content box. Sometimes people ram giant images in to the reference
// so we wrap it in a scrollable box to avoid spill.
var $content = $('<div style="overflow:auto">'),
// Configuration button
$conf = $('<a href="#" class="refpopups-configure" />')
.prop('title', module.messages.coreConfigureHover)
.click(onClickConfigure);
// We need to get just the reference body itself, without the backreference links
$content.append($cite.find('.reference-text').clone());
// And away we go
// NOTE: We're not using the $.fn.referencePopup wrapper as it's safer to stay
// in our own namespace.
return new module.Popup({
content: $conf.add($content),
activateBy: userConfig.react,
hoverDelay: userConfig.hoverDelay,
animation: userConfig.animate && 'fold',
stickyHover: userConfig.stick,
contentBoxClass: 'WikiaArticle'
// Disabled. Attach to body so on top of everything.
// NOTE: May cause CSS malfunctions in the popup due to not being descended
// from .WikiaPage.V2 or #WikiaMainContent
//context: '#WikiaMainContent, #mw-content-text'
}, $ref[0]);
}
}); // End Core
//