Templates Wiki
Advertisement

//

/*! 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

//
Advertisement