mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Popups
synced 2024-11-23 23:24:39 +00:00
Merge "Restructuring code"
This commit is contained in:
commit
fade5cb53a
|
@ -62,6 +62,9 @@ $wgHooks[ 'ResourceLoaderRegisterModules' ][] = function ( ResourceLoader &$reso
|
||||||
$resourceLoader->register( "ext.popups", array(
|
$resourceLoader->register( "ext.popups", array(
|
||||||
'scripts' => array(
|
'scripts' => array(
|
||||||
'resources/ext.popups.core.js',
|
'resources/ext.popups.core.js',
|
||||||
|
'resources/ext.popups.eventlogging.js',
|
||||||
|
'resources/ext.popups.renderer.js',
|
||||||
|
'resources/ext.popups.renderer.article.js',
|
||||||
),
|
),
|
||||||
'styles' => array(
|
'styles' => array(
|
||||||
'resources/ext.popups.core.less',
|
'resources/ext.popups.core.less',
|
||||||
|
|
|
@ -1,573 +1,154 @@
|
||||||
/* Code adapted from Yair Rand's NavPopupsRestyled.js
|
|
||||||
* https://en.wikipedia.org/wiki/User:Yair_rand/NavPopupsRestyled.js
|
|
||||||
*/
|
|
||||||
|
|
||||||
( function ( $, mw ) {
|
( function ( $, mw ) {
|
||||||
var closeTimer, // The timer use to delay `closeBox`
|
|
||||||
openTimer, // The timer used to delay sending the API request/opening the popup form cache
|
|
||||||
scrolled = false, // true if user scrolled the page but haven't moved mouse cursor
|
|
||||||
elTime, // EL: UNIX timestamp of when the popup was rendered
|
|
||||||
elDuration, // EL: How long was the popup open in milliseconds
|
|
||||||
elAction, // EL: Was the popup clicked or middle clicked or dismissed
|
|
||||||
elSessionId, // EL: Get defined after the getSessionId method is created
|
|
||||||
currentLink, // DOM element of the current anchor tag
|
|
||||||
cache = {},
|
|
||||||
curRequest, // Current API request
|
|
||||||
supportsSVG = document.implementation.hasFeature( 'http://www.w3.org/TR/SVG11/feature#Image', '1.1' ),
|
|
||||||
api = new mw.Api(),
|
|
||||||
SIZES = {
|
|
||||||
portraitImage: {
|
|
||||||
h: 250, // Exact height
|
|
||||||
w: 203 // Max width
|
|
||||||
},
|
|
||||||
landscapeImage: {
|
|
||||||
h: 200, // Max height
|
|
||||||
w: 300 // Exact Width
|
|
||||||
},
|
|
||||||
landscapePopupWidth: 450, // Exact width of a landscape popup
|
|
||||||
portraitPopupWidth: 300 // Exact width of a portrait popup
|
|
||||||
},
|
|
||||||
POPUP_DELAY = 150, // Time to wait in ms before showing a popup on hover
|
|
||||||
POPUP_CLOSE_DELAY = 100, // Time to wait in ms before closing a popup on de-hover
|
|
||||||
API_DELAY = 50, // Time to wait in ms before starting the API queries on hover, must be <= POPUP_DELAY
|
|
||||||
$svg, $box; // defined at the end of the file
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a promise corresponding to a `setTimeout()` call. Call `.abort()` on the return value
|
* @class mw.popups
|
||||||
* to perform the equivalent of `clearTimeout()`.
|
* @singleton
|
||||||
|
*/
|
||||||
|
mw.popups = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks SVG support on the browser
|
||||||
|
* @property {Boolean} supportsSVG
|
||||||
|
*/
|
||||||
|
mw.popups.supportsSVG = document.implementation.hasFeature( 'http://www.w3.org/TR/SVG11/feature#Image', '1.1' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The API object used for all this extension's requests
|
||||||
|
* @property {Object} api
|
||||||
|
*/
|
||||||
|
mw.popups.api = new mw.Api();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the page is being scrolled.
|
||||||
|
* @property {Boolean} scrolled
|
||||||
|
*/
|
||||||
|
mw.popups.scrolled = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of classes of which links are ignored
|
||||||
|
* @property {Array} IGNORE_CLASSES
|
||||||
|
*/
|
||||||
|
mw.popups.IGNORE_CLASSES = [
|
||||||
|
'.exitw',
|
||||||
|
'.image',
|
||||||
|
'.new',
|
||||||
|
'.internal'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If SVG is supported, creates the SVG mask used to create the
|
||||||
|
* the triangle pointer on popups with images
|
||||||
*
|
*
|
||||||
* @param {number} ms Milliseconds to wait
|
* @method createSVGMask
|
||||||
* @return {jQuery.Promise}
|
|
||||||
*/
|
*/
|
||||||
function timeoutPromise( ms ) {
|
mw.popups.createSVGMask = function () {
|
||||||
var deferred, promise, timeout;
|
if ( !mw.popups.supportsSVG ) {
|
||||||
|
|
||||||
deferred = $.Deferred();
|
|
||||||
|
|
||||||
timeout = setTimeout( function () {
|
|
||||||
deferred.resolve();
|
|
||||||
}, ms );
|
|
||||||
|
|
||||||
promise = deferred.promise( { abort: function () {
|
|
||||||
clearTimeout( timeout );
|
|
||||||
deferred.reject();
|
|
||||||
} } );
|
|
||||||
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @method sendRequest
|
|
||||||
* Send an API request, create DOM elements and
|
|
||||||
* put them in the cache. Returns a promise.
|
|
||||||
* @param {String} href
|
|
||||||
* @param {String} title
|
|
||||||
* @return {jQuery.Promise}
|
|
||||||
*/
|
|
||||||
function sendRequest( href, title ) {
|
|
||||||
var deferred = $.Deferred();
|
|
||||||
|
|
||||||
curRequest = api.get( {
|
|
||||||
action: 'query',
|
|
||||||
prop: 'extracts|pageimages|revisions|info',
|
|
||||||
redirects: 'true',
|
|
||||||
exintro: 'true',
|
|
||||||
exsentences: 2,
|
|
||||||
// there is an added geometric limit on .mwe-popups-extract
|
|
||||||
// so that text does not overflow from the card
|
|
||||||
explaintext: 'true',
|
|
||||||
piprop: 'thumbnail',
|
|
||||||
pithumbsize: 300,
|
|
||||||
rvprop: 'timestamp',
|
|
||||||
inprop: 'watched',
|
|
||||||
indexpageids: true,
|
|
||||||
titles: title
|
|
||||||
} );
|
|
||||||
|
|
||||||
curRequest.done( function ( re ) {
|
|
||||||
curRequest = undefined;
|
|
||||||
|
|
||||||
if ( re.query.pages[ re.query.pageids[ 0 ] ].extract === '' ){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var $a,
|
|
||||||
page = re.query.pages[ re.query.pageids[ 0 ] ],
|
|
||||||
$contentbox = $( '<div>' )
|
|
||||||
.addClass( 'mwe-popups-extract' )
|
|
||||||
.text( page.extract ),
|
|
||||||
thumbnail = page.thumbnail,
|
|
||||||
tall = thumbnail && thumbnail.height > thumbnail.width,
|
|
||||||
$thumbnail = createThumbnail( thumbnail, tall ),
|
|
||||||
timestamp = new Date( page.revisions[ 0 ].timestamp ),
|
|
||||||
timediff = new Date() - timestamp,
|
|
||||||
oneDay = 1000 * 60 * 60 * 24,
|
|
||||||
timestampclass = ( timediff < oneDay ) ?
|
|
||||||
'mwe-popups-timestamp-recent' :
|
|
||||||
'mwe-popups-timestamp-older',
|
|
||||||
$timestamp = $( '<div>' )
|
|
||||||
.addClass( timestampclass )
|
|
||||||
.append(
|
|
||||||
$( '<span>' ).text( mw.message( 'popups-last-edited',
|
|
||||||
moment( timestamp ).fromNow() ).text() )
|
|
||||||
);
|
|
||||||
|
|
||||||
$a = $( '<a>' )
|
|
||||||
.append( $thumbnail, $contentbox, $timestamp )
|
|
||||||
.attr( 'href', href )
|
|
||||||
.on( 'click', logClick );
|
|
||||||
|
|
||||||
cache[ href ] = {
|
|
||||||
box: $a,
|
|
||||||
thumbnail: thumbnail,
|
|
||||||
tall: tall
|
|
||||||
};
|
|
||||||
|
|
||||||
deferred.resolve();
|
|
||||||
} );
|
|
||||||
|
|
||||||
return deferred.promise();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @method createSVGTag
|
|
||||||
* Use createElementNS to create the svg:image tag as jQuery
|
|
||||||
* uses createElement instead. Some browsers map the `image` tag
|
|
||||||
* to `img` tag, thus an `svg:image` is required.
|
|
||||||
* @param {String} tag
|
|
||||||
* @return {Object}
|
|
||||||
*/
|
|
||||||
function createSVGTag( tag ) {
|
|
||||||
return document.createElementNS( 'http://www.w3.org/2000/svg', tag );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @method createThumbnail
|
|
||||||
* Returns a thumbnail object based on the ratio of the image
|
|
||||||
* Uses an SVG image where available to add the triangle/pokey
|
|
||||||
* mask on the image. Crops and resizes the SVG image so that
|
|
||||||
* is fits inside a rectangle of a particular size.
|
|
||||||
* @param {Object} thumbnail
|
|
||||||
* @param {boolean} tall
|
|
||||||
* @return {Object} jQuery DOM element of the thumbnail
|
|
||||||
*/
|
|
||||||
function createThumbnail( thumbnail, tall ) {
|
|
||||||
if ( !thumbnail ) {
|
|
||||||
return $( '<span>' );
|
|
||||||
}
|
|
||||||
|
|
||||||
var $thumbnailSVGImage, $thumbnail;
|
|
||||||
|
|
||||||
if ( tall ) {
|
|
||||||
if ( supportsSVG ) {
|
|
||||||
$thumbnailSVGImage = $( createSVGTag( 'image' ) );
|
|
||||||
$thumbnailSVGImage
|
|
||||||
.addClass( 'mwe-popups-is-not-tall' )
|
|
||||||
.attr( {
|
|
||||||
'xlink:href': thumbnail.source,
|
|
||||||
x: ( thumbnail.width > SIZES.portraitImage.w ) ?
|
|
||||||
( ( thumbnail.width - SIZES.portraitImage.w ) / -2 ) :
|
|
||||||
( SIZES.portraitImage.w - thumbnail.width ),
|
|
||||||
y: ( thumbnail.height > SIZES.portraitImage.h ) ?
|
|
||||||
( ( thumbnail.height - SIZES.portraitImage.h ) / -2 ) :
|
|
||||||
0,
|
|
||||||
width: thumbnail.width,
|
|
||||||
height: thumbnail.height
|
|
||||||
} );
|
|
||||||
|
|
||||||
$thumbnail = $( '<svg>' )
|
|
||||||
.attr( {
|
|
||||||
xmlns: 'http://www.w3.org/2000/svg',
|
|
||||||
width: SIZES.portraitImage.w,
|
|
||||||
height: SIZES.portraitImage.h
|
|
||||||
} )
|
|
||||||
.append( $thumbnailSVGImage );
|
|
||||||
} else {
|
|
||||||
$thumbnail = $( '<div>' )
|
|
||||||
.addClass( 'mwe-popups-is-tall' )
|
|
||||||
.css( 'background-image', 'url(' + thumbnail.source + ')' );
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ( supportsSVG ) {
|
|
||||||
$thumbnailSVGImage = $( createSVGTag( 'image' ) );
|
|
||||||
$thumbnailSVGImage
|
|
||||||
.addClass( 'mwe-popups-is-not-tall' )
|
|
||||||
.attr( {
|
|
||||||
'xlink:href': thumbnail.source,
|
|
||||||
'clip-path': 'url(#mwe-popups-mask)',
|
|
||||||
x: 0,
|
|
||||||
y: ( thumbnail.height > SIZES.landscapeImage.h ) ?
|
|
||||||
( ( thumbnail.height - SIZES.landscapeImage.h ) / -2 ) :
|
|
||||||
0,
|
|
||||||
width: thumbnail.width,
|
|
||||||
height: thumbnail.height
|
|
||||||
} );
|
|
||||||
|
|
||||||
$thumbnail = $( '<svg>' )
|
|
||||||
.attr( {
|
|
||||||
xmlns: 'http://www.w3.org/2000/svg',
|
|
||||||
width: SIZES.landscapeImage.w + 3,
|
|
||||||
height: ( thumbnail.height > SIZES.landscapeImage.h ) ?
|
|
||||||
SIZES.landscapeImage.h :
|
|
||||||
thumbnail.height
|
|
||||||
} )
|
|
||||||
.append( $thumbnailSVGImage );
|
|
||||||
} else {
|
|
||||||
$thumbnail = $( '<div>' )
|
|
||||||
.addClass( 'mwe-popups-is-not-tall' )
|
|
||||||
.css( 'background-image', 'url(' + thumbnail.source + ')' );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $thumbnail;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @method createBox
|
|
||||||
* Looks into the `cache` and uses the href to render the popup
|
|
||||||
* and offsets it to the link. Rebinds the `mouseleave` event
|
|
||||||
* for the anchor element to `leaveActive`.
|
|
||||||
* @param {String} href
|
|
||||||
* @param {Object} $el
|
|
||||||
* @param {Object} event
|
|
||||||
*/
|
|
||||||
function createBox( href, $el, event ) {
|
|
||||||
var
|
|
||||||
bar = cache[ href ],
|
|
||||||
offsetTop = ( event.pageY ) ?
|
|
||||||
event.pageY + 20 :
|
|
||||||
$el.offset().top + $el.height() + 9,
|
|
||||||
offsetLeft = ( event.pageX ) ?
|
|
||||||
event.pageX :
|
|
||||||
$el.offset().left,
|
|
||||||
flipped = false;
|
|
||||||
|
|
||||||
elTime = mw.now();
|
|
||||||
elAction = 'dismissed';
|
|
||||||
|
|
||||||
if ( bar.thumbnail === undefined ) {
|
|
||||||
bar.thumbnail = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( bar.tall === undefined ) {
|
|
||||||
bar.tall = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( offsetLeft > ( $( window ).width() / 2 ) ) {
|
|
||||||
offsetLeft += ( !event.pageX ) ? $el.width() : 0;
|
|
||||||
offsetLeft -= ( !bar.tall ) ? SIZES.portraitPopupWidth : SIZES.landscapePopupWidth;
|
|
||||||
flipped = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( event.pageX ) {
|
|
||||||
offsetLeft += ( flipped ) ? 20 : -20; // compensating the position of the triangle
|
|
||||||
}
|
|
||||||
|
|
||||||
$box
|
|
||||||
.children()
|
|
||||||
.detach()
|
|
||||||
// avoid .empty() to keep event handlers
|
|
||||||
.end()
|
|
||||||
.removeClass( 'mwe-popups-is-tall mwe-popups-is-not-tall mwe-popups-no-image-tri mwe-popups-image-tri flipped' )
|
|
||||||
.toggleClass( 'flipped', flipped )
|
|
||||||
// Add border triangle if there is no image or its landscape
|
|
||||||
.toggleClass( 'mwe-popups-no-image-tri', ( !bar.thumbnail || bar.tall ) )
|
|
||||||
// If theres an image and the popup is portrait do the SVG stuff
|
|
||||||
.toggleClass( 'mwe-popups-image-tri', ( bar.thumbnail && !bar.tall ) )
|
|
||||||
.addClass( bar.tall ? 'mwe-popups-is-tall' : 'mwe-popups-is-not-tall' )
|
|
||||||
.css( {
|
|
||||||
top: offsetTop,
|
|
||||||
left: offsetLeft
|
|
||||||
} )
|
|
||||||
.append( bar.box )
|
|
||||||
.attr( 'aria-hidden', 'false' )
|
|
||||||
.show()
|
|
||||||
.removeClass( 'mwe-popups-fade-out mwe-popups-fade-in' )
|
|
||||||
.addClass( 'mwe-popups-fade-in' )
|
|
||||||
// Hack to 'refresh' the SVG and thus display it
|
|
||||||
// Elements get added to the DOM and not to the screen because of different namespaces of HTML and SVG
|
|
||||||
// More information and workarounds here - http://stackoverflow.com/a/13654655/366138
|
|
||||||
.html( $box.html() );
|
|
||||||
|
|
||||||
if ( flipped && bar.thumbnail ) {
|
|
||||||
if ( !bar.tall ) {
|
|
||||||
$box.find( 'image' )[ 0 ].setAttribute( 'clip-path', 'url(#mwe-popups-mask-flip)' );
|
|
||||||
} else {
|
|
||||||
$box
|
|
||||||
.removeClass( 'mwe-popups-no-image-tri' )
|
|
||||||
.find( 'image' )[ 0 ].setAttribute( 'clip-path', 'url(#mwe-popups-landscape-mask)' );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$el
|
|
||||||
.off( 'mouseleave blur', leaveInactive )
|
|
||||||
.on( 'mouseleave blur', leaveActive );
|
|
||||||
|
|
||||||
$( document ).on( 'keydown', closeOnEsc );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @method closeOnEsc
|
|
||||||
* Use escape to close popup
|
|
||||||
*/
|
|
||||||
function closeOnEsc( e ) {
|
|
||||||
if ( e.keyCode === 27 ) {
|
|
||||||
closeBox();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @method leaveActive
|
|
||||||
* Closes the box after a delay
|
|
||||||
* Delay to give enough time for the user to move the pointer from
|
|
||||||
* the link to the popup box. Also avoids closing the popup by accident.
|
|
||||||
*/
|
|
||||||
function leaveActive() {
|
|
||||||
closeTimer = setTimeout( closeBox, POPUP_CLOSE_DELAY );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @method leaveInactive
|
|
||||||
* Unbinds events on the anchor tag and aborts AJAX request.
|
|
||||||
*/
|
|
||||||
function leaveInactive() {
|
|
||||||
$( currentLink ).off( 'mouseleave', leaveInactive );
|
|
||||||
if ( openTimer ) {
|
|
||||||
openTimer.abort();
|
|
||||||
}
|
|
||||||
if ( curRequest ) {
|
|
||||||
curRequest.abort();
|
|
||||||
}
|
|
||||||
currentLink = openTimer = curRequest = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @method closeBox
|
|
||||||
* Removes the hover class from the link and unbinds events
|
|
||||||
* Hides the popup, clears timers and sets it and the
|
|
||||||
* `currentLink` to undefined.
|
|
||||||
*/
|
|
||||||
function closeBox() {
|
|
||||||
elDuration = mw.now() - elTime;
|
|
||||||
|
|
||||||
$( currentLink ).removeClass( 'mwe-popups-anchor-hover' ).off( 'mouseleave', leaveActive );
|
|
||||||
|
|
||||||
$box
|
|
||||||
.removeClass( 'mwe-popups-fade-out mwe-popups-fade-in' )
|
|
||||||
.addClass( 'mwe-popups-fade-out' ); // Removed and added to trigger animation
|
|
||||||
|
|
||||||
setTimeout( function () {
|
|
||||||
if ( $box.hasClass( 'mwe-popups-fade-out' ) ) {
|
|
||||||
$box
|
|
||||||
.attr( 'aria-hidden', 'true' )
|
|
||||||
.hide()
|
|
||||||
.removeClass( 'mwe-popups-fade-out' );
|
|
||||||
}
|
|
||||||
}, 150 ); // Matches 0.15s in the .mwe-popups-fade-out class
|
|
||||||
|
|
||||||
if ( closeTimer ) {
|
|
||||||
clearTimeout( closeTimer );
|
|
||||||
}
|
|
||||||
|
|
||||||
$( document ).off( 'keydown', closeOnEsc );
|
|
||||||
|
|
||||||
logEvent();
|
|
||||||
currentLink = closeTimer = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @method logClick
|
|
||||||
* Logs different actions such as meta and shift click on the popup.
|
|
||||||
* Is bound to the `click` event.
|
|
||||||
* @param {Object} e
|
|
||||||
*/
|
|
||||||
function logClick( e ) {
|
|
||||||
if ( e.which === 2 ) { // middle click
|
|
||||||
elAction = 'opened in new tab';
|
|
||||||
} else if ( e.which === 1 ) {
|
|
||||||
if ( e.ctrlKey || e.metaKey ) {
|
|
||||||
elAction = 'opened in new tab';
|
|
||||||
} else if ( e.shiftKey ) {
|
|
||||||
elAction = 'opened in new window';
|
|
||||||
} else {
|
|
||||||
elAction = 'opened in same tab';
|
|
||||||
elDuration = mw.now() - elTime;
|
|
||||||
logEvent( this.href );
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @method logEvent
|
|
||||||
* Logs the Popup event as defined in the following schema -
|
|
||||||
* https://meta.wikimedia.org/wiki/Schema:Popups
|
|
||||||
* If `href` is passed it redirects to that location after the event is logged.
|
|
||||||
* @param {string} href
|
|
||||||
*/
|
|
||||||
function logEvent( href ) {
|
|
||||||
if ( typeof mw.eventLog !== 'function' ) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var dfd = $.Deferred(),
|
$( '<div>' )
|
||||||
event = {
|
.attr( 'id', 'mwe-popups-svg' )
|
||||||
duration: Math.round( elDuration ),
|
.appendTo( document.body )
|
||||||
action: elAction
|
.html(
|
||||||
};
|
'<svg width="0" height="0">' +
|
||||||
|
'<defs>' +
|
||||||
if ( elSessionId !== null ) {
|
'<clippath id="mwe-popups-mask">' +
|
||||||
event.sessionId = elSessionId;
|
'<polygon points="0 8, 10 8, 18 0, 26 8, 1000 8, 1000 1000, 0 1000"/>' +
|
||||||
}
|
'</clippath>' +
|
||||||
|
'<clippath id="mwe-popups-mask-flip">' +
|
||||||
if ( href ) {
|
'<polygon points="0 8, 274 8, 282 0, 290 8, 1000 8, 1000 1000, 0 1000"/>' +
|
||||||
dfd.always( function () {
|
'</clippath>' +
|
||||||
location.href = href;
|
'<clippath id="mwe-popups-landscape-mask">' +
|
||||||
} );
|
'<polygon points="0 8, 174 8, 182 0, 190 8, 1000 8, 1000 1000, 0 1000"/>' +
|
||||||
}
|
'</clippath>' +
|
||||||
|
'</defs>' +
|
||||||
mw.eventLog.logEvent( 'Popups', event ).then( dfd.resolve, dfd.reject );
|
'</svg>'
|
||||||
setTimeout( dfd.reject, 1000 );
|
);
|
||||||
elTime = elDuration = elAction = undefined;
|
return true;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @method getSessionId
|
* Create the element that holds the popups
|
||||||
* Generates a unique sessionId or pulls an existing one from localStorage
|
*
|
||||||
* @return {string} sessionId
|
* @method createPopupElement
|
||||||
*/
|
*/
|
||||||
function getSessionId() {
|
mw.popups.createPopupElement = function () {
|
||||||
var sessionId = null;
|
mw.popups.$popup = $( '<div>' )
|
||||||
try {
|
|
||||||
sessionId = localStorage.getItem( 'popupsSessionId' );
|
|
||||||
if ( sessionId === null ) {
|
|
||||||
sessionId = mw.user.generateRandomSessionId();
|
|
||||||
localStorage.setItem( 'popupsSessionId', sessionId );
|
|
||||||
}
|
|
||||||
} catch ( e ) {}
|
|
||||||
return sessionId;
|
|
||||||
}
|
|
||||||
elSessionId = getSessionId();
|
|
||||||
|
|
||||||
// Prevent popups from showing up when the mouse cursor accidentally
|
|
||||||
// ends up hovering a link after scrolling
|
|
||||||
$( window ).on( 'scroll', function () {
|
|
||||||
scrolled = true;
|
|
||||||
} );
|
|
||||||
$( window ).on( 'mousemove', function () {
|
|
||||||
scrolled = false;
|
|
||||||
} );
|
|
||||||
|
|
||||||
$( function () {
|
|
||||||
// Container for the popup
|
|
||||||
$box = $( '<div>' )
|
|
||||||
.attr( 'role', 'tooltip' )
|
.attr( 'role', 'tooltip' )
|
||||||
|
.attr( 'aria-hidden', 'true' )
|
||||||
.addClass( 'mwe-popups' )
|
.addClass( 'mwe-popups' )
|
||||||
.on( {
|
|
||||||
mouseleave: leaveActive,
|
|
||||||
mouseenter: function () {
|
|
||||||
if ( closeTimer ) {
|
|
||||||
clearTimeout( closeTimer );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} )
|
|
||||||
.appendTo( document.body );
|
.appendTo( document.body );
|
||||||
|
};
|
||||||
|
|
||||||
// SVG for masking and creating the triangle/pokey
|
/**
|
||||||
if ( supportsSVG ) {
|
* Temorarily remove tooltips from links on hover
|
||||||
$svg = $( '<div>' )
|
*
|
||||||
.attr( 'id', 'mwe-popups-svg' )
|
* @method removeTooltips
|
||||||
.appendTo( document.body )
|
*/
|
||||||
.html(
|
mw.popups.removeTooltips = function () {
|
||||||
'<svg width="0" height="0">' +
|
var notSelector = ':not(' + mw.popups.IGNORE_CLASSES.join(', ') + ')';
|
||||||
'<defs>' +
|
mw.popups.$content.find( 'a' + notSelector + ':not([title=""])' )
|
||||||
'<clippath id="mwe-popups-mask">' +
|
|
||||||
'<polygon points="0 8, 10 8, 18 0, 26 8, 1000 8, 1000 1000, 0 1000"/>' +
|
|
||||||
'</clippath>' +
|
|
||||||
'<clippath id="mwe-popups-mask-flip">' +
|
|
||||||
'<polygon points="0 8, 274 8, 282 0, 290 8, 1000 8, 1000 1000, 0 1000"/>' +
|
|
||||||
'</clippath>' +
|
|
||||||
'<clippath id="mwe-popups-landscape-mask">' +
|
|
||||||
'<polygon points="0 8, 174 8, 182 0, 190 8, 1000 8, 1000 1000, 0 1000"/>' +
|
|
||||||
'</clippath>' +
|
|
||||||
'</defs>' +
|
|
||||||
'</svg>'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
mw.hook( 'wikipage.content' ).add( function ( $content ) {
|
|
||||||
// Remove title attribute to remove the default yellow tooltip
|
|
||||||
// Put the title back after the hover
|
|
||||||
$content.find( 'a:not(.extiw):not(.image):not(.new):not(.internal):not([title=""])' )
|
|
||||||
.on( 'mouseenter focus', function () {
|
.on( 'mouseenter focus', function () {
|
||||||
$( this )
|
$( this )
|
||||||
.attr( 'data-original-title', $( this ).attr( 'title' ) )
|
.data( 'title', $( this ).attr( 'title' ) )
|
||||||
.attr( 'title', '' );
|
.attr( 'title', '' );
|
||||||
} )
|
} )
|
||||||
.on( 'mouseleave blur', function () {
|
.on( 'mouseleave blur', function () {
|
||||||
$( this )
|
$( this )
|
||||||
.attr( 'title', $( this ).attr( 'data-original-title' ) )
|
.attr( 'title', $( this ).data( 'title' ) );
|
||||||
.attr( 'data-original-title', '' );
|
|
||||||
} );
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
$content.find( 'a' ).on( 'mouseenter focus', function ( event ) {
|
/**
|
||||||
var $this = $( this ),
|
* Checks if the user is scrolling, sets to false on mousemove
|
||||||
href = $this.attr( 'href' ),
|
*
|
||||||
title = $this.attr( 'data-original-title' );
|
* @method checkScroll
|
||||||
|
*/
|
||||||
|
mw.popups.checkScroll = function () {
|
||||||
|
$( window ).on( 'scroll', function () {
|
||||||
|
mw.popups.scrolled = true;
|
||||||
|
} );
|
||||||
|
|
||||||
|
$( window ).on( 'mousemove', function () {
|
||||||
|
mw.popups.scrolled = false;
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the hover event on links
|
||||||
|
*
|
||||||
|
* @method setupTriggers
|
||||||
|
*/
|
||||||
|
mw.popups.setupTriggers = function () {
|
||||||
|
var notSelector = ':not(' + mw.popups.IGNORE_CLASSES.join(', ') + ')';
|
||||||
|
|
||||||
|
mw.popups.$content.find( 'a' + notSelector + ':not([title=""])' ).on( 'mouseenter focus', function ( event ) {
|
||||||
|
var
|
||||||
|
$this = $( this ),
|
||||||
|
href = $this.attr( 'href' );
|
||||||
|
|
||||||
// If a popup for the following link can't be shown
|
|
||||||
if (
|
if (
|
||||||
scrolled ||
|
mw.popups.scrolled || // Prevents hovering on popups while scrolling
|
||||||
!title ||
|
|
||||||
$this.hasClass( 'extiw' ) ||
|
|
||||||
$this.hasClass( 'image' ) ||
|
|
||||||
$this.hasClass( 'new' ) ||
|
|
||||||
$this.hasClass( 'internal' ) ||
|
|
||||||
href.indexOf( '?' ) !== -1 ||
|
href.indexOf( '?' ) !== -1 ||
|
||||||
href.indexOf( location.origin + location.pathname + '#' ) === 0
|
href.indexOf( location.origin + location.pathname + '#' ) === 0
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This will happen when the mouse goes from the popup box back to the
|
mw.popups.render.render( $this, event );
|
||||||
// anchor tag. In such a case, the timer to close the box is cleared.
|
|
||||||
if ( currentLink === this ) {
|
|
||||||
clearTimeout( closeTimer );
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the mouse moves to another link (we already check if its the same
|
|
||||||
// link in the previous condition), then close the popup.
|
|
||||||
if ( currentLink ) {
|
|
||||||
closeBox();
|
|
||||||
}
|
|
||||||
|
|
||||||
currentLink = this;
|
|
||||||
$this.on( 'mouseleave blur', leaveInactive );
|
|
||||||
|
|
||||||
// Delay to avoid triggering the popup and AJAX requests on accidental
|
|
||||||
// hovers (likes ones during srcolling or moving the pointer elsewhere).
|
|
||||||
if ( cache[ href ] ) {
|
|
||||||
// If we have this popup cached, just wait normally
|
|
||||||
openTimer = timeoutPromise( POPUP_DELAY ).done( function () {
|
|
||||||
createBox( href, $this, event );
|
|
||||||
} );
|
|
||||||
} else {
|
|
||||||
// Otherwise wait a little before we start loading the data (to avoid unnecessary requests),
|
|
||||||
// then wait normally before actually displaying the output
|
|
||||||
openTimer = timeoutPromise( API_DELAY ).done( function () {
|
|
||||||
openTimer = timeoutPromise( POPUP_DELAY - API_DELAY );
|
|
||||||
$.when(
|
|
||||||
sendRequest( href, title ),
|
|
||||||
openTimer
|
|
||||||
).done( function () {
|
|
||||||
// Data is cached now
|
|
||||||
createBox( href, $this, event );
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
} );
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
mw.hook( 'wikipage.content').add( function ( $content ) {
|
||||||
|
mw.popups.$content = $content;
|
||||||
|
mw.popups.removeTooltips();
|
||||||
|
mw.popups.setupTriggers();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
}( jQuery, mediaWiki ) );
|
$( function () {
|
||||||
|
mw.popups.checkScroll();
|
||||||
|
mw.popups.createSVGMask();
|
||||||
|
mw.popups.createPopupElement();
|
||||||
|
} );
|
||||||
|
|
||||||
|
} ) ( jQuery, mediaWiki );
|
||||||
|
|
110
resources/ext.popups.eventlogging.js
Normal file
110
resources/ext.popups.eventlogging.js
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
( function ( $, mw ) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class mw.popups.eventLogging
|
||||||
|
* @singleton
|
||||||
|
*/
|
||||||
|
var eventLogging = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unix timestamp of when the popup was rendered
|
||||||
|
* @property time
|
||||||
|
*/
|
||||||
|
eventLogging.time = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How long was the popup open in milliseconds
|
||||||
|
* @property {Number} duration
|
||||||
|
*/
|
||||||
|
eventLogging.duration = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Was the popup clicked, middle clicked or dismissed
|
||||||
|
* @property {String} action
|
||||||
|
*/
|
||||||
|
eventLogging.action = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs different actions such as meta and shift click on the popup
|
||||||
|
* Is bound to the `click` event
|
||||||
|
*
|
||||||
|
* @method logClick
|
||||||
|
* @param {Object} event
|
||||||
|
*/
|
||||||
|
eventLogging.logClick = function ( event ) {
|
||||||
|
if ( event.which === 2 ) { // middle click
|
||||||
|
eventLogging.action = 'opened in new tab';
|
||||||
|
} else if ( event.which === 1 ) {
|
||||||
|
if ( event.ctrlKey || event.metaKey ) {
|
||||||
|
eventLogging.action = 'opened in new tab';
|
||||||
|
} else if ( event.shiftKey ) {
|
||||||
|
eventLogging.action = 'opened in new window';
|
||||||
|
} else {
|
||||||
|
eventLogging.action = 'opened in same tab';
|
||||||
|
eventLogging.duration = mw.now() - eventLogging.time;
|
||||||
|
eventLogging.logEvent( mw.popups.render.currentLink.attr( 'href' ) );
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs the popup event as defined in the following schema-
|
||||||
|
* https://meta.wikimedia.org/wiki/Schema:Popups
|
||||||
|
* If `href` is passed it redirects to that location after the event is logged.
|
||||||
|
*
|
||||||
|
* @method logEvent
|
||||||
|
* @param {String} href
|
||||||
|
*/
|
||||||
|
eventLogging.logEvent = function ( href ) {
|
||||||
|
var
|
||||||
|
deferred = $.Deferred(),
|
||||||
|
event = {
|
||||||
|
'duration': Math.round( eventLogging.duration ),
|
||||||
|
'action': eventLogging.action
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( eventLogging.sessionId !== null ) {
|
||||||
|
event.sessionId = eventLogging.sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( href ) {
|
||||||
|
deferred.always( function () {
|
||||||
|
location.href = href;
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
mw.eventLog.logEvent( 'Popups', event ).then( deferred.resolve, deferred.reject );
|
||||||
|
|
||||||
|
// reset
|
||||||
|
eventLogging.time = undefined;
|
||||||
|
eventLogging.duration = undefined;
|
||||||
|
eventLogging.action = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a unique sessionId or pulls an existing one from localStorage
|
||||||
|
*
|
||||||
|
* @method getSessionsId
|
||||||
|
* @return {String} sessionId
|
||||||
|
*/
|
||||||
|
eventLogging.getSessionId = function () {
|
||||||
|
var sessionId = null;
|
||||||
|
try {
|
||||||
|
sessionId = localStorage.getItem( 'popupsSessionId' );
|
||||||
|
if ( sessionId === null ) {
|
||||||
|
sessionId = mw.user.getRandomSessionId();
|
||||||
|
localStorage.setItem( 'popupsSessionId', sessionId );
|
||||||
|
}
|
||||||
|
} catch ( e ) {}
|
||||||
|
return sessionId;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property sessionId
|
||||||
|
*/
|
||||||
|
eventLogging.sessionId = eventLogging.getSessionId();
|
||||||
|
|
||||||
|
mw.popups.eventLogging = eventLogging;
|
||||||
|
|
||||||
|
} ) ( jQuery, mediaWiki );
|
323
resources/ext.popups.renderer.article.js
Normal file
323
resources/ext.popups.renderer.article.js
Normal file
|
@ -0,0 +1,323 @@
|
||||||
|
( function ( $, mw ) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class mw.popups.render.article
|
||||||
|
* @singleton
|
||||||
|
*/
|
||||||
|
var article = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Size constants for popup images
|
||||||
|
* @property SIZES
|
||||||
|
*/
|
||||||
|
article.SIZES = {
|
||||||
|
portraitImage: {
|
||||||
|
h: 250, // Exact height
|
||||||
|
w: 203 // Max width
|
||||||
|
},
|
||||||
|
landscapeImage: {
|
||||||
|
h: 200, // Max height
|
||||||
|
w: 300 // Exact Width
|
||||||
|
},
|
||||||
|
landscapePopupWidth: 450, // Exact width of a landscape popup
|
||||||
|
portraitPopupWidth: 300 // Exact width of a portrait popup
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an API request and cache the jQuery element
|
||||||
|
*
|
||||||
|
* @param {jQuery} link
|
||||||
|
* @return {jQuery.Promise}
|
||||||
|
*/
|
||||||
|
article.init = function ( link ) {
|
||||||
|
var
|
||||||
|
href = link.attr( 'href' ),
|
||||||
|
title = link.data( 'title' ),
|
||||||
|
deferred = $.Deferred();
|
||||||
|
|
||||||
|
mw.popups.render.currentRequest = mw.popups.api.get( {
|
||||||
|
action: 'query',
|
||||||
|
prop: 'extracts|pageimages|revisions|info',
|
||||||
|
redirects: 'true',
|
||||||
|
exintro: 'true',
|
||||||
|
exsentences: 2,
|
||||||
|
// there is an added geometric limit on .mwe-popups-extract
|
||||||
|
// so that text does not overflow from the card
|
||||||
|
explaintext: 'true',
|
||||||
|
piprop: 'thumbnail',
|
||||||
|
pithumbsize: 300,
|
||||||
|
rvprop: 'timestamp',
|
||||||
|
inprop: 'watched',
|
||||||
|
indexpageids: true,
|
||||||
|
titles: title
|
||||||
|
} );
|
||||||
|
|
||||||
|
mw.popups.render.currentRequest.done( function ( re ) {
|
||||||
|
mw.popups.render.currentRequest = undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!re.query.pages[ re.query.pageids[ 0 ] ].extract ||
|
||||||
|
re.query.pages[ re.query.pageids[ 0 ] ].extract === ''
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mw.popups.render.cache[ href ] = {};
|
||||||
|
mw.popups.render.cache[ href ].popup = article.createPopup( re, href );
|
||||||
|
mw.popups.render.cache[ href ].getOffset = article.getOffset;
|
||||||
|
mw.popups.render.cache[ href ].getClasses = article.getClasses;
|
||||||
|
mw.popups.render.cache[ href ].process = article.processPopup;
|
||||||
|
deferred.resolve();
|
||||||
|
} );
|
||||||
|
|
||||||
|
return deferred.promise();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a thumbnail object based on the ratio of the image
|
||||||
|
* Uses an SVG image where available to add the triangle/pokey
|
||||||
|
* mask on the image. Crops and resizes the SVG image so that
|
||||||
|
* is fits inside a rectangle of a particular size.
|
||||||
|
*
|
||||||
|
* @method createPopup
|
||||||
|
* @param {Object} re
|
||||||
|
* @param {String} href
|
||||||
|
* @return {jQuery}
|
||||||
|
*/
|
||||||
|
article.createPopup = function ( re, href ) {
|
||||||
|
var $a,
|
||||||
|
page = re.query.pages[ re.query.pageids[ 0 ] ],
|
||||||
|
$contentbox = $( '<div>' )
|
||||||
|
.addClass( 'mwe-popups-extract' )
|
||||||
|
.text( page.extract ),
|
||||||
|
thumbnail = page.thumbnail,
|
||||||
|
tall = thumbnail && thumbnail.height > thumbnail.width,
|
||||||
|
$thumbnail = article.createThumbnail( thumbnail, tall ),
|
||||||
|
timestamp = new Date( page.revisions[ 0 ].timestamp ),
|
||||||
|
timediff = new Date() - timestamp,
|
||||||
|
oneDay = 1000 * 60 * 60 * 24,
|
||||||
|
timestampclass = ( timediff < oneDay ) ?
|
||||||
|
'mwe-popups-timestamp-recent' :
|
||||||
|
'mwe-popups-timestamp-older',
|
||||||
|
$timestamp = $( '<div>' )
|
||||||
|
.addClass( timestampclass )
|
||||||
|
.append(
|
||||||
|
$( '<span>' ).text( mw.message( 'popups-last-edited',
|
||||||
|
moment( timestamp ).fromNow() ).text() )
|
||||||
|
);
|
||||||
|
|
||||||
|
$a = $( '<a>' )
|
||||||
|
.append( $thumbnail, $contentbox, $timestamp )
|
||||||
|
.attr( 'href', href )
|
||||||
|
.on( 'click', mw.popups.eventLogging.logClick );
|
||||||
|
|
||||||
|
mw.popups.render.cache[ href ].settings = {
|
||||||
|
'tall': ( tall === undefined ) ? false : tall,
|
||||||
|
'thumbnail': ( thumbnail === undefined ) ? false : thumbnail
|
||||||
|
};
|
||||||
|
|
||||||
|
return $a;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use createElementNS to create the svg:image tag as jQuery
|
||||||
|
* uses createElement instead. Some browsers map the `image` tag
|
||||||
|
* to `img` tag, thus an `svg:image` is required.
|
||||||
|
*
|
||||||
|
* @method createSVGTag
|
||||||
|
* @param {String} tag
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
article.createSVGTag = function ( tag ) {
|
||||||
|
return document.createElementNS( 'http://www.w3.org/2000/svg', tag );
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a thumbnail object based on the ratio of the image
|
||||||
|
* Uses an SVG image where available to add the triangle/pokey
|
||||||
|
* mask on the image. Crops and resizes the SVG image so that
|
||||||
|
* is fits inside a rectangle of a particular size.
|
||||||
|
*
|
||||||
|
* @method createThumbnail
|
||||||
|
* @param {Object} thumbnail
|
||||||
|
* @param {boolean} tall
|
||||||
|
* @return {Object} jQuery DOM element of the thumbnail
|
||||||
|
*/
|
||||||
|
article.createThumbnail = function ( thumbnail, tall ) {
|
||||||
|
if ( !thumbnail ) {
|
||||||
|
return $( '<span>' );
|
||||||
|
}
|
||||||
|
|
||||||
|
var $thumbnailSVGImage, $thumbnail;
|
||||||
|
|
||||||
|
if ( tall ) {
|
||||||
|
if ( mw.popups.supportsSVG ) {
|
||||||
|
$thumbnailSVGImage = $( article.createSVGTag( 'image' ) );
|
||||||
|
$thumbnailSVGImage
|
||||||
|
.addClass( 'mwe-popups-is-not-tall' )
|
||||||
|
.attr( {
|
||||||
|
'xlink:href': thumbnail.source,
|
||||||
|
x: ( thumbnail.width > article.SIZES.portraitImage.w ) ?
|
||||||
|
( ( thumbnail.width - article.SIZES.portraitImage.w ) / -2 ) :
|
||||||
|
( article.SIZES.portraitImage.w - thumbnail.width ),
|
||||||
|
y: ( thumbnail.height > article.SIZES.portraitImage.h ) ?
|
||||||
|
( ( thumbnail.height - article.SIZES.portraitImage.h ) / -2 ) :
|
||||||
|
0,
|
||||||
|
width: thumbnail.width,
|
||||||
|
height: thumbnail.height
|
||||||
|
} );
|
||||||
|
|
||||||
|
$thumbnail = $( '<svg>' )
|
||||||
|
.attr( {
|
||||||
|
xmlns: 'http://www.w3.org/2000/svg',
|
||||||
|
width: article.SIZES.portraitImage.w,
|
||||||
|
height: article.SIZES.portraitImage.h
|
||||||
|
} )
|
||||||
|
.append( $thumbnailSVGImage );
|
||||||
|
} else {
|
||||||
|
$thumbnail = $( '<div>' )
|
||||||
|
.addClass( 'mwe-popups-is-tall' )
|
||||||
|
.css( 'background-image', 'url(' + thumbnail.source + ')' );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ( mw.popups.supportsSVG ) {
|
||||||
|
$thumbnailSVGImage = $( article.createSVGTag( 'image' ) );
|
||||||
|
$thumbnailSVGImage
|
||||||
|
.addClass( 'mwe-popups-is-not-tall' )
|
||||||
|
.attr( {
|
||||||
|
'xlink:href': thumbnail.source,
|
||||||
|
'clip-path': 'url(#mwe-popups-mask)',
|
||||||
|
x: 0,
|
||||||
|
y: ( thumbnail.height > article.SIZES.landscapeImage.h ) ?
|
||||||
|
( ( thumbnail.height - article.SIZES.landscapeImage.h ) / -2 ) :
|
||||||
|
0,
|
||||||
|
width: thumbnail.width,
|
||||||
|
height: thumbnail.height
|
||||||
|
} );
|
||||||
|
|
||||||
|
$thumbnail = $( '<svg>' )
|
||||||
|
.attr( {
|
||||||
|
xmlns: 'http://www.w3.org/2000/svg',
|
||||||
|
width: article.SIZES.landscapeImage.w + 3,
|
||||||
|
height: ( thumbnail.height > article.SIZES.landscapeImage.h ) ?
|
||||||
|
article.SIZES.landscapeImage.h :
|
||||||
|
thumbnail.height
|
||||||
|
} )
|
||||||
|
.append( $thumbnailSVGImage );
|
||||||
|
} else {
|
||||||
|
$thumbnail = $( '<div>' )
|
||||||
|
.addClass( 'mwe-popups-is-not-tall' )
|
||||||
|
.css( 'background-image', 'url(' + thumbnail.source + ')' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $thumbnail;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Positions the popup based on the mouse position and popup size
|
||||||
|
*
|
||||||
|
* @method getOffset
|
||||||
|
* @param {jQuery} link
|
||||||
|
* @param {Object} event
|
||||||
|
* @return {Object} This can be passed to `.css()` to position the element
|
||||||
|
*/
|
||||||
|
article.getOffset = function ( link, event ) {
|
||||||
|
var
|
||||||
|
href = link.attr( 'href' ),
|
||||||
|
flipped = false,
|
||||||
|
settings = mw.popups.render.cache[ href ].settings,
|
||||||
|
offsetTop = ( event.pageY ) ?
|
||||||
|
event.pageY + 20 :
|
||||||
|
link.offset().top + link.height() + 9,
|
||||||
|
offsetLeft = ( event.pageX ) ?
|
||||||
|
event.pageX :
|
||||||
|
link.offset().left;
|
||||||
|
|
||||||
|
if ( offsetLeft > ( $( window ).width() / 2 ) ) {
|
||||||
|
offsetLeft += ( !event.pageX ) ? link.width() : 0;
|
||||||
|
offsetLeft -= ( !settings.tall ) ?
|
||||||
|
article.SIZES.portraitPopupWidth :
|
||||||
|
article.SIZES.landscapePopupWidth;
|
||||||
|
flipped = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( event.pageX ) {
|
||||||
|
offsetLeft += ( flipped ) ? 20 : -20;
|
||||||
|
}
|
||||||
|
|
||||||
|
mw.popups.render.cache[ href ].settings.flipped = flipped;
|
||||||
|
|
||||||
|
return {
|
||||||
|
top: offsetTop + 'px',
|
||||||
|
left: offsetLeft + 'px'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of classes based on the size and setting of the popup
|
||||||
|
*
|
||||||
|
* @method getClassses
|
||||||
|
* @param {jQuery} link
|
||||||
|
* @return {Array} List of classes to applied to the parent `div`
|
||||||
|
*/
|
||||||
|
article.getClasses = function ( link ) {
|
||||||
|
var
|
||||||
|
classes = [],
|
||||||
|
cache = mw.popups.render.cache [ link.attr( 'href' ) ],
|
||||||
|
tall = cache.settings.tall,
|
||||||
|
thumbnail = cache.settings.thumbnail,
|
||||||
|
flipped = cache.settings.flipped;
|
||||||
|
|
||||||
|
if ( flipped ) {
|
||||||
|
classes.push( 'flipped' );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !thumbnail || tall ) {
|
||||||
|
classes.push( 'mwe-popups-no-image-tri' );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( thumbnail && !tall ) {
|
||||||
|
classes.push( 'mwe-popups-image-tri' );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( tall ) {
|
||||||
|
classes.push( 'mwe-popups-is-tall' );
|
||||||
|
} else {
|
||||||
|
classes.push( 'mwe-popups-is-not-tall' );
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processed the popup div after it has been displayed
|
||||||
|
* to correctly render the triangle/pokeys
|
||||||
|
*
|
||||||
|
* @method processPopups
|
||||||
|
* @param {jQuery} link
|
||||||
|
*/
|
||||||
|
article.processPopup = function ( link ) {
|
||||||
|
var
|
||||||
|
cache = mw.popups.render.cache [ link.attr( 'href' ) ],
|
||||||
|
tall = cache.settings.tall,
|
||||||
|
thumbnail = cache.settings.thumbnail,
|
||||||
|
flipped = cache.settings.flipped;
|
||||||
|
|
||||||
|
if ( flipped && thumbnail ) {
|
||||||
|
if ( !tall ) {
|
||||||
|
mw.popups.$popup
|
||||||
|
.find( 'image' )[ 0 ]
|
||||||
|
.setAttribute( 'clip-path', 'url(#mwe-popups-mask-flip)' );
|
||||||
|
} else {
|
||||||
|
mw.popups.$popup
|
||||||
|
.removeClass( 'mwe-popups-no-image-tri' )
|
||||||
|
.find( 'image' )[ 0 ]
|
||||||
|
.setAttribute( 'clip-path', 'url(#mwe-popups-landscape-mask)' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mw.popups.render.article = article;
|
||||||
|
|
||||||
|
} ) ( jQuery, mediaWiki );
|
263
resources/ext.popups.renderer.js
Normal file
263
resources/ext.popups.renderer.js
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
( function ( $, mw ) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class mw.popups.render
|
||||||
|
* @singleton
|
||||||
|
*/
|
||||||
|
mw.popups.render = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time to wait in ms before showing a popup on hover
|
||||||
|
* @property POPUP_DELAY
|
||||||
|
*/
|
||||||
|
mw.popups.render.POPUP_DELAY = 150;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time to wait in ms before closing a popup on de-hover
|
||||||
|
* @property POPUP_CLOSE_DELAY
|
||||||
|
*/
|
||||||
|
mw.popups.render.POPUP_CLOSE_DELAY = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time to wait in ms before starting the API queries on hover, must be <= POPUP_DELAY
|
||||||
|
* @property API_DELAY
|
||||||
|
*/
|
||||||
|
mw.popups.render.API_DELAY = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache of all the popups that were opened in this session
|
||||||
|
* @property {Object} cache
|
||||||
|
*/
|
||||||
|
mw.popups.render.cache = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timer used to delay `closePopups`
|
||||||
|
* @property {jQuery.Deferred} closeTimer
|
||||||
|
*/
|
||||||
|
mw.popups.render.closeTimer = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timer used to delay sending the API request/opening the popup form cache
|
||||||
|
* @property {jQuery.Deferred} openTimer
|
||||||
|
*/
|
||||||
|
mw.popups.render.openTimer = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The link the currently has a popup
|
||||||
|
* @property {jQuery} currentLink
|
||||||
|
*/
|
||||||
|
mw.popups.render.currentLink = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current API request
|
||||||
|
* @property {jQuery.Deferred} currentRequest
|
||||||
|
*/
|
||||||
|
mw.popups.render.currentRequest = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close all other popups and render the new one from the cache
|
||||||
|
* or by finding and calling the correct renderer
|
||||||
|
*
|
||||||
|
* @method render
|
||||||
|
* @param {Object} link
|
||||||
|
* @param {Object} event
|
||||||
|
*/
|
||||||
|
mw.popups.render.render = function ( link, event ) {
|
||||||
|
// This will happen when the mouse goes from the popup box back to the
|
||||||
|
// anchor tag. In such a case, the timer to close the box is cleared.
|
||||||
|
if ( mw.popups.render.currentLink === link ) {
|
||||||
|
mw.popups.render.closeTimer.abort();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the mouse moves to another link (we already check if its the same
|
||||||
|
// link in the previous condition), then close the popup.
|
||||||
|
if ( mw.popups.render.currentLink ) {
|
||||||
|
mw.popups.render.closePopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
mw.popups.render.currentLink = link;
|
||||||
|
link.on( 'mouseleave blur', mw.popups.render.leaveInactive );
|
||||||
|
|
||||||
|
if ( mw.popups.render.cache[ link.attr( 'href' ) ] ) {
|
||||||
|
mw.popups.render.openTimer = mw.popups.render.wait( mw.popups.render.POPUP_DELAY )
|
||||||
|
.done( function () {
|
||||||
|
mw.popups.render.openPopup( link, event );
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
// TODO: check for link type and call correct renderer
|
||||||
|
// There is only one popup type so it isn't necessary right now
|
||||||
|
var cachePopup = mw.popups.render.article.init( link );
|
||||||
|
|
||||||
|
mw.popups.render.openTimer = mw.popups.render.wait( mw.popups.render.API_DELAY )
|
||||||
|
.done( function () {
|
||||||
|
mw.popups.render.openTimer = mw.popups.render.wait( mw.popups.render.POPUP_DELAY - mw.popups.render.API_DELAY );
|
||||||
|
|
||||||
|
$.when( mw.popups.render.openTimer, cachePopup ).done( function () {
|
||||||
|
mw.popups.render.openPopup( link, event );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the popup from the cache, uses its offset function
|
||||||
|
* applied classes and calls the process function.
|
||||||
|
* Takes care of event logging and attaching other events.
|
||||||
|
*
|
||||||
|
* @method openPopup
|
||||||
|
* @param {Object} link
|
||||||
|
* @param {Object} event
|
||||||
|
*/
|
||||||
|
mw.popups.render.openPopup = function ( link, event ) {
|
||||||
|
var
|
||||||
|
cache = mw.popups.render.cache [ link.attr( 'href' ) ],
|
||||||
|
popup = cache.popup,
|
||||||
|
offset = cache.getOffset( link, event ),
|
||||||
|
classes = cache.getClasses( link );
|
||||||
|
|
||||||
|
mw.popups.$popup
|
||||||
|
.html( '' )
|
||||||
|
.attr( 'class', 'mwe-popups' )
|
||||||
|
.addClass( classes.join( ' ' ) )
|
||||||
|
.css( offset )
|
||||||
|
.append( popup )
|
||||||
|
.show()
|
||||||
|
.attr( 'aria-hidden', 'false' )
|
||||||
|
.addClass( 'mwe-popups-fade-in' )
|
||||||
|
.on( 'mouseleave', mw.popups.render.leaveActive )
|
||||||
|
.on( 'mouseenter', function () {
|
||||||
|
if ( mw.popups.render.closeTimer ) {
|
||||||
|
mw.popups.render.closeTimer.abort();
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
// Hack to 'refresh' the SVG and thus display it
|
||||||
|
// Elements get added to the DOM and not to the screen because of different namespaces of HTML and SVG
|
||||||
|
// More information and workarounds here - http://stackoverflow.com/a/13654655/366138
|
||||||
|
mw.popups.$popup.html( mw.popups.$popup.html() );
|
||||||
|
|
||||||
|
cache.process( link );
|
||||||
|
|
||||||
|
// Event logging
|
||||||
|
mw.popups.eventLogging.time = mw.now();
|
||||||
|
mw.popups.eventLogging.action = 'dismissed';
|
||||||
|
|
||||||
|
link
|
||||||
|
.off( 'mouseleave blur', mw.popups.render.leaveInactive )
|
||||||
|
.on( 'mouseleave blur', mw.popups.render.leaveActive );
|
||||||
|
|
||||||
|
$( document ).on( 'keydown', mw.popups.render.closeOnEsc );
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the hover class from the link and unbinds events
|
||||||
|
* Hides the popup, clears timers and sets it and the resets the renderer
|
||||||
|
*
|
||||||
|
* @method closePopup
|
||||||
|
*/
|
||||||
|
mw.popups.render.closePopup = function () {
|
||||||
|
mw.popups.eventLogging.duration = mw.now() - mw.popups.eventLogging.time;
|
||||||
|
|
||||||
|
$( mw.popups.render.currentLink ).off( 'mouseleave blur', mw.popups.render.leaveActive );
|
||||||
|
|
||||||
|
mw.popups.$popup
|
||||||
|
.removeClass( 'mwe-popups-fade-in' )
|
||||||
|
.addClass( 'mwe-popups-fade-out' );
|
||||||
|
|
||||||
|
mw.popups.render.wait( 150 ).done( function () {
|
||||||
|
if ( mw.popups.$popup.hasClass( 'mwe-popups-fade-out' ) ) {
|
||||||
|
mw.popups.$popup
|
||||||
|
.attr( 'aria-hidden', 'true' )
|
||||||
|
.hide()
|
||||||
|
.removeClass( 'mwe-popups-fade-out' );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
if ( mw.popups.render.closeTimer ) {
|
||||||
|
mw.popups.render.closeTimer.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
$( document ).off( 'keydown', mw.popups.render.closeOnEsc );
|
||||||
|
mw.popups.render.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a promise corresponding to a `setTimeout()` call. Call `.abort()` on the return value
|
||||||
|
* to perform the equivalent of `clearTimeout()`
|
||||||
|
*
|
||||||
|
* @method wait
|
||||||
|
* @param {Number} ms Milliseconds to wait
|
||||||
|
* @return {jQuery.Promise}
|
||||||
|
*/
|
||||||
|
mw.popups.render.wait = function ( ms ) {
|
||||||
|
var deferred, promise, timeout;
|
||||||
|
|
||||||
|
deferred = $.Deferred();
|
||||||
|
|
||||||
|
timeout = setTimeout( function () {
|
||||||
|
deferred.resolve();
|
||||||
|
}, ms );
|
||||||
|
|
||||||
|
promise = deferred.promise( { abort: function () {
|
||||||
|
clearTimeout( timeout );
|
||||||
|
deferred.reject();
|
||||||
|
} } );
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use escape to close popup
|
||||||
|
*
|
||||||
|
* @method closeOnEsc
|
||||||
|
*/
|
||||||
|
mw.popups.render.closeOnEsc = function ( event ) {
|
||||||
|
if ( event.keyCode === 27 ) {
|
||||||
|
mw.popups.render.closePopup();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the box after a delay
|
||||||
|
* Delay to give enough time for the user to move the pointer from
|
||||||
|
* the link to the popup box. Also avoids closing the popup by accident
|
||||||
|
*
|
||||||
|
* @method leaveActive
|
||||||
|
*/
|
||||||
|
mw.popups.render.leaveActive = function () {
|
||||||
|
mw.popups.render.closeTimer = mw.popups.render.wait( mw.popups.render.POPUP_CLOSE_DELAY ).done( function () {
|
||||||
|
mw.popups.render.closePopup();
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unbinds events on the anchor tag and aborts AJAX request.
|
||||||
|
*
|
||||||
|
* @method leaveInactive
|
||||||
|
*/
|
||||||
|
mw.popups.render.leaveInactive = function () {
|
||||||
|
$( mw.popups.render.currentLink ).off( 'mouseleave', mw.popups.render.leaveInactive );
|
||||||
|
if ( mw.popups.render.openTimer ) {
|
||||||
|
mw.popups.render.openTimer.abort();
|
||||||
|
}
|
||||||
|
if ( mw.popups.render.currentRequest ) {
|
||||||
|
mw.popups.render.currentRequest.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
mw.popups.render.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the renderer
|
||||||
|
*
|
||||||
|
* @method reset
|
||||||
|
*/
|
||||||
|
mw.popups.render.reset = function () {
|
||||||
|
mw.popups.render.currentLink = undefined;
|
||||||
|
mw.popups.render.currentRequest = undefined;
|
||||||
|
mw.popups.render.openTimer = undefined;
|
||||||
|
mw.popups.render.closeTimer = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
} ) ( jQuery, mediaWiki );
|
Loading…
Reference in a new issue