Phuedx has uploaded a new change for review.
https://gerrit.wikimedia.org/r/322635
Change subject: WIP: Render previews
......................................................................
WIP: Render previews
Change-Id: I14b437e7c2f55b988837fcb2800dd61a23c29a01
---
M extension.json
D resources/ext.popups.desktop/ext.popups.renderer.article.js
M resources/ext.popups.desktop/popup.mustache
M resources/ext.popups.renderer.desktopRenderer/desktopRenderer.js
D resources/ext.popups.targets.desktopTarget/desktopTarget.js
M resources/ext.popups/boot.js
M resources/ext.popups/reducers.js
A resources/ext.popups/renderChangeListener.js
A resources/ext.popups/renderer.js
9 files changed, 530 insertions(+), 1,151 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Popups
refs/changes/35/322635/1
diff --git a/extension.json b/extension.json
index d1ffca8..1d9eb16 100644
--- a/extension.json
+++ b/extension.json
@@ -67,6 +67,8 @@
"resources/ext.popups/changeListener.js",
"resources/ext.popups/footerLinkChangeListener.js",
"resources/ext.popups/linkTitleChangeListener.js",
+ "resources/ext.popups/renderer.js",
+ "resources/ext.popups/renderChangeListener.js",
"resources/ext.popups/boot.js"
],
"templates": {
@@ -74,6 +76,7 @@
"settings.mustache":
"resources/ext.popups.desktop/settings.mustache"
},
"styles": [
+
"resources/ext.popups.core/ext.popups.core.less",
"resources/ext.popups.desktop/ext.popups.animation.less",
"resources/ext.popups.desktop/ext.popups.settings.less"
],
diff --git a/resources/ext.popups.desktop/ext.popups.renderer.article.js
b/resources/ext.popups.desktop/ext.popups.renderer.article.js
deleted file mode 100644
index b77f31f..0000000
--- a/resources/ext.popups.desktop/ext.popups.renderer.article.js
+++ /dev/null
@@ -1,630 +0,0 @@
-( function ( $, mw ) {
- 'use strict';
-
- /**
- * @class mw.popups.render.article
- * @singleton
- */
- var currentRequest,
- isSafari = navigator.userAgent.match( /Safari/ ) !== null,
- article = {},
- $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, data ) {
- // only log genuine errors, not client aborts
- if ( data.textStatus !== 'abort' ) {
- mw.track( 'ext.popups.event', $.extend(
logData, {
- action: 'error',
- errorState: textStatus
- } ) );
- }
- 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 ) );
-
- mw.popups.render.cache[ href ].settings = {
- title: page.title,
- namespace: page.ns,
- tall: ( tall === undefined ) ? false : tall,
- thumbnail: ( thumbnail === undefined ) ? false :
thumbnail
- };
-
- return $div;
- };
-
- /**
- * 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' );
-
- $thumbnailSVGImage = $( svgElement );
- $thumbnailSVGImage
- .addClass( className )
- .attr( {
- x: x,
- y: y,
- width: thumbnailWidth,
- height: thumbnailHeight,
- 'clip-path': 'url(#' + clipPath + ')'
- } );
-
- // Make image render in Safari (T138430)
- if ( isSafari ) {
- svgElement.setAttribute( 'xlink:href', url );
- } else {
- // certain browsers e.g. ie9 will not correctly set
attributes from foreign namespaces (T134979)
- svgElement.setAttributeNS( ns, 'xlink:href', url );
- }
- $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.event', $.extend( logData, {
- action: 'tapped settings cog'
- } ) );
- } );
-
- 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 );
diff --git a/resources/ext.popups.desktop/popup.mustache
b/resources/ext.popups.desktop/popup.mustache
index 064e85e..9bf4e2a 100644
--- a/resources/ext.popups.desktop/popup.mustache
+++ b/resources/ext.popups.desktop/popup.mustache
@@ -1,12 +1,14 @@
-<div>
- {{#hasThumbnail}}
- <a href="{{href}}" class="mwe-popups-discreet"><!-- thumbnail injected
post render --></a>
- {{/hasThumbnail}}
- <a dir="{{langdir}}" lang="{{langcode}}" class="mwe-popups-extract"
href="{{href}}"><!-- extract will be appended here --></a>
- <footer>
- <span
-
class="{{#isRecent}}mwe-popups-timestamp-recent{{/isRecent}}{{^isRecent}}mwe-popups-timestamp-older{{/isRecent}}">{{lastModified}}</span>
- <a class="mwe-popups-icon mwe-popups-settings-icon"></a>
- <!-- survey link injected here if found -->
- </footer>
-</div>
\ No newline at end of file
+{{! Extracted from `mw.popups.render.createPopupElement` }}
+<div class="mwe-popups" role="tooltip" aria-hidden>
+ <div>
+ {{#hasThumbnail}}
+ <a href="{{href}}" class="mwe-popups-discreet"><!-- thumbnail injected
post render --></a>
+ {{/hasThumbnail}}
+ <a dir="{{langdir}}" lang="{{langcode}}" class="mwe-popups-extract"
href="{{href}}"><!-- extract will be appended here --></a>
+ <footer>
+ <span
+
class="{{#isRecent}}mwe-popups-timestamp-recent{{/isRecent}}{{^isRecent}}mwe-popups-timestamp-older{{/isRecent}}">{{lastModified}}</span>
+ <a class="mwe-popups-icon mwe-popups-settings-icon"></a>
+ </footer>
+ </div>
+</div>
diff --git a/resources/ext.popups.renderer.desktopRenderer/desktopRenderer.js
b/resources/ext.popups.renderer.desktopRenderer/desktopRenderer.js
index b94edd9..67f7091 100644
--- a/resources/ext.popups.renderer.desktopRenderer/desktopRenderer.js
+++ b/resources/ext.popups.renderer.desktopRenderer/desktopRenderer.js
@@ -1,186 +1,8 @@
-/*global popupDelay: true, popupHideDelay: true*/
-
-( function ( $, mw ) {
- var closeTimer, openTimer,
- $activeLink = null,
- logData = {};
-
- /**
- * Sets the link that the currently shown popup relates to
- *
- * @ignore
- * @param {jQuery|null} [$link] if undefined there is no active link
- */
- function setActiveLink( $link ) {
- $activeLink = $link;
- }
-
- /**
- * Gets the link that the currently shown popup relates to
- *
- * @ignore
- * @return {jQuery|null} if undefined there is no active link
- */
- function getActiveLink() {
- return $activeLink;
- }
-
- /**
- * Logs the click on link or popup
- *
- * @param {Object} event
- */
- function logClickAction( event ) {
- mw.track( 'ext.popups.event', $.extend( {}, logData, {
- action: mw.popups.getAction( event )
- } ) );
- }
-
- /**
- * Logs when a popup is dismissed
- */
- function logDismissAction() {
- mw.track( 'ext.popups.event', $.extend( {}, logData, {
- action: 'dismissed'
- } ) );
- }
- /**
- * @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 = {};
-
- /**
- * 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 {jQuery.Object} $link that a hovercard should be shown for
- * @param {jQuery.Event} event that triggered the render
- * @param {string} linkInteractionToken random token representing the
current interaction with the link
- */
- mw.popups.render.render = function ( $link, event, linkInteractionToken
) {
- var linkHref = $link.attr( 'href' ),
- $activeLink = getActiveLink();
-
- // 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 (
- $activeLink &&
- $activeLink[ 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 ( $activeLink ) {
- mw.popups.render.closePopup( logDismissAction );
- }
-
- // Ignore if its meant to call a function
- // TODO: Remove this when adding reference popups
- if ( linkHref === '#' ) {
- return;
- }
-
- setActiveLink( $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 ),
- linkInteractionToken: linkInteractionToken
- };
-
- $link
- .on( 'mouseleave blur', leaveInactive )
- .off( 'click', clickHandler )
- .on( 'click', clickHandler );
-
- 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
*/
@@ -236,27 +58,6 @@
};
/**
- * Click handler for the hovercard
- *
- * @method clickHandler
- * @ignore
- * @param {Object} event
- */
- function clickHandler( event ) {
- var action = mw.popups.getAction( event ),
- $activeLink = getActiveLink();
-
- logClickAction( event );
-
- if ( action === 'opened in same tab' ) {
- window.location.href = $activeLink.attr( 'href' );
- }
-
- // close the popup
- mw.popups.render.closePopup();
- }
-
- /**
* Removes the hover class from the link and unbinds events
* Hides the popup, clears timers and sets it and the resets the
renderer
*
@@ -277,11 +78,6 @@
'mwe-popups-fade-out-down' :
'mwe-popups-fade-out-up';
- mw.popups.$popup
- .off( 'mouseleave', leaveActive )
- .removeClass( fadeInClass )
- .addClass( fadeOutClass );
-
mw.popups.render.wait( 150 ).done( function () {
if ( mw.popups.$popup.hasClass( fadeOutClass ) ) {
mw.popups.$popup
@@ -290,109 +86,6 @@
.removeClass(
'mwe-popups-fade-out-down' );
}
} );
-
- if ( closeTimer ) {
- closeTimer.abort();
- }
-
- $( document ).off( 'keydown', closeOnEsc );
-
- if ( $.isFunction( logCallback ) ) {
- logCallback();
- }
-
- 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
- * @ignore
- * @param {jQuery.Event} event
- */
- function closeOnEsc( event ) {
- var $activeLink = getActiveLink();
- if ( event.keyCode === 27 && $activeLink ) {
- mw.popups.render.closePopup( logDismissAction );
- }
- }
-
- /**
- * 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
- * @ignore
- */
- function leaveActive() {
- closeTimer = mw.popups.render.wait(
mw.popups.render.POPUP_CLOSE_DELAY ).done( function () {
- var $activeLink = getActiveLink();
- if ( $activeLink ) {
- mw.popups.render.closePopup( logDismissAction );
- }
- } );
- }
-
- /**
- * Unbinds events on the anchor tag and aborts AJAX request.
- *
- * @method leaveInactive
- * @ignore
- */
- function leaveInactive() {
- var $activeLink = getActiveLink();
-
- mw.track( 'ext.popups.event', $.extend( {}, logData, {
- action: 'dwelledButAbandoned'
- } ) );
-
- $activeLink.off( 'mouseleave blur', leaveInactive );
- if ( openTimer ) {
- openTimer.abort();
- }
- mw.popups.render.abortCurrentRequest();
-
- mw.popups.render.reset();
- }
-
- /**
- * Resets the renderer
- *
- * @method reset
- */
- mw.popups.render.reset = function () {
- logData = {};
- setActiveLink( null );
- mw.popups.render.abortCurrentRequest();
- openTimer = undefined;
- closeTimer = undefined;
- };
+
} )( jQuery, mediaWiki );
diff --git a/resources/ext.popups.targets.desktopTarget/desktopTarget.js
b/resources/ext.popups.targets.desktopTarget/desktopTarget.js
deleted file mode 100644
index 0d51b26..0000000
--- a/resources/ext.popups.targets.desktopTarget/desktopTarget.js
+++ /dev/null
@@ -1,200 +0,0 @@
-( function ( $, mw ) {
- /**
- * Check whether the Navigation Popups gadget is enabled
- *
- * @return {boolean}
- */
- function isNavigationPopupsGadgetEnabled() {
- return window.pg !== undefined;
- }
-
- /**
- * `mouseleave` and `blur` events handler for links that are eligible
for
- * popups, but when Popups are disabled.
- *
- * @param {Object} event
- */
- function onLinkAbandon( event ) {
- var $this = $( this ),
- data = event.data || {};
-
- $this.off( 'mouseleave.popups blur.popups', onLinkAbandon );
-
- mw.track( 'ext.popups.event', {
- pageTitleHover: $this.attr( 'title' ),
- action: 'dwelledButAbandoned',
- linkInteractionToken: data.linkInteractionToken,
- hovercardsSuppressedByGadget:
data.hovercardsSuppressedByGadget
- } );
- }
-
- /**
- * `mouseenter` and `focus` events handler for links that are eligible
for
- * popups. Handles the disply of a popup.
- *
- * @param {Object} event
- */
- function onLinkHover( event ) {
- var $link = $( this ),
- // Cache the hover start time and link interaction
token for a later use
- eventData = {
- linkInteractionToken:
mw.popups.getRandomToken(),
- hovercardsSuppressedByGadget:
isNavigationPopupsGadgetEnabled()
- };
-
- mw.track( 'ext.popups.event', $.extend( {}, eventData, {
- action: 'hover'
- } )
- );
- // Only enable Popups when the Navigation popups gadget is not
enabled
- if ( !eventData.hovercardsSuppressedByGadget &&
mw.popups.enabled ) {
- if ( mw.popups.scrolled ) {
- return;
- }
-
- mw.popups.removeTooltip( $link );
- mw.popups.render.render( $link, event,
eventData.linkInteractionToken );
- } else {
- // Remove existing handlers and replace with logging
only
- $link
- .off( 'mouseleave.popups blur.popups
mouseenter.popups focus.popups click.popups' )
- // We are passing the same data, rather than a
shared object, into two different
- // functions so that one function doesn't
change the data and
- // have a side-effect on the other function's
data.
- .on( 'mouseleave.popups blur.popups',
eventData, onLinkAbandon )
- .on( 'click.popups', eventData, onLinkClick );
- }
- }
-
- /**
- * `mouseleave` and `blur` events handler for links that are eligible
for
- * popups. Handles the restoration of title attributes
- */
- function onLinkBlur() {
- mw.popups.restoreTooltip( $( this ) );
- }
-
- /**
- * `click` event handler for links that are eligible for popups, but
when
- * Popups are disabled.
- *
- * @param {Object} event
- */
- function onLinkClick( event ) {
- var $this = $( this ),
- action = mw.popups.getAction( event ),
- href = $this.attr( 'href' ),
- data = event.data || {};
-
- $this.off( 'click.popups', onLinkClick );
-
- mw.track( 'ext.popups.event', {
- pageTitleHover: $this.attr( 'title' ),
- action: action,
- linkInteractionToken: data.linkInteractionToken,
- hovercardsSuppressedByGadget:
data.hovercardsSuppressedByGadget
- } );
-
- if ( action === 'opened in same tab' ) {
- event.preventDefault();
- window.location.href = href;
- }
- }
-
- mw.popups.enabled = mw.popups.getEnabledState();
-
- /**
- * Creates the SVG mask used to create the
- * the triangle pointer on popups with images
- *
- * @method createSVGMask
- */
- mw.popups.createSVGMask = function () {
- $( '<div>' )
- .attr( 'id', 'mwe-popups-svg' )
- .appendTo( document.body )
- .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>'
- );
- return true;
- };
-
- /**
- * Create the element that holds the popups
- *
- * @method createPopupElement
- */
- mw.popups.createPopupElement = function () {
- mw.popups.$popup = $( '<div>' )
- .attr( {
- 'class': 'mwe-popups',
- role: 'tooltip',
- 'aria-hidden': 'true'
- } )
- .appendTo( document.body );
- };
-
- /**
- * Checks if the user is scrolling, sets to false on mousemove
- *
- * @method checkScroll
- */
- mw.popups.checkScroll = function () {
- $( window ).on( 'scroll.popups', function () {
- mw.popups.scrolled = true;
- } );
-
- $( window ).on( 'mousemove.popups', function () {
- mw.popups.scrolled = false;
- } );
- };
-
- /**
- * Adds the events necessary to all links within a container
- * so that a popup shows on hover.
- *
- * @param {jQuery.Object} $content to setup mouse events for
- * @ignore
- * @method setupMouseEvents
- */
- function setupMouseEvents( $content ) {
- mw.popups.$content = $content;
- mw.popups.selectPopupElements()
- .on( 'mouseenter.popups focus.popups', onLinkHover )
- .on( 'mouseleave.popups blur.popups', onLinkBlur );
- }
-
- mw.hook( 'wikipage.content' ).add( setupMouseEvents );
-
- function initPopups() {
- mw.popups.checkScroll();
- mw.popups.createSVGMask();
- mw.popups.createPopupElement();
- }
-
- $( function () {
- if ( mw.popups.enabled ) {
- initPopups();
- }
-
- mw.track( 'ext.popups.event', {
- action: 'pageLoaded',
- hovercardsSuppressedByGadget:
isNavigationPopupsGadgetEnabled()
- } );
- } );
-} )( jQuery, mediaWiki );
diff --git a/resources/ext.popups/boot.js b/resources/ext.popups/boot.js
index cdc2844..9ae31b3 100644
--- a/resources/ext.popups/boot.js
+++ b/resources/ext.popups/boot.js
@@ -110,6 +110,8 @@
BLACKLISTED_LINKS
);
+ mw.popups.renderer.init();
+
previewLinks
.on( 'mouseover focus', function ( event ) {
actions.linkDwell( this, event, gateway
);
diff --git a/resources/ext.popups/reducers.js b/resources/ext.popups/reducers.js
index 442aee7..6c8b9c7 100644
--- a/resources/ext.popups/reducers.js
+++ b/resources/ext.popups/reducers.js
@@ -85,7 +85,7 @@
case mw.popups.actionTypes.FETCH_END:
return nextState( state, {
isFetching: false,
- fetchResponse: action.response
+ fetchResponse: action.result
} );
case mw.popups.actionTypes.FETCH_FAILED:
return nextState( state, {
diff --git a/resources/ext.popups/renderChangeListener.js
b/resources/ext.popups/renderChangeListener.js
new file mode 100644
index 0000000..f49ab4b
--- /dev/null
+++ b/resources/ext.popups/renderChangeListener.js
@@ -0,0 +1,17 @@
+( function ( mw ) {
+
+ /**
+ * @return {ext.popups.ChangeListener}
+ */
+ mw.popups.changeListeners.render = function () {
+ return function ( prevState, state ) {
+ var preview;
+
+ if ( state.preview.fetchResponse ) {
+ preview = mw.popups.renderer.render(
state.preview.fetchResponse );
+ preview.show( state.preview.activeEvent );
+ }
+ };
+ };
+
+}( mediaWiki ) );
diff --git a/resources/ext.popups/renderer.js b/resources/ext.popups/renderer.js
new file mode 100644
index 0000000..7eb4b90
--- /dev/null
+++ b/resources/ext.popups/renderer.js
@@ -0,0 +1,492 @@
+( function ( mw, $ ) {
+
+ var isSafari = navigator.userAgent.match( /Safari/ ) !== null,
+ 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.
+ },
+ $window = $( window );
+
+ mw.popups.renderer = {};
+
+ /**
+ * 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 );
+ }
+
+ /**
+ *
+ */
+ mw.popups.renderer.init = function () {
+ createPokeyMasks();
+ };
+
+ /**
+ *
+ */
+ mw.popups.renderer.render = function ( data ) {
+ var templateData,
+ thumbnail = createThumbnail( data.thumbnail ),
+ hasThumbnail = thumbnail !== null,
+ extract = renderExtract( data.extract, data.title ),
+ $el,
+ result = {};
+
+ templateData = $.extend( {}, data, {
+ lastModifiedMsg: mw.msg( 'popups-last-edited', moment(
data.lastModified ).fromNow() ),
+ hasThumbnail: hasThumbnail
+ } );
+
+ $el = mw.template.get( 'ext.popups', 'popup.mustache' )
+ .render( templateData )
+ .appendTo( document.body );
+
+ if ( thumbnail ) {
+ $el.find( '.mwe-popups-discreet' ).append( thumbnail.el
);
+ }
+
+ if ( extract.length ) {
+ $el.find( '.mwe-popups-extract' ).append( extract );
+ }
+
+ result = {
+ el: $el,
+ hasThumbnail: hasThumbnail,
+ thumbnail: thumbnail,
+ isTall: hasThumbnail && thumbnail.isTall, // TODO: Get
rid of this.
+ };
+
+ result.show = show.bind( result );
+ result.hide = hide.bind( result );
+
+ return result;
+ };
+
+ /**
+ * Returns an array of elements to be appended after removing
parentheticals
+ * and making the title in the extract bold.
+ *
+ * Using the bolded elements of the extract of the page directly is
covered by
+ * [T141651](https://phabricator.wikimedia.org/T141651).
+ *
+ * @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;
+ }
+
+ /**
+ * There may be more than one link that resolves to the same title.
+ */
+ function show( event ) {
+ var layout = createLayout( this, event );
+
+ processPreview( this, layout );
+
+ this.el.css( layout.offset )
+ .attr( {
+ 'aria-hidden': 'false',
+ class: getClasses( this, layout ).join( ' ' )
+ } )
+ .show();
+ }
+
+ /**
+ *
+ */
+ function hide() {
+ }
+
+ /**
+ * 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
+ * @return {Object|null}
+ */
+ function createThumbnail( thumbnail ) {
+ var tall, thumbWidth, thumbHeight,
+ x, y, width, height, clipPath,
+ devicePixelRatio = $.bracketedDevicePixelRatio();
+
+ if ( !thumbnail ) {
+ return null;
+ }
+
+ tall = thumbnail.width < thumbnail.height;
+ 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 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: createSvgImageThumbnail(
+ tall ? 'mwe-popups-is-tall' :
'mwe-popups-is-not-tall',
+ thumbnail.url,
+ x,
+ y,
+ thumbWidth,
+ thumbHeight,
+ width,
+ height,
+ clipPath
+ ),
+ isTall: tall
+ };
+ }
+
+ /**
+ * Returns the `svg:image` object for thumbnail
+ *
+ * @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 createSvgImageThumbnail(
+ 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 + ')'
+ } );
+
+ // Make image render in Safari (T138430)
+ if ( isSafari ) {
+ svgElement.setAttribute( 'xlink:href', url );
+ } else {
+ // certain browsers e.g. ie9 will not correctly set
attributes from foreign namespaces (T134979)
+ svgElement.setAttributeNS( ns, 'xlink:href', url );
+ }
+ $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.
+ *
+ * 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;
+
+ // 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;
+ }
+ }
+
+ return {
+ offset: {
+ top: offsetTop + 'px',
+ left: offsetLeft + 'px'
+ },
+ flippedX: flippedX,
+ flippedY: flippedY
+ };
+ }
+
+ /**
+ * Returns an array of classes based on the size and setting of the
popup
+ *
+ * @param {ext.popups.Preview} preview
+ * @param {ext.popups.PreviewLayout} layout
+ * @return {String[]}
+ */
+ function getClasses( preview, layout ) {
+ var classes = [ 'mwe-popups' ],
+ isTall = preview.isTall,
+ hasThumbnail = preview.hasThumbnail,
+ flippedY = layout.flippedY,
+ flippedX = layout.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 ( ( !hasThumbnail || isTall ) && !flippedY ) {
+ classes.push( 'mwe-popups-no-image-tri' );
+ }
+
+ if ( ( hasThumbnail && !isTall ) && !flippedY ) {
+ classes.push( 'mwe-popups-image-tri' );
+ }
+
+ if ( isTall ) {
+ 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
+ */
+ function processPreview( preview, layout ) {
+ var popup = preview.el,
+ isTall = preview.isTall,
+ hasThumbnail = preview.hasThumbnail,
+ thumbnail = preview.thumbnail,
+ flippedY = layout.flippedY,
+ flippedX = layout.flippedX;
+
+ if ( !flippedY && !isTall && hasThumbnail && thumbnail.height <
SIZES.landscapeImage.h ) {
+ $( '.mwe-popups-extract' ).css(
+ 'margin-top',
+ thumbnail.height - SIZES.pokeySize
+ );
+ }
+
+ if ( flippedY ) {
+ popup.css( {
+ top: popup.offset().top - popup.outerHeight()
+ } );
+ }
+
+ 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;
+ }
+
+}( mediaWiki, jQuery ) );
--
To view, visit https://gerrit.wikimedia.org/r/322635
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I14b437e7c2f55b988837fcb2800dd61a23c29a01
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Popups
Gerrit-Branch: mpga
Gerrit-Owner: Phuedx <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits