mediawiki-extensions-Popups/resources/ext.popups.renderer/desktopRenderer.js

366 lines
10 KiB
JavaScript
Raw Normal View History

/*global popupDelay: true, popupHideDelay: true*/
( function ( $, mw ) {
var closeTimer, openTimer,
logData = {};
/**
* Logs the click on link or popup
*
* @param {Object} event
*/
function logClickAction( event ) {
mw.track( 'ext.popups.schemaPopups', $.extend( {}, logData, {
action: mw.popups.getAction( event ),
totalInteractionTime: Math.round( mw.now() - logData.dwellStartTime )
} ) );
}
/**
* @class mw.popups.render
* @singleton
*/
mw.popups.render = {};
/**
* Time to wait in ms before showing a popup on hover.
* Use the navigation popup delay if it has been set by the user.
* This isn't the official way of setting the delay
* TODO: Add setting to change delay
* @property POPUP_DELAY
*/
mw.popups.render.POPUP_DELAY = ( typeof popupDelay === 'undefined' ) ?
500 :
popupDelay * 1000;
/**
* Time to wait in ms before closing a popup on de-hover.
* Use the navigation popup delay if it has been set by the user
* This isn't the official way of setting the delay
* TODO: Add setting to change delay
* @property POPUP_CLOSE_DELAY
*/
mw.popups.render.POPUP_CLOSE_DELAY = ( typeof popupHideDelay === 'undefined' ) ?
300 :
popupHideDelay * 1000;
/**
* 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 link the currently has a popup
* @property {jQuery} currentLink
*/
mw.popups.render.currentLink = undefined;
/**
* Object to store all renderers
* @property {Object} renderers
*/
mw.popups.render.renderers = {};
/**
* 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
* @param {number} dwellStartTime the instant when the link is dwelled on
* @param {string} linkInteractionToken random token representing the current interaction with the link
*/
mw.popups.render.render = function ( link, event, dwellStartTime, linkInteractionToken ) {
var linkHref = link.attr( 'href' );
// 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 &&
mw.popups.render.currentLink[ 0 ] === link[ 0 ]
) {
if ( closeTimer ) {
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();
}
// Ignore if its meant to call a function
// TODO: Remove this when adding reference popups
if ( linkHref === '#' ) {
return;
}
mw.popups.render.currentLink = link;
// Set the log data only after the current link is set, otherwise, functions like
// closePopup will use the new log data when closing an old popup.
logData = {
pageTitleHover: mw.popups.getTitle( linkHref ),
dwellStartTime: dwellStartTime,
linkInteractionToken: linkInteractionToken
};
link.on( 'mouseleave blur', mw.popups.render.leaveInactive );
link.off( 'click', logClickAction ).on( 'click', logClickAction );
if ( mw.popups.render.cache[ link.attr( 'href' ) ] ) {
openTimer = mw.popups.render.wait( mw.popups.render.POPUP_DELAY )
.done( function () {
mw.popups.render.openPopup( link, event );
} );
} else {
// Wait for timer before making API queries and showing hovercard
openTimer = mw.popups.render.wait( mw.popups.render.API_DELAY )
.done( function () {
var cachePopup, key,
renderers = mw.popups.render.renderers;
// Check run the matcher method of all renderers to find the right one
for ( key in renderers ) {
if ( renderers.hasOwnProperty( key ) && key !== 'article' ) {
if ( !!renderers[ key ].matcher( link.attr( 'href' ) ) ) {
cachePopup = renderers[ key ].init( link, $.extend( {}, logData ) );
}
}
}
// Use the article renderer if nothing else matches
if ( cachePopup === undefined ) {
cachePopup = mw.popups.render.renderers.article.init( link, $.extend( {}, logData ) );
}
openTimer = mw.popups.render.wait( mw.popups.render.POPUP_DELAY - mw.popups.render.API_DELAY );
$.when( 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 )
// Clone the element to avoid manipulating the cached object accidentally (see T68496)
.append( popup.clone() )
.show()
.attr( 'aria-hidden', 'false' )
.on( 'mouseleave', mw.popups.render.leaveActive )
.on( 'mouseenter', function () {
if ( closeTimer ) {
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() );
// Event logging
$.extend( logData, {
pageTitleHover: cache.settings.title,
namespaceIdHover: cache.settings.namespace,
perceivedWait: Math.round( mw.now() - logData.dwellStartTime )
} );
cache.process( link, $.extend( {}, logData ) );
mw.popups.$popup.find( 'a.mwe-popups-extract, a.mwe-popups-discreet' ).click( mw.popups.render.clickHandler );
link
.off( 'mouseleave blur', mw.popups.render.leaveInactive )
.on( 'mouseleave blur', mw.popups.render.leaveActive );
$( document ).on( 'keydown', mw.popups.render.closeOnEsc );
mw.popups.incrementPreviewCount();
};
/**
* Click handler for the hovercard
*
* @method clickHandler
* @param {Object} event
*/
mw.popups.render.clickHandler = function ( event ) {
var action = mw.popups.getAction( event );
logClickAction( event );
if ( action === 'opened in same tab' ) {
window.location.href = mw.popups.render.currentLink.attr( 'href' );
}
};
/**
* 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 () {
var fadeInClass, fadeOutClass;
if ( mw.popups.render.currentLink === undefined ) {
return false;
}
$( mw.popups.render.currentLink ).off( 'mouseleave blur', mw.popups.render.leaveActive );
fadeInClass = ( mw.popups.$popup.hasClass( 'mwe-popups-fade-in-up' ) ) ?
'mwe-popups-fade-in-up' :
'mwe-popups-fade-in-down';
fadeOutClass = ( fadeInClass === 'mwe-popups-fade-in-up' ) ?
'mwe-popups-fade-out-down' :
'mwe-popups-fade-out-up';
mw.popups.$popup
.off( 'mouseleave', mw.popups.render.leaveActive )
.removeClass( fadeInClass )
.addClass( fadeOutClass );
mw.popups.render.wait( 150 ).done( function () {
if ( mw.popups.$popup.hasClass( fadeOutClass ) ) {
mw.popups.$popup
.attr( 'aria-hidden', 'true' )
.hide()
.removeClass( 'mwe-popups-fade-out-down' );
}
} );
mw.track( 'ext.popups.schemaPopups', $.extend( {}, logData, {
action: 'dismissed',
totalInteractionTime: Math.round( mw.now() - logData.dwellStartTime )
} ) );
if ( closeTimer ) {
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 () {
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 () {
if ( logData.dwellStartTime &&
logData.linkInteractionToken &&
mw.now() - logData.dwellStartTime >= 250
) {
mw.track( 'ext.popups.schemaPopups', $.extend( {}, logData, {
action: 'dwelledButAbandoned',
totalInteractionTime: Math.round( mw.now() - logData.dwellStartTime )
} ) );
}
// TODO: should `blur` also be here?
$( mw.popups.render.currentLink ).off( 'mouseleave', mw.popups.render.leaveInactive );
if ( openTimer ) {
openTimer.abort();
}
mw.popups.render.abortCurrentRequest();
mw.popups.render.reset();
};
/**
* Resets the renderer
*
* @method reset
*/
mw.popups.render.reset = function () {
logData = {};
mw.popups.render.currentLink = undefined;
mw.popups.render.abortCurrentRequest();
openTimer = undefined;
closeTimer = undefined;
};
} )( jQuery, mediaWiki );