mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Popups
synced 2024-11-15 11:46:55 +00:00
dedb61caf9
According to caniuse.com SVG support is available from IE > 8, Firefox > 3, Safari > 3.1 and Android > 2.3. Android 3-4.3 does not support masking. Out of all these browsers, considering market share and ResourceLoader support, none of these browsers are of concern to us. In IE8 for example we do not run JavaScript for our end users. Thus we should remove this fallback support. Changes * Remove createImgThumbnail method and its test * Groups duplicate CSS groups * Refactor createThumbnail function ** Leave a FIXME on some curious code Change-Id: I59ac2e320b2e07815bc4136d5942016fdc1d4340 Bug: T135554
654 lines
18 KiB
JavaScript
654 lines
18 KiB
JavaScript
( function ( $, mw ) {
|
|
'use strict';
|
|
|
|
/**
|
|
* @class mw.popups.render.article
|
|
* @singleton
|
|
*/
|
|
var currentRequest,
|
|
article = {},
|
|
surveyLink = mw.config.get( 'wgPopupsSurveyLink' ),
|
|
$window = $( window ),
|
|
CHARS = 525,
|
|
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
|
|
pokeySize: 8 // Height of the triangle used to point at the link
|
|
};
|
|
|
|
/**
|
|
* Send an API request and cache the jQuery element
|
|
*
|
|
* @param {jQuery} link
|
|
* @param {Object} logData data to be logged
|
|
* @return {jQuery.Promise}
|
|
*/
|
|
article.init = function ( link, logData ) {
|
|
var href = link.attr( 'href' ),
|
|
title = mw.popups.getTitle( href ),
|
|
deferred = $.Deferred(),
|
|
scaledThumbSize = 300 * $.bracketedDevicePixelRatio();
|
|
|
|
if ( !title ) {
|
|
return deferred.reject().promise();
|
|
}
|
|
|
|
currentRequest = mw.popups.api.get( {
|
|
action: 'query',
|
|
prop: 'info|extracts|pageimages|revisions',
|
|
formatversion: 2,
|
|
redirects: true,
|
|
exintro: true,
|
|
exchars: CHARS,
|
|
// there is an added geometric limit on .mwe-popups-extract
|
|
// so that text does not overflow from the card
|
|
explaintext: true,
|
|
piprop: 'thumbnail',
|
|
pithumbsize: scaledThumbSize,
|
|
rvprop: 'timestamp',
|
|
titles: title,
|
|
smaxage: 300,
|
|
maxage: 300,
|
|
uselang: 'content'
|
|
}, {
|
|
headers: {
|
|
'X-Analytics': 'preview=1'
|
|
}
|
|
} );
|
|
|
|
currentRequest.fail( function ( textStatus ) {
|
|
mw.track( 'ext.popups.schemaPopups', $.extend( logData, {
|
|
action: 'error',
|
|
errorState: textStatus,
|
|
totalInteractionTime: Math.round( mw.now() - logData.dwellStartTime )
|
|
} ) );
|
|
deferred.reject();
|
|
} )
|
|
.done( function ( re ) {
|
|
currentRequest = undefined;
|
|
|
|
if (
|
|
!re.query ||
|
|
!re.query.pages ||
|
|
!re.query.pages[ 0 ].extract ||
|
|
re.query.pages[ 0 ].extract === ''
|
|
) {
|
|
// Restore the title attribute and set flag
|
|
if ( link.data( 'dont-empty-title' ) !== true ) {
|
|
link
|
|
.attr( 'title', link.data( 'title' ) )
|
|
.removeData( 'title' )
|
|
.data( 'dont-empty-title', true );
|
|
}
|
|
deferred.reject();
|
|
return;
|
|
}
|
|
|
|
re.query.pages[ 0 ].extract = removeEllipsis( re.query.pages[ 0 ].extract );
|
|
|
|
mw.popups.render.cache[ href ] = {};
|
|
mw.popups.render.cache[ href ].popup = article.createPopup( re.query.pages[ 0 ], 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} page Information about the linked page
|
|
* @param {string} href
|
|
* @return {jQuery}
|
|
*/
|
|
article.createPopup = function ( page, href ) {
|
|
var $div, hasThumbnail,
|
|
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;
|
|
|
|
// createThumbnail returns an empty <span> if there is no thumbnail
|
|
hasThumbnail = $thumbnail.prop( 'tagName' ) !== 'SPAN';
|
|
|
|
$div = mw.template.get( 'ext.popups.desktop', 'popup.mustache' ).render( {
|
|
langcode: page.pagelanguagehtmlcode,
|
|
langdir: page.pagelanguagedir,
|
|
href: href,
|
|
isRecent: timediff < oneDay,
|
|
lastModified: mw.message( 'popups-last-edited', moment( timestamp ).fromNow() ).text(),
|
|
hasThumbnail: hasThumbnail
|
|
} );
|
|
|
|
// FIXME: Ideally these things should be added in template. These will be refactored in future patches.
|
|
if ( !hasThumbnail ) {
|
|
tall = thumbnail = undefined;
|
|
} else {
|
|
$div.find( '.mwe-popups-discreet' ).append( $thumbnail );
|
|
}
|
|
$div.find( '.mwe-popups-extract' )
|
|
.append( article.getProcessedElements( page.extract, page.title ) );
|
|
if ( surveyLink ) {
|
|
$div.find( 'footer' ).append( article.createSurveyLink( surveyLink ) );
|
|
}
|
|
|
|
mw.popups.render.cache[ href ].settings = {
|
|
title: page.title,
|
|
namespace: page.ns,
|
|
tall: ( tall === undefined ) ? false : tall,
|
|
thumbnail: ( thumbnail === undefined ) ? false : thumbnail
|
|
};
|
|
|
|
return $div;
|
|
};
|
|
|
|
/**
|
|
* Creates a link to a survey, possibly hosted on an external site.
|
|
*
|
|
* @param {string} url
|
|
* @return {jQuery}
|
|
*/
|
|
article.createSurveyLink = function ( url ) {
|
|
if ( !/https?:\/\//.test( url ) ) {
|
|
throw new Error(
|
|
'The survey link URL, i.e. PopupsSurveyLink, must start with https or http.'
|
|
);
|
|
}
|
|
|
|
return $( '<a>' )
|
|
.attr( 'href', url )
|
|
.attr( 'target', '_blank' )
|
|
.attr( 'title', mw.message( 'popups-send-feedback' ) )
|
|
|
|
// Don't leak referrer information or `window.opener` to the survey hosting site. See
|
|
// https://html.spec.whatwg.org/multipage/semantics.html#link-type-noreferrer for more
|
|
// information.
|
|
.attr( 'rel', 'noreferrer' )
|
|
|
|
.addClass( 'mwe-popups-icon mwe-popups-survey-icon' );
|
|
};
|
|
|
|
/**
|
|
* Returns an array of elements to be appended after removing parentheses
|
|
* and making the title in the extract bold.
|
|
*
|
|
* @method getProcessedElements
|
|
* @param {string} extract Should be unescaped
|
|
* @param {string} title Should be unescaped
|
|
* @return {Array} of elements to appended
|
|
*/
|
|
article.getProcessedElements = function ( extract, title ) {
|
|
var regExp, escapedTitle,
|
|
elements = [],
|
|
boldIdentifier = '<bi-' + Math.random() + '>',
|
|
snip = '<snip-' + Math.random() + '>';
|
|
|
|
title = article.removeParensFromText( title );
|
|
title = title.replace( /\s+/g, ' ' ).trim(); // Remove extra white spaces
|
|
escapedTitle = mw.RegExp.escape( title ); // Escape RegExp elements
|
|
regExp = new RegExp( '(^|\\s)(' + escapedTitle + ')(|$)', 'i' );
|
|
|
|
// Remove text in parentheses along with the parentheses
|
|
extract = article.removeParensFromText( extract );
|
|
extract = extract.replace( /\s+/, ' ' ); // Remove extra white spaces
|
|
|
|
// Make title bold in the extract text
|
|
// As the extract is html escaped there can be no such string in it
|
|
// Also, the title is escaped of RegExp elements thus can't have "*"
|
|
extract = extract.replace( regExp, '$1' + snip + boldIdentifier + '$2' + snip + '$3' );
|
|
extract = extract.split( snip );
|
|
|
|
$.each( extract, function ( index, part ) {
|
|
if ( part.indexOf( boldIdentifier ) === 0 ) {
|
|
elements.push( $( '<b>' ).text( part.substring( boldIdentifier.length ) ) );
|
|
} else {
|
|
elements.push( document.createTextNode( part ) );
|
|
}
|
|
} );
|
|
|
|
return elements;
|
|
};
|
|
|
|
/**
|
|
* Removes content in parentheses from a string. Returns the original
|
|
* string as is if the parentheses are unbalanced or out or order. Does not
|
|
* remove extra spaces.
|
|
*
|
|
* @method removeParensFromText
|
|
* @param {string} string
|
|
* @return {string}
|
|
*/
|
|
article.removeParensFromText = function ( string ) {
|
|
var
|
|
ch,
|
|
newString = '',
|
|
level = 0,
|
|
i = 0;
|
|
|
|
for ( i; i < string.length; i++ ) {
|
|
ch = string.charAt( i );
|
|
|
|
if ( ch === ')' && level === 0 ) {
|
|
return string;
|
|
}
|
|
if ( ch === '(' ) {
|
|
level++;
|
|
continue;
|
|
} else if ( ch === ')' ) {
|
|
level--;
|
|
continue;
|
|
}
|
|
if ( level === 0 ) {
|
|
// Remove leading spaces before brackets
|
|
if ( ch === ' ' && string.charAt( i + 1 ) === '(' ) {
|
|
continue;
|
|
}
|
|
newString += ch;
|
|
}
|
|
}
|
|
|
|
return ( level === 0 ) ? newString : string;
|
|
};
|
|
|
|
/**
|
|
* 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 ) {
|
|
var thumbWidth, thumbHeight,
|
|
x, y, width, height, clipPath,
|
|
devicePixelRatio = $.bracketedDevicePixelRatio();
|
|
|
|
// No thumbnail
|
|
if ( !thumbnail ) {
|
|
return $( '<span>' );
|
|
}
|
|
|
|
thumbWidth = thumbnail.width / devicePixelRatio;
|
|
thumbHeight = thumbnail.height / devicePixelRatio;
|
|
|
|
if (
|
|
// Image too small for landscape display
|
|
( !tall && thumbWidth < SIZES.landscapeImage.w ) ||
|
|
// Image too small for portrait display
|
|
( tall && thumbHeight < SIZES.portraitImage.h ) ||
|
|
// These characters in URL that could inject CSS and thus JS
|
|
(
|
|
thumbnail.source.indexOf( '\\' ) > -1 ||
|
|
thumbnail.source.indexOf( '\'' ) > -1 ||
|
|
thumbnail.source.indexOf( '\"' ) > -1
|
|
)
|
|
) {
|
|
return $( '<span>' );
|
|
}
|
|
|
|
if ( tall ) {
|
|
x = ( thumbWidth > SIZES.portraitImage.w ) ?
|
|
( ( thumbWidth - SIZES.portraitImage.w ) / -2 ) :
|
|
( SIZES.portraitImage.w - thumbWidth );
|
|
y = ( thumbHeight > SIZES.portraitImage.h ) ?
|
|
( ( thumbHeight - SIZES.portraitImage.h ) / -2 ) : 0;
|
|
width = SIZES.portraitImage.w;
|
|
height = SIZES.portraitImage.h;
|
|
} else {
|
|
x = 0;
|
|
y = ( thumbHeight > SIZES.landscapeImage.h ) ?
|
|
( ( thumbHeight - SIZES.landscapeImage.h ) / -2 ) : 0;
|
|
width = SIZES.landscapeImage.w + 3;
|
|
height = ( thumbHeight > SIZES.landscapeImage.h ) ?
|
|
SIZES.landscapeImage.h : thumbHeight;
|
|
clipPath = 'mwe-popups-mask';
|
|
}
|
|
return article.createSvgImageThumbnail(
|
|
// FIXME: Not clear why this class is always added even if the popup is not tall
|
|
'mwe-popups-is-not-tall',
|
|
thumbnail.source,
|
|
x,
|
|
y,
|
|
thumbWidth,
|
|
thumbHeight,
|
|
width,
|
|
height,
|
|
clipPath
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Returns the `svg:image` object for thumbnail
|
|
*
|
|
* @method createSvgImageThumbnail
|
|
* @param {string} className
|
|
* @param {string} url
|
|
* @param {number} x
|
|
* @param {number} y
|
|
* @param {number} thumbnailWidth
|
|
* @param {number} thumbnailHeight
|
|
* @param {number} width
|
|
* @param {number} height
|
|
* @param {string} clipPath
|
|
* @return {jQuery}
|
|
*/
|
|
article.createSvgImageThumbnail = function (
|
|
className, url, x, y, thumbnailWidth, thumbnailHeight, width, height, clipPath
|
|
) {
|
|
var $thumbnailSVGImage, $thumbnail,
|
|
ns = 'http://www.w3.org/2000/svg',
|
|
svgElement = article.createSVGTag( 'image' );
|
|
|
|
// certain browsers e.g. ie9 will not correctly set attributes from foreign namespaces (T134979)
|
|
svgElement.setAttributeNS( ns, 'xlink:href', url );
|
|
$thumbnailSVGImage = $( svgElement );
|
|
$thumbnailSVGImage
|
|
.addClass( className )
|
|
.attr( {
|
|
x: x,
|
|
y: y,
|
|
width: thumbnailWidth,
|
|
height: thumbnailHeight,
|
|
'clip-path': 'url(#' + clipPath + ')'
|
|
} );
|
|
|
|
$thumbnail = $( '<svg>' )
|
|
.attr( {
|
|
xmlns: ns,
|
|
width: width,
|
|
height: height
|
|
} )
|
|
.append( $thumbnailSVGImage );
|
|
|
|
return $thumbnail;
|
|
};
|
|
|
|
/**
|
|
* Positions the popup based on the mouse position and popup size
|
|
* Default popup positioning is below and to the right of the mouse or link,
|
|
* unless flippedX or flippedY is true. flippedX and flippedY are cached.
|
|
*
|
|
* @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' ),
|
|
flippedX = false,
|
|
flippedY = false,
|
|
settings = mw.popups.render.cache[ href ].settings,
|
|
offsetTop = ( event.pageY ) ? // If it was a mouse event
|
|
// Position according to mouse
|
|
// Since client rectangles are relative to the viewport,
|
|
// take scroll position into account.
|
|
getClosestYPosition(
|
|
event.pageY - $window.scrollTop(),
|
|
link.get( 0 ).getClientRects(),
|
|
false
|
|
) + $window.scrollTop() + SIZES.pokeySize :
|
|
// Position according to link position or size
|
|
link.offset().top + link.height() + SIZES.pokeySize,
|
|
clientTop = ( event.clientY ) ?
|
|
event.clientY :
|
|
offsetTop,
|
|
offsetLeft = ( event.pageX ) ?
|
|
event.pageX :
|
|
link.offset().left;
|
|
|
|
// X Flip
|
|
if ( offsetLeft > ( $( window ).width() / 2 ) ) {
|
|
offsetLeft += ( !event.pageX ) ? link.width() : 0;
|
|
offsetLeft -= ( !settings.tall ) ?
|
|
SIZES.portraitPopupWidth :
|
|
SIZES.landscapePopupWidth;
|
|
flippedX = true;
|
|
}
|
|
|
|
if ( event.pageX ) {
|
|
offsetLeft += ( flippedX ) ? 20 : -20;
|
|
}
|
|
|
|
mw.popups.render.cache[ href ].settings.flippedX = flippedX;
|
|
|
|
// Y Flip
|
|
if ( clientTop > ( $( window ).height() / 2 ) ) {
|
|
flippedY = true;
|
|
|
|
// Change the Y position to the top of the link
|
|
if ( event.pageY ) {
|
|
// Since client rectangles are relative to the viewport,
|
|
// take scroll position into account.
|
|
offsetTop = getClosestYPosition(
|
|
event.pageY - $window.scrollTop(),
|
|
link.get( 0 ).getClientRects(),
|
|
true
|
|
) + $window.scrollTop() + 2 * SIZES.pokeySize;
|
|
}
|
|
}
|
|
|
|
mw.popups.render.cache[ href ].settings.flippedY = flippedY;
|
|
|
|
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,
|
|
flippedY = cache.settings.flippedY,
|
|
flippedX = cache.settings.flippedX;
|
|
|
|
if ( flippedY ) {
|
|
classes.push( 'mwe-popups-fade-in-down' );
|
|
} else {
|
|
classes.push( 'mwe-popups-fade-in-up' );
|
|
}
|
|
|
|
if ( flippedY && flippedX ) {
|
|
classes.push( 'flipped_x_y' );
|
|
}
|
|
|
|
if ( flippedY && !flippedX ) {
|
|
classes.push( 'flipped_y' );
|
|
}
|
|
|
|
if ( flippedX && !flippedY ) {
|
|
classes.push( 'flipped_x' );
|
|
}
|
|
|
|
if ( ( !thumbnail || tall ) && !flippedY ) {
|
|
classes.push( 'mwe-popups-no-image-tri' );
|
|
}
|
|
|
|
if ( ( thumbnail && !tall ) && !flippedY ) {
|
|
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
|
|
* @param {Object} logData data to be logged
|
|
*/
|
|
article.processPopup = function ( link, logData ) {
|
|
var
|
|
cache = mw.popups.render.cache [ link.attr( 'href' ) ],
|
|
popup = mw.popups.$popup,
|
|
tall = cache.settings.tall,
|
|
thumbnail = cache.settings.thumbnail,
|
|
flippedY = cache.settings.flippedY,
|
|
flippedX = cache.settings.flippedX;
|
|
|
|
popup.find( '.mwe-popups-settings-icon' ).click( function () {
|
|
delete logData.pageTitleHover;
|
|
delete logData.namespaceIdHover;
|
|
|
|
mw.popups.settings.open( $.extend( {}, logData ) );
|
|
|
|
mw.track( 'ext.popups.schemaPopups', $.extend( logData, {
|
|
action: 'tapped settings cog',
|
|
totalInteractionTime: Math.round( mw.now() - logData.dwellStartTime )
|
|
} ) );
|
|
} );
|
|
|
|
if ( !flippedY && !tall && cache.settings.thumbnail.height < SIZES.landscapeImage.h ) {
|
|
$( '.mwe-popups-extract' ).css(
|
|
'margin-top',
|
|
cache.settings.thumbnail.height - SIZES.pokeySize
|
|
);
|
|
}
|
|
|
|
if ( flippedY ) {
|
|
popup.css( {
|
|
top: popup.offset().top - popup.outerHeight()
|
|
} );
|
|
}
|
|
|
|
if ( flippedY && thumbnail ) {
|
|
mw.popups.$popup
|
|
.find( 'image' )[ 0 ]
|
|
.setAttribute( 'clip-path', '' );
|
|
}
|
|
|
|
if ( flippedY && flippedX && thumbnail && tall ) {
|
|
mw.popups.$popup
|
|
.find( 'image' )[ 0 ]
|
|
.setAttribute( 'clip-path', 'url(#mwe-popups-landscape-mask-flip)' );
|
|
}
|
|
|
|
if ( flippedX && !flippedY && thumbnail && !tall ) {
|
|
mw.popups.$popup
|
|
.find( 'image' )[ 0 ]
|
|
.setAttribute( 'clip-path', 'url(#mwe-popups-mask-flip)' );
|
|
}
|
|
|
|
if ( flippedX && !flippedY && thumbnail && tall ) {
|
|
mw.popups.$popup
|
|
.removeClass( 'mwe-popups-no-image-tri' )
|
|
.find( 'image' )[ 0 ]
|
|
.setAttribute( 'clip-path', 'url(#mwe-popups-landscape-mask)' );
|
|
}
|
|
};
|
|
|
|
mw.popups.render.renderers.article = article;
|
|
|
|
/**
|
|
* Given the rectangular box(es) find the 'y' boundary of the closest
|
|
* rectangle to the point 'y'. The point 'y' is the location of the mouse
|
|
* on the 'y' axis and the rectangular box(es) are the borders of the
|
|
* element over which the mouse is located. There will be more than one
|
|
* rectangle in case the element spans multiple lines.
|
|
* In the majority of cases the mouse pointer will be inside a rectangle.
|
|
* However, some browsers (i.e. Chrome) trigger a hover action even when
|
|
* the mouse pointer is just outside a bounding rectangle. That's why
|
|
* we need to look at all rectangles and not just the rectangle that
|
|
* encloses the point.
|
|
*
|
|
* @param {number} y the point for which the closest location is being
|
|
* looked for
|
|
* @param {ClientRectList} rects list of rectangles defined by four edges
|
|
* @param {boolean} [isTop] should the resulting rectangle's top 'y'
|
|
* boundary be returned. By default the bottom 'y' value is returned.
|
|
* @return {number}
|
|
*/
|
|
function getClosestYPosition( y, rects, isTop ) {
|
|
var result,
|
|
deltaY,
|
|
minY = null;
|
|
|
|
$.each( rects, function ( i, rect ) {
|
|
deltaY = Math.abs( y - rect.top + y - rect.bottom );
|
|
|
|
if ( minY === null || minY > deltaY ) {
|
|
minY = deltaY;
|
|
// Make sure the resulting point is at or outside the rectangle
|
|
// boundaries.
|
|
result = ( isTop ) ? Math.floor( rect.top ) : Math.ceil( rect.bottom );
|
|
}
|
|
} );
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Aborts any pending ajax requests
|
|
*/
|
|
mw.popups.render.abortCurrentRequest = function () {
|
|
if ( currentRequest ) {
|
|
currentRequest.abort();
|
|
currentRequest = undefined;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Expose for tests
|
|
*/
|
|
mw.popups.render.getClosestYPosition = getClosestYPosition;
|
|
|
|
/**
|
|
* Remove ellipsis if exists at the end
|
|
*/
|
|
function removeEllipsis( text ) {
|
|
return text.replace( /\.\.\.$/, '' );
|
|
}
|
|
|
|
} )( jQuery, mediaWiki );
|