MediaWiki:Vector.js
Uwaga: aby zobaczyć zmiany po opublikowaniu, może zajść potrzeba wyczyszczenia pamięci podręcznej przeglądarki.
- Firefox / Safari: Przytrzymaj Shift podczas klikania Odśwież bieżącą stronę, lub naciśnij klawisze Ctrl+F5, lub Ctrl+R (⌘-R na komputerze Mac)
- Google Chrome: Naciśnij Ctrl-Shift-R (⌘-Shift-R na komputerze Mac)
- Internet Explorer / Edge: Przytrzymaj Ctrl, jednocześnie klikając Odśwież, lub naciśnij klawisze Ctrl+F5
- Opera: Naciśnij klawisze Ctrl+F5.
/* Umieszczony tutaj kod JavaScript zostanie załadowany wyłącznie przez użytkowników korzystających ze skórki Wektor */ /** Przypisy z dymkami */ /*! 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' }, be: { coreConfigureText: 'Налада усплываючых падказак', coreConfigureHover: 'Змяніць налады усплываючых падказак' }, 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' }, 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' }, ja: { coreConfigureText: '注釈ふきだしの設定', coreConfigureHover: '注釈ふきだしの設定を変更する' }, ko: { coreConfigureText: '주석 말풍선 설정', coreConfigureHover: '주석 말풍선 설정하기' }, nl: { coreConfigureText: 'Configureer Referentie Popups', coreConfigureHover: 'Wijzig de instellingen voor Referentie Popups' }, pl: { coreConfigureText: 'Skonfiguruj wyskakujące przypisy', coreConfigureHover: 'Zmień ustawienia dla wyskakujących przypisów' }, ru: { coreConfigureText: 'Настройка всплывающих подсказок', coreConfigureHover: 'Изменить настройки всплывающих подсказок' }, 'pt-br': { coreConfigureText: 'Configurar Popups de Referência', coreConfigureHover: 'Mudar configurações para Popups de Referência' }, uk: { coreConfigureText: 'Налаштування спливаючих підказок', coreConfigureHover: 'Змінити налаштування спливаючих підказок' }, vi: { coreConfigureText: 'Thiết Đặt Popup Tham Khảo', coreConfigureHover: 'Thay đổi thiết đặt cho Popup Tham Khảo' }, }; 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: 'u:dev:Colors/code.js' }, dataType: 'script', cache: true }); // Support CSS if (!module.cssLoaded) { window.importArticle({type: 'style', article: 'u: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. $('#articleCategories, #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