mediawiki-extensions-Popups/resources/ext.popups.renderer/desktopRenderer.js
Baha a8e8ca489f Correctly log the 'dismissed' action when hovering over another link
Unlike what was previously implemented, this patch does not care whether
the user hovers over another link while dismissing the popup.
The `dismissed` action is correctly logged in these cases too.

Bug: T136649
Change-Id: I68473cb8b66bae53213bce186345ca1ce436573f
2016-06-02 15:24:31 -04:00

366 lines
10 KiB
JavaScript

/*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 );