mediawiki-extensions-Popups/src/ui/thumbnail.js
Stephen Niedzielski ab7a5808ef Hygiene: update JSDoc boxed and JQuery types
Although Popups only uses JSDocs at this time which seemingly doesn't
care about casing[1], we should endeavor to use the proper return types.

This patch lowercases typing to indicate primitive / boxed type as
appropriate.[2] As a special case, function types are uppercased for
compatibility with TypeScript type checking.

Lastly, JQuery types are of type "JQuery". The global JQuery object's
identifier is "jQuery". This patch uppercases J's where appropriate.

[0] https://github.com/jsdoc3/jsdoc/issues/1046#issuecomment-126477791

[1] find src tests -iname \*.js|
    xargs -rd\\n sed -ri '
      s%\{\s*([?!])?(number|string|boolean|null|undefined)%{\1\L\2%gi;
      s%\{\s*([?!])?(function|object)%{\1\u\2%gi;
      s%\{\s*([?!])?jquery%{\1JQuery%gi
    '

Change-Id: I771bdbb69dc978796a331998c0657622ac39c449
2018-07-17 08:20:08 -05:00

173 lines
4.8 KiB
JavaScript

/**
* @module thumbnail
*/
export const SIZES = {
portraitImage: {
h: 250, // Exact height
w: 203 // Max width
},
landscapeImage: {
h: 200, // Max height
w: 320 // Exact Width
}
};
const $ = jQuery;
/**
* @typedef {Object} ext.popups.Thumbnail
* @property {JQuery} el
* @property {boolean} isTall Whether or not the thumbnail is portrait
* @property {number} width
* @property {number} height
* @property {boolean} isNarrow whether the thumbnail is portrait and also
* thinner than the default portrait thumbnail width
* (as defined in SIZES.portraitImage.w)
* @property {number} offset in pixels between the thumbnail width and the
* standard portrait thumbnail width (as defined in SIZES.portraitImage.w)
*/
/**
* Creates a thumbnail from the representation of a thumbnail returned by the
* PageImages MediaWiki API query module.
*
* If there's no thumbnail, the thumbnail is too small, or the thumbnail's URL
* contains characters that could be used to perform an
* [XSS attack via CSS](https://www.owasp.org/index.php/Testing_for_CSS_Injection_(OTG-CLIENT-005)),
* then `null` is returned.
*
* Extracted from `mw.popups.renderer.article.createThumbnail`.
*
* @param {Object} rawThumbnail
* @return {ext.popups.Thumbnail|null}
*/
export function createThumbnail( rawThumbnail ) {
const devicePixelRatio = $.bracketedDevicePixelRatio();
if ( !rawThumbnail ) {
return null;
}
const tall = rawThumbnail.width < rawThumbnail.height;
const thumbWidth = rawThumbnail.width / devicePixelRatio;
const thumbHeight = rawThumbnail.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
(
rawThumbnail.source.indexOf( '\\' ) > -1 ||
rawThumbnail.source.indexOf( '\'' ) > -1 ||
rawThumbnail.source.indexOf( '"' ) > -1
)
) {
return null;
}
let x, y, width, height;
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;
// Special handling for thin tall images
// https://phabricator.wikimedia.org/T192928#4312088
if ( thumbWidth < width ) {
x = 0;
width = thumbWidth;
}
} else {
x = 0;
y = ( thumbHeight > SIZES.landscapeImage.h ) ?
( ( thumbHeight - SIZES.landscapeImage.h ) / -2 ) : 0;
width = SIZES.landscapeImage.w;
height = ( thumbHeight > SIZES.landscapeImage.h ) ?
SIZES.landscapeImage.h : thumbHeight;
}
const isNarrow = tall && thumbWidth < SIZES.portraitImage.w;
return {
el: createThumbnailElement(
tall ? 'mwe-popups-is-tall' : 'mwe-popups-is-not-tall',
rawThumbnail.source,
x,
y,
thumbWidth,
thumbHeight,
width,
height
),
isTall: tall,
isNarrow,
offset: isNarrow ? SIZES.portraitImage.w - thumbWidth : 0,
width: thumbWidth,
height: thumbHeight
};
}
/**
* Creates the SVG image element that represents the thumbnail.
*
* This function is distinct from `createThumbnail` as it abstracts away some
* browser issues that are uncovered when manipulating elements across
* namespaces.
*
* @param {string} className
* @param {string} url
* @param {number} x
* @param {number} y
* @param {number} thumbnailWidth
* @param {number} thumbnailHeight
* @param {number} width
* @param {number} height
* @return {JQuery}
*/
export function createThumbnailElement(
className, url, x, y, thumbnailWidth, thumbnailHeight, width, height
) {
const nsSvg = 'http://www.w3.org/2000/svg',
nsXlink = 'http://www.w3.org/1999/xlink';
// We want to visually separate the image from the summary
// Given we use an SVG mask, we cannot rely on border to do this
// and instead must insert a polyline element to visually separate
const line = document.createElementNS( nsSvg, 'polyline' );
const isTall = className.indexOf( 'not-tall' ) === -1;
const points = isTall ? [ 0, 0, 0, height ] :
[ 0, height - 1, width, height - 1 ];
line.setAttribute( 'stroke', 'rgba(0,0,0,0.1)' );
line.setAttribute( 'points', points.join( ' ' ) );
line.setAttribute( 'stroke-width', 1 );
const $thumbnailSVGImage = $( document.createElementNS( nsSvg, 'image' ) );
$thumbnailSVGImage[ 0 ].setAttributeNS( nsXlink, 'href', url );
$thumbnailSVGImage
.addClass( className )
.attr( {
x,
y,
width: thumbnailWidth,
height: thumbnailHeight
} );
const $thumbnail = $( document.createElementNS( nsSvg, 'svg' ) )
.attr( {
xmlns: nsSvg,
width,
height
} )
.append( $thumbnailSVGImage );
$thumbnail.append( line );
return $thumbnail;
}