Jhernandez has uploaded a new change for review. (
https://gerrit.wikimedia.org/r/337830 )
Change subject: Hygiene: Remove unnecessary IIFE in renderer.js
......................................................................
Hygiene: Remove unnecessary IIFE in renderer.js
Change-Id: I89a1ac2205385db8e2f2c040ac22d2f4de793a18
---
M src/renderer.js
1 file changed, 676 insertions(+), 679 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Popups
refs/changes/30/337830/1
diff --git a/src/renderer.js b/src/renderer.js
index e90d58c..9e01209 100644
--- a/src/renderer.js
+++ b/src/renderer.js
@@ -1,690 +1,687 @@
-( function ( mw, $ ) {
-
- var isSafari = navigator.userAgent.match( /Safari/ ) !== null,
- wait = require( './wait' ),
- SIZES = {
- portraitImage: {
- h: 250, // Exact height
- w: 203 // Max width
- },
- landscapeImage: {
- h: 200, // Max height
- w: 300 // Exact Width
- },
- landscapePopupWidth: 450,
- portraitPopupWidth: 300,
- pokeySize: 8 // Height of the pokey.
+var mw = window.mediaWiki,
+ isSafari = navigator.userAgent.match( /Safari/ ) !== null,
+ wait = require( './wait' ),
+ SIZES = {
+ portraitImage: {
+ h: 250, // Exact height
+ w: 203 // Max width
},
- $window = $( window );
+ landscapeImage: {
+ h: 200, // Max height
+ w: 300 // Exact Width
+ },
+ landscapePopupWidth: 450,
+ portraitPopupWidth: 300,
+ pokeySize: 8 // Height of the pokey.
+ },
+ $window = $( window );
- /**
- * Extracted from `mw.popups.createSVGMasks`.
- */
- function createPokeyMasks() {
- $( '<div>' )
- .attr( 'id', 'mwe-popups-svg' )
- .html(
- '<svg width="0" height="0">' +
- '<defs>' +
- '<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>' +
- '<clippath
id="mwe-popups-landscape-mask-flip">' +
- '<polygon points="0 0,
1000 0, 1000 243, 190 243, 182 250, 174 243, 0 243"/>' +
- '</clippath>' +
- '</defs>' +
- '</svg>'
- )
- .appendTo( document.body );
- }
+/**
+ * Extracted from `mw.popups.createSVGMasks`.
+ */
+function createPokeyMasks() {
+ $( '<div>' )
+ .attr( 'id', 'mwe-popups-svg' )
+ .html(
+ '<svg width="0" height="0">' +
+ '<defs>' +
+ '<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>' +
+ '<clippath
id="mwe-popups-landscape-mask-flip">' +
+ '<polygon points="0 0, 1000 0,
1000 243, 190 243, 182 250, 174 243, 0 243"/>' +
+ '</clippath>' +
+ '</defs>' +
+ '</svg>'
+ )
+ .appendTo( document.body );
+}
- /**
- * Initializes the renderer.
- */
- function init() {
- createPokeyMasks();
- }
+/**
+ * Initializes the renderer.
+ */
+function init() {
+ createPokeyMasks();
+}
- /**
- * The model of how a view is rendered, which is constructed from a
response
- * from the gateway.
- *
- * TODO: Rename `isTall` to `isPortrait`.
- *
- * @typedef {Object} ext.popups.Preview
- * @property {jQuery} el
- * @property {Boolean} hasThumbnail
- * @property {Object} thumbnail
- * @property {Boolean} isTall Sugar around
- * `preview.hasThumbnail && thumbnail.isTall`
- */
+/**
+ * The model of how a view is rendered, which is constructed from a response
+ * from the gateway.
+ *
+ * TODO: Rename `isTall` to `isPortrait`.
+ *
+ * @typedef {Object} ext.popups.Preview
+ * @property {jQuery} el
+ * @property {Boolean} hasThumbnail
+ * @property {Object} thumbnail
+ * @property {Boolean} isTall Sugar around
+ * `preview.hasThumbnail && thumbnail.isTall`
+ */
- /**
- * Renders a preview given data from the {@link gateway
ext.popups.Gateway}.
- * The preview is rendered and added to the DOM but will remain hidden
until
- * the `show` method is called.
- *
- * Previews are rendered at:
- *
- * # The position of the mouse when the user dwells on the link with
their
- * mouse.
- * # The centermost point of the link when the user dwells on the link
with
- * their keboard or other assistive device.
- *
- * Since the content of the preview doesn't change but its position
might, we
- * distinguish between "rendering" - generating HTML from a MediaWiki
API
- * response - and "showing/hiding" - positioning the layout and
changing its
- * orientation, if necessary.
- *
- * @param {ext.popups.PreviewModel} model
- * @return {ext.popups.Preview}
- */
- function render( model ) {
- var preview = model.extract === undefined ? createEmptyPreview(
model ) : createPreview( model );
+/**
+ * Renders a preview given data from the {@link gateway ext.popups.Gateway}.
+ * The preview is rendered and added to the DOM but will remain hidden until
+ * the `show` method is called.
+ *
+ * Previews are rendered at:
+ *
+ * # The position of the mouse when the user dwells on the link with their
+ * mouse.
+ * # The centermost point of the link when the user dwells on the link with
+ * their keboard or other assistive device.
+ *
+ * Since the content of the preview doesn't change but its position might, we
+ * distinguish between "rendering" - generating HTML from a MediaWiki API
+ * response - and "showing/hiding" - positioning the layout and changing its
+ * orientation, if necessary.
+ *
+ * @param {ext.popups.PreviewModel} model
+ * @return {ext.popups.Preview}
+ */
+function render( model ) {
+ var preview = model.extract === undefined ? createEmptyPreview( model )
: createPreview( model );
- return {
+ return {
- /**
- * Shows the preview given an event representing the
user's interaction
- * with the active link, e.g. an instance of
- *
[MouseEvent](https://developer.mozilla.org/en/docs/Web/API/MouseEvent).
- *
- * See `show` for more detail.
- *
- * @param {Event} event
- * @param {Object} boundActions The
- * [bound action
creators](http://redux.js.org/docs/api/bindActionCreators.html)
- * that were (likely) created in [boot.js](./boot.js).
- * @return {jQuery.Promise}
- */
- show: function ( event, boundActions ) {
- return show( preview, event, boundActions );
- },
+ /**
+ * Shows the preview given an event representing the user's
interaction
+ * with the active link, e.g. an instance of
+ *
[MouseEvent](https://developer.mozilla.org/en/docs/Web/API/MouseEvent).
+ *
+ * See `show` for more detail.
+ *
+ * @param {Event} event
+ * @param {Object} boundActions The
+ * [bound action
creators](http://redux.js.org/docs/api/bindActionCreators.html)
+ * that were (likely) created in [boot.js](./boot.js).
+ * @return {jQuery.Promise}
+ */
+ show: function ( event, boundActions ) {
+ return show( preview, event, boundActions );
+ },
- /**
- * Hides the preview.
- *
- * See `hide` for more detail.
- *
- * @return {jQuery.Promise}
- */
- hide: function () {
- return hide( preview );
- }
- };
- }
-
- /**
- * Creates an instance of the DTO backing a preview.
- *
- * @param {ext.popups.PreviewModel} model
- * @return {ext.popups.Preview}
- */
- function createPreview( model ) {
- var templateData,
- thumbnail = createThumbnail( model.thumbnail ),
- hasThumbnail = thumbnail !== null,
-
- // FIXME: This should probably be moved into the
gateway as we'll soon be
- // fetching HTML from the API. See
- // https://phabricator.wikimedia.org/T141651 for more
detail.
- extract = renderExtract( model.extract, model.title ),
-
- $el;
-
- templateData = $.extend( {}, model, {
- hasThumbnail: hasThumbnail
- } );
-
- $el = mw.template.get( 'ext.popups', 'preview.mustache' )
- .render( templateData );
-
- if ( hasThumbnail ) {
- $el.find( '.mwe-popups-discreet' ).append( thumbnail.el
);
+ /**
+ * Hides the preview.
+ *
+ * See `hide` for more detail.
+ *
+ * @return {jQuery.Promise}
+ */
+ hide: function () {
+ return hide( preview );
}
-
- if ( extract.length ) {
- $el.find( '.mwe-popups-extract' ).append( extract );
- }
-
- return {
- el: $el,
- hasThumbnail: hasThumbnail,
- thumbnail: thumbnail,
- isTall: hasThumbnail && thumbnail.isTall
- };
- }
-
- /**
- * Creates an instance of the DTO backing a preview. In this case the
DTO
- * represents a generic preview, which covers the following scenarios:
- *
- * * The page doesn't exist, i.e. the user hovered over a redlink or a
- * redirect to a page that doesn't exist.
- * * The page doesn't have a viable extract.
- *
- * @param {ext.popups.PreviewModel} model
- * @return {ext.popups.Preview}
- */
- function createEmptyPreview( model ) {
- var templateData,
- $el;
-
- templateData = $.extend( {}, model, {
- extractMsg: mw.msg( 'popups-preview-no-preview' ),
- readMsg: mw.msg( 'popups-preview-footer-read' )
- } );
-
- $el = mw.template.get( 'ext.popups', 'preview-empty.mustache' )
- .render( templateData );
-
- return {
- el: $el,
- hasThumbnail: false,
- isTall: false
- };
- }
-
- /**
- * Converts the extract into a list of elements, which correspond to
fragments
- * of the extract. Fragements that match the title verbatim are wrapped
in a
- * `<b>` element.
- *
- * Using the bolded elements of the extract of the page directly is
covered by
- * [T141651](https://phabricator.wikimedia.org/T141651).
- *
- * Extracted from `mw.popups.renderer.article.getProcessedElements`.
- *
- * @param {String} extract
- * @param {String} title
- * @return {Array}
- */
- function renderExtract( extract, title ) {
- var regExp, escapedTitle,
- elements = [],
- boldIdentifier = '<bi-' + Math.random() + '>',
- snip = '<snip-' + Math.random() + '>';
-
- 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 = 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;
- }
-
- /**
- * Shows the preview.
- *
- * Extracted from `mw.popups.render.openPopup`.
- *
- * TODO: From the perspective of the client, there's no need to
distinguish
- * between renderering and showing a preview. Merge #render and
Preview#show.
- *
- * @param {ext.popups.Preview} preview
- * @param {Event} event
- * @param {ext.popups.PreviewBehavior} behavior
- * @return {jQuery.Promise} A promise that resolves when the promise
has faded
- * in
- */
- function show( preview, event, behavior ) {
- var layout = createLayout( preview, event );
-
- preview.el.appendTo( document.body );
-
- // Hack to "refresh" the SVG so that it's displayed.
- //
- // Elements get added to the DOM and not to the screen because
of different
- // namespaces of HTML and SVG.
- //
- // See http://stackoverflow.com/a/13654655/366138 for more
detail.
- //
- // TODO: Find out how early on in the render that this could be
done, e.g.
- // createThumbnail?
- preview.el.html( preview.el.html() );
-
- layoutPreview( preview, layout );
-
- preview.el.hover( behavior.previewDwell,
behavior.previewAbandon );
-
- preview.el.find( '.mwe-popups-settings-icon' )
- .attr( 'href', behavior.settingsUrl )
- .click( behavior.showSettings );
-
- preview.el.show();
-
- return wait( 200 )
- .then( behavior.previewShow );
- }
-
- /**
- * Extracted from `mw.popups.render.closePopup`.
- *
- * @param {ext.popups.Preview} preview
- * @return {jQuery.Promise} A promise that resolves when the preview
has faded
- * out
- */
- function hide( preview ) {
- var fadeInClass,
- fadeOutClass;
-
- // FIXME: This method clearly needs access to the layout of the
preview.
- fadeInClass = ( preview.el.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';
-
- preview.el
- .removeClass( fadeInClass )
- .addClass( fadeOutClass );
-
- return wait( 150 ).then( function () {
- preview.el.remove();
- } );
- }
-
- /**
- * @typedef {Object} ext.popups.Thumbnail
- * @property {Element} el
- * @property {Boolean} isTall Whether or not the thumbnail is portrait
- */
-
- /**
- * 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}
- */
- function createThumbnail( rawThumbnail ) {
- var tall, thumbWidth, thumbHeight,
- x, y, width, height, clipPath,
- devicePixelRatio = $.bracketedDevicePixelRatio();
-
- if ( !rawThumbnail ) {
- return null;
- }
-
- tall = rawThumbnail.width < rawThumbnail.height;
- thumbWidth = rawThumbnail.width / devicePixelRatio;
- 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;
- }
-
- 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 {
- el: createThumbnailElement(
- tall ? 'mwe-popups-is-tall' :
'mwe-popups-is-not-tall',
- rawThumbnail.source,
- x,
- y,
- thumbWidth,
- thumbHeight,
- width,
- height,
- clipPath
- ),
- isTall: tall,
- 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
- * @param {String} clipPath
- * @return {jQuery}
- */
- function createThumbnailElement( className, url, x, y, thumbnailWidth,
thumbnailHeight, width, height, clipPath ) {
- var $thumbnailSVGImage, $thumbnail,
- ns = 'http://www.w3.org/2000/svg',
-
- // Use createElementNS to create the svg:image tag as
jQuery uses
- // createElement instead. Some browsers mistakenly map
the image tag to
- // img tag.
- svgElement = document.createElementNS(
'http://www.w3.org/2000/svg', 'image' );
-
- $thumbnailSVGImage = $( svgElement );
- $thumbnailSVGImage
- .addClass( className )
- .attr( {
- x: x,
- y: y,
- width: thumbnailWidth,
- height: thumbnailHeight,
- 'clip-path': 'url(#' + clipPath + ')'
- } );
-
- // Certain browsers, e.g. IE9, will not correctly set
attributes from
- // foreign namespaces using Element#setAttribute (see T134979).
Apart from
- // Safari, all supported browsers can set them using
Element#setAttributeNS
- // (see T134979).
- if ( isSafari ) {
- svgElement.setAttribute( 'xlink:href', url );
- } else {
- svgElement.setAttributeNS( ns, 'xlink:href', url );
- }
- $thumbnail = $( '<svg>' )
- .attr( {
- xmlns: ns,
- width: width,
- height: height
- } )
- .append( $thumbnailSVGImage );
-
- return $thumbnail;
- }
-
- /**
- * Represents the layout of a preview, which consists of a position
(`offset`)
- * and whether or not the preview should be flipped horizontally or
- * vertically (`flippedX` and `flippedY` respectively).
- *
- * @typedef {Object} ext.popups.PreviewLayout
- * @property {Object} offset
- * @property {Boolean} flippedX
- * @property {Boolean} flippedY
- */
-
- /**
- * Extracted from `mw.popups.renderer.article.getOffset`.
- *
- * @param {ext.popups.Preview} preview
- * @param {Object} event
- * @return {ext.popups.PreviewLayout}
- */
- function createLayout( preview, event ) {
- var flippedX = false,
- flippedY = false,
- link = $( event.target ),
- 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 -= !preview.isTall ?
- SIZES.portraitPopupWidth :
- SIZES.landscapePopupWidth;
- flippedX = true;
- }
-
- if ( event.pageX ) {
- offsetLeft += ( flippedX ) ? 20 : -20;
- }
-
- // Y Flip
- if ( clientTop > ( $window.height() / 2 ) ) {
- flippedY = true;
-
- // Mirror the positioning of the preview when there's
no "Y flip": rest
- // the pokey on the edge of the link's bounding
rectangle. In this case
- // the edge is the top-most.
- offsetTop = link.offset().top - SIZES.pokeySize;
-
- // 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();
- }
- }
-
- return {
- offset: {
- top: offsetTop,
- left: offsetLeft
- },
- flippedX: flippedX,
- flippedY: flippedY
- };
- }
-
- /**
- * Generates a list of declarative CSS classes that represent the
layout of
- * the preview.
- *
- * @param {ext.popups.Preview} preview
- * @param {ext.popups.PreviewLayout} layout
- * @return {String[]}
- */
- function getClasses( preview, layout ) {
- var classes = [];
-
- if ( layout.flippedY ) {
- classes.push( 'mwe-popups-fade-in-down' );
- } else {
- classes.push( 'mwe-popups-fade-in-up' );
- }
-
- if ( layout.flippedY && layout.flippedX ) {
- classes.push( 'flipped_x_y' );
- }
-
- if ( layout.flippedY && !layout.flippedX ) {
- classes.push( 'flipped_y' );
- }
-
- if ( layout.flippedX && !layout.flippedY ) {
- classes.push( 'flipped_x' );
- }
-
- if ( ( !preview.hasThumbnail || preview.isTall ) &&
!layout.flippedY ) {
- classes.push( 'mwe-popups-no-image-tri' );
- }
-
- if ( ( preview.hasThumbnail && !preview.isTall ) &&
!layout.flippedY ) {
- classes.push( 'mwe-popups-image-tri' );
- }
-
- if ( preview.isTall ) {
- classes.push( 'mwe-popups-is-tall' );
- } else {
- classes.push( 'mwe-popups-is-not-tall' );
- }
-
- return classes;
- }
-
- /**
- * Lays out the preview given the layout.
- *
- * If the preview should be oriented differently, then the pokey is
updated,
- * e.g. if the preview should be flipped vertically, then the pokey is
- * removed.
- *
- * If the thumbnail is landscape and isn't the full height of the
thumbnail
- * container, then pull the extract up to keep whitespace consistent
across
- * previews.
- *
- * @param {ext.popups.Preview} preview
- * @param {ext.popups.PreviewLayout} layout
- */
- function layoutPreview( preview, layout ) {
- var popup = preview.el,
- isTall = preview.isTall,
- hasThumbnail = preview.hasThumbnail,
- thumbnail = preview.thumbnail,
- flippedY = layout.flippedY,
- flippedX = layout.flippedX,
- offsetTop = layout.offset.top;
-
- if ( !flippedY && !isTall && hasThumbnail && thumbnail.height <
SIZES.landscapeImage.h ) {
- $( '.mwe-popups-extract' ).css(
- 'margin-top',
- thumbnail.height - SIZES.pokeySize
- );
- }
-
- popup.addClass( getClasses( preview, layout ).join( ' ' ) );
-
- if ( flippedY ) {
- offsetTop -= popup.outerHeight();
- }
-
- popup.css( {
- top: offsetTop,
- left: layout.offset.left + 'px'
- } );
-
- if ( flippedY && hasThumbnail ) {
- popup.find( 'image' )[ 0 ]
- .setAttribute( 'clip-path', '' );
- }
-
- if ( flippedY && flippedX && hasThumbnail && isTall ) {
- popup.find( 'image' )[ 0 ]
- .setAttribute( 'clip-path',
'url(#mwe-popups-landscape-mask-flip)' );
- }
-
- if ( flippedX && !flippedY && hasThumbnail && !isTall ) {
- popup.find( 'image' )[ 0 ]
- .setAttribute( 'clip-path',
'url(#mwe-popups-mask-flip)' );
- }
-
- if ( flippedX && !flippedY && hasThumbnail && isTall ) {
- popup.removeClass( 'mwe-popups-no-image-tri' )
- .find( 'image' )[ 0 ]
- .setAttribute( 'clip-path',
'url(#mwe-popups-landscape-mask)' );
- }
- }
-
- /**
- * 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;
- }
-
- module.exports = {
- render: render,
- init: init
};
+}
-}( mediaWiki, jQuery ) );
+/**
+ * Creates an instance of the DTO backing a preview.
+ *
+ * @param {ext.popups.PreviewModel} model
+ * @return {ext.popups.Preview}
+ */
+function createPreview( model ) {
+ var templateData,
+ thumbnail = createThumbnail( model.thumbnail ),
+ hasThumbnail = thumbnail !== null,
+
+ // FIXME: This should probably be moved into the gateway as
we'll soon be
+ // fetching HTML from the API. See
+ // https://phabricator.wikimedia.org/T141651 for more detail.
+ extract = renderExtract( model.extract, model.title ),
+
+ $el;
+
+ templateData = $.extend( {}, model, {
+ hasThumbnail: hasThumbnail
+ } );
+
+ $el = mw.template.get( 'ext.popups', 'preview.mustache' )
+ .render( templateData );
+
+ if ( hasThumbnail ) {
+ $el.find( '.mwe-popups-discreet' ).append( thumbnail.el );
+ }
+
+ if ( extract.length ) {
+ $el.find( '.mwe-popups-extract' ).append( extract );
+ }
+
+ return {
+ el: $el,
+ hasThumbnail: hasThumbnail,
+ thumbnail: thumbnail,
+ isTall: hasThumbnail && thumbnail.isTall
+ };
+}
+
+/**
+ * Creates an instance of the DTO backing a preview. In this case the DTO
+ * represents a generic preview, which covers the following scenarios:
+ *
+ * * The page doesn't exist, i.e. the user hovered over a redlink or a
+ * redirect to a page that doesn't exist.
+ * * The page doesn't have a viable extract.
+ *
+ * @param {ext.popups.PreviewModel} model
+ * @return {ext.popups.Preview}
+ */
+function createEmptyPreview( model ) {
+ var templateData,
+ $el;
+
+ templateData = $.extend( {}, model, {
+ extractMsg: mw.msg( 'popups-preview-no-preview' ),
+ readMsg: mw.msg( 'popups-preview-footer-read' )
+ } );
+
+ $el = mw.template.get( 'ext.popups', 'preview-empty.mustache' )
+ .render( templateData );
+
+ return {
+ el: $el,
+ hasThumbnail: false,
+ isTall: false
+ };
+}
+
+/**
+ * Converts the extract into a list of elements, which correspond to fragments
+ * of the extract. Fragements that match the title verbatim are wrapped in a
+ * `<b>` element.
+ *
+ * Using the bolded elements of the extract of the page directly is covered by
+ * [T141651](https://phabricator.wikimedia.org/T141651).
+ *
+ * Extracted from `mw.popups.renderer.article.getProcessedElements`.
+ *
+ * @param {String} extract
+ * @param {String} title
+ * @return {Array}
+ */
+function renderExtract( extract, title ) {
+ var regExp, escapedTitle,
+ elements = [],
+ boldIdentifier = '<bi-' + Math.random() + '>',
+ snip = '<snip-' + Math.random() + '>';
+
+ 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 = 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;
+}
+
+/**
+ * Shows the preview.
+ *
+ * Extracted from `mw.popups.render.openPopup`.
+ *
+ * TODO: From the perspective of the client, there's no need to distinguish
+ * between renderering and showing a preview. Merge #render and Preview#show.
+ *
+ * @param {ext.popups.Preview} preview
+ * @param {Event} event
+ * @param {ext.popups.PreviewBehavior} behavior
+ * @return {jQuery.Promise} A promise that resolves when the promise has faded
+ * in
+ */
+function show( preview, event, behavior ) {
+ var layout = createLayout( preview, event );
+
+ preview.el.appendTo( document.body );
+
+ // Hack to "refresh" the SVG so that it's displayed.
+ //
+ // Elements get added to the DOM and not to the screen because of
different
+ // namespaces of HTML and SVG.
+ //
+ // See http://stackoverflow.com/a/13654655/366138 for more detail.
+ //
+ // TODO: Find out how early on in the render that this could be done,
e.g.
+ // createThumbnail?
+ preview.el.html( preview.el.html() );
+
+ layoutPreview( preview, layout );
+
+ preview.el.hover( behavior.previewDwell, behavior.previewAbandon );
+
+ preview.el.find( '.mwe-popups-settings-icon' )
+ .attr( 'href', behavior.settingsUrl )
+ .click( behavior.showSettings );
+
+ preview.el.show();
+
+ return wait( 200 )
+ .then( behavior.previewShow );
+}
+
+/**
+ * Extracted from `mw.popups.render.closePopup`.
+ *
+ * @param {ext.popups.Preview} preview
+ * @return {jQuery.Promise} A promise that resolves when the preview has faded
+ * out
+ */
+function hide( preview ) {
+ var fadeInClass,
+ fadeOutClass;
+
+ // FIXME: This method clearly needs access to the layout of the preview.
+ fadeInClass = ( preview.el.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';
+
+ preview.el
+ .removeClass( fadeInClass )
+ .addClass( fadeOutClass );
+
+ return wait( 150 ).then( function () {
+ preview.el.remove();
+ } );
+}
+
+/**
+ * @typedef {Object} ext.popups.Thumbnail
+ * @property {Element} el
+ * @property {Boolean} isTall Whether or not the thumbnail is portrait
+ */
+
+/**
+ * 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}
+ */
+function createThumbnail( rawThumbnail ) {
+ var tall, thumbWidth, thumbHeight,
+ x, y, width, height, clipPath,
+ devicePixelRatio = $.bracketedDevicePixelRatio();
+
+ if ( !rawThumbnail ) {
+ return null;
+ }
+
+ tall = rawThumbnail.width < rawThumbnail.height;
+ thumbWidth = rawThumbnail.width / devicePixelRatio;
+ 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;
+ }
+
+ 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 {
+ el: createThumbnailElement(
+ tall ? 'mwe-popups-is-tall' : 'mwe-popups-is-not-tall',
+ rawThumbnail.source,
+ x,
+ y,
+ thumbWidth,
+ thumbHeight,
+ width,
+ height,
+ clipPath
+ ),
+ isTall: tall,
+ 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
+ * @param {String} clipPath
+ * @return {jQuery}
+ */
+function createThumbnailElement( className, url, x, y, thumbnailWidth,
thumbnailHeight, width, height, clipPath ) {
+ var $thumbnailSVGImage, $thumbnail,
+ ns = 'http://www.w3.org/2000/svg',
+
+ // Use createElementNS to create the svg:image tag as jQuery
uses
+ // createElement instead. Some browsers mistakenly map the
image tag to
+ // img tag.
+ svgElement = document.createElementNS(
'http://www.w3.org/2000/svg', 'image' );
+
+ $thumbnailSVGImage = $( svgElement );
+ $thumbnailSVGImage
+ .addClass( className )
+ .attr( {
+ x: x,
+ y: y,
+ width: thumbnailWidth,
+ height: thumbnailHeight,
+ 'clip-path': 'url(#' + clipPath + ')'
+ } );
+
+ // Certain browsers, e.g. IE9, will not correctly set attributes from
+ // foreign namespaces using Element#setAttribute (see T134979). Apart
from
+ // Safari, all supported browsers can set them using
Element#setAttributeNS
+ // (see T134979).
+ if ( isSafari ) {
+ svgElement.setAttribute( 'xlink:href', url );
+ } else {
+ svgElement.setAttributeNS( ns, 'xlink:href', url );
+ }
+ $thumbnail = $( '<svg>' )
+ .attr( {
+ xmlns: ns,
+ width: width,
+ height: height
+ } )
+ .append( $thumbnailSVGImage );
+
+ return $thumbnail;
+}
+
+/**
+ * Represents the layout of a preview, which consists of a position (`offset`)
+ * and whether or not the preview should be flipped horizontally or
+ * vertically (`flippedX` and `flippedY` respectively).
+ *
+ * @typedef {Object} ext.popups.PreviewLayout
+ * @property {Object} offset
+ * @property {Boolean} flippedX
+ * @property {Boolean} flippedY
+ */
+
+/**
+ * Extracted from `mw.popups.renderer.article.getOffset`.
+ *
+ * @param {ext.popups.Preview} preview
+ * @param {Object} event
+ * @return {ext.popups.PreviewLayout}
+ */
+function createLayout( preview, event ) {
+ var flippedX = false,
+ flippedY = false,
+ link = $( event.target ),
+ 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 -= !preview.isTall ?
+ SIZES.portraitPopupWidth :
+ SIZES.landscapePopupWidth;
+ flippedX = true;
+ }
+
+ if ( event.pageX ) {
+ offsetLeft += ( flippedX ) ? 20 : -20;
+ }
+
+ // Y Flip
+ if ( clientTop > ( $window.height() / 2 ) ) {
+ flippedY = true;
+
+ // Mirror the positioning of the preview when there's no "Y
flip": rest
+ // the pokey on the edge of the link's bounding rectangle. In
this case
+ // the edge is the top-most.
+ offsetTop = link.offset().top - SIZES.pokeySize;
+
+ // 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();
+ }
+ }
+
+ return {
+ offset: {
+ top: offsetTop,
+ left: offsetLeft
+ },
+ flippedX: flippedX,
+ flippedY: flippedY
+ };
+}
+
+/**
+ * Generates a list of declarative CSS classes that represent the layout of
+ * the preview.
+ *
+ * @param {ext.popups.Preview} preview
+ * @param {ext.popups.PreviewLayout} layout
+ * @return {String[]}
+ */
+function getClasses( preview, layout ) {
+ var classes = [];
+
+ if ( layout.flippedY ) {
+ classes.push( 'mwe-popups-fade-in-down' );
+ } else {
+ classes.push( 'mwe-popups-fade-in-up' );
+ }
+
+ if ( layout.flippedY && layout.flippedX ) {
+ classes.push( 'flipped_x_y' );
+ }
+
+ if ( layout.flippedY && !layout.flippedX ) {
+ classes.push( 'flipped_y' );
+ }
+
+ if ( layout.flippedX && !layout.flippedY ) {
+ classes.push( 'flipped_x' );
+ }
+
+ if ( ( !preview.hasThumbnail || preview.isTall ) && !layout.flippedY ) {
+ classes.push( 'mwe-popups-no-image-tri' );
+ }
+
+ if ( ( preview.hasThumbnail && !preview.isTall ) && !layout.flippedY ) {
+ classes.push( 'mwe-popups-image-tri' );
+ }
+
+ if ( preview.isTall ) {
+ classes.push( 'mwe-popups-is-tall' );
+ } else {
+ classes.push( 'mwe-popups-is-not-tall' );
+ }
+
+ return classes;
+}
+
+/**
+ * Lays out the preview given the layout.
+ *
+ * If the preview should be oriented differently, then the pokey is updated,
+ * e.g. if the preview should be flipped vertically, then the pokey is
+ * removed.
+ *
+ * If the thumbnail is landscape and isn't the full height of the thumbnail
+ * container, then pull the extract up to keep whitespace consistent across
+ * previews.
+ *
+ * @param {ext.popups.Preview} preview
+ * @param {ext.popups.PreviewLayout} layout
+ */
+function layoutPreview( preview, layout ) {
+ var popup = preview.el,
+ isTall = preview.isTall,
+ hasThumbnail = preview.hasThumbnail,
+ thumbnail = preview.thumbnail,
+ flippedY = layout.flippedY,
+ flippedX = layout.flippedX,
+ offsetTop = layout.offset.top;
+
+ if ( !flippedY && !isTall && hasThumbnail && thumbnail.height <
SIZES.landscapeImage.h ) {
+ $( '.mwe-popups-extract' ).css(
+ 'margin-top',
+ thumbnail.height - SIZES.pokeySize
+ );
+ }
+
+ popup.addClass( getClasses( preview, layout ).join( ' ' ) );
+
+ if ( flippedY ) {
+ offsetTop -= popup.outerHeight();
+ }
+
+ popup.css( {
+ top: offsetTop,
+ left: layout.offset.left + 'px'
+ } );
+
+ if ( flippedY && hasThumbnail ) {
+ popup.find( 'image' )[ 0 ]
+ .setAttribute( 'clip-path', '' );
+ }
+
+ if ( flippedY && flippedX && hasThumbnail && isTall ) {
+ popup.find( 'image' )[ 0 ]
+ .setAttribute( 'clip-path',
'url(#mwe-popups-landscape-mask-flip)' );
+ }
+
+ if ( flippedX && !flippedY && hasThumbnail && !isTall ) {
+ popup.find( 'image' )[ 0 ]
+ .setAttribute( 'clip-path',
'url(#mwe-popups-mask-flip)' );
+ }
+
+ if ( flippedX && !flippedY && hasThumbnail && isTall ) {
+ popup.removeClass( 'mwe-popups-no-image-tri' )
+ .find( 'image' )[ 0 ]
+ .setAttribute( 'clip-path',
'url(#mwe-popups-landscape-mask)' );
+ }
+}
+
+/**
+ * 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;
+}
+
+module.exports = {
+ render: render,
+ init: init
+};
--
To view, visit https://gerrit.wikimedia.org/r/337830
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I89a1ac2205385db8e2f2c040ac22d2f4de793a18
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Popups
Gerrit-Branch: master
Gerrit-Owner: Jhernandez <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits