2017-05-26 17:35:07 +00:00
|
|
|
|
/**
|
|
|
|
|
* @module renderer
|
|
|
|
|
*/
|
|
|
|
|
|
2017-07-28 17:32:46 +00:00
|
|
|
|
import wait from '../wait';
|
2018-04-26 13:25:55 +00:00
|
|
|
|
import pointerMaskSVG from './pointer-mask.svg';
|
2018-03-08 20:38:23 +00:00
|
|
|
|
import { SIZES, createThumbnail } from './thumbnail';
|
2018-03-07 11:10:53 +00:00
|
|
|
|
import { previewTypes } from '../preview/model';
|
2018-03-27 19:10:48 +00:00
|
|
|
|
import { renderPreview } from './templates/preview/preview';
|
2018-12-12 18:37:19 +00:00
|
|
|
|
import { renderReferencePreview } from './templates/referencePreview/referencePreview';
|
2018-03-27 19:10:48 +00:00
|
|
|
|
import { renderPagePreview } from './templates/pagePreview/pagePreview';
|
2017-07-28 17:32:46 +00:00
|
|
|
|
|
2018-06-21 15:47:13 +00:00
|
|
|
|
const mw = mediaWiki,
|
2017-02-14 20:17:32 +00:00
|
|
|
|
$ = jQuery,
|
2018-06-26 20:47:10 +00:00
|
|
|
|
defaultExtractWidth = 215,
|
2018-03-08 20:38:23 +00:00
|
|
|
|
$window = $( window ),
|
|
|
|
|
landscapePopupWidth = 450,
|
|
|
|
|
portraitPopupWidth = 320,
|
2018-04-26 13:25:55 +00:00
|
|
|
|
pointerSize = 8; // Height of the pointer.
|
2017-02-14 20:17:32 +00:00
|
|
|
|
|
|
|
|
|
/**
|
2018-03-07 20:35:40 +00:00
|
|
|
|
* Extracted from `mw.popups.createSVGMasks`. This is just an SVG mask to point
|
2018-04-26 13:25:55 +00:00
|
|
|
|
* or "point" at the link that's hovered over. The "pointer" appears to be cut
|
|
|
|
|
* out of the image itself:
|
2018-04-13 17:29:57 +00:00
|
|
|
|
* _______ link
|
2018-04-26 13:25:55 +00:00
|
|
|
|
* | | _/\_____ _/\____ <-- Pointer pointing at link
|
2018-04-13 17:29:57 +00:00
|
|
|
|
* | :-] | + |xxxxxxx = | :-] |
|
|
|
|
|
* |_______| |xxxxxxx |_______|
|
|
|
|
|
* :
|
2018-04-26 13:25:55 +00:00
|
|
|
|
* Thumbnail Pointer Page preview
|
|
|
|
|
* image clip-path bubble w/ pointer
|
2018-03-07 20:35:40 +00:00
|
|
|
|
*
|
|
|
|
|
* SVG masks are used in place of CSS masks for browser support issues (see
|
|
|
|
|
* https://caniuse.com/#feat=css-masks).
|
|
|
|
|
*
|
2017-04-27 19:55:43 +00:00
|
|
|
|
* @private
|
2018-04-26 13:25:55 +00:00
|
|
|
|
* @param {Object} container DOM object to which pointer masks are appended
|
2018-07-09 16:56:45 +00:00
|
|
|
|
* @return {void}
|
2017-02-14 20:17:32 +00:00
|
|
|
|
*/
|
2018-04-26 13:25:55 +00:00
|
|
|
|
export function createPointerMasks( container ) {
|
2017-02-14 20:17:32 +00:00
|
|
|
|
$( '<div>' )
|
|
|
|
|
.attr( 'id', 'mwe-popups-svg' )
|
2018-04-26 13:25:55 +00:00
|
|
|
|
.html( pointerMaskSVG )
|
2017-04-27 19:55:43 +00:00
|
|
|
|
.appendTo( container );
|
2017-02-14 20:17:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initializes the renderer.
|
2018-07-09 16:56:45 +00:00
|
|
|
|
* @return {void}
|
2017-02-14 20:17:32 +00:00
|
|
|
|
*/
|
2017-07-28 17:32:46 +00:00
|
|
|
|
export function init() {
|
2018-04-26 13:25:55 +00:00
|
|
|
|
createPointerMasks( document.body );
|
2017-02-14 20:17:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
2018-07-13 13:07:35 +00:00
|
|
|
|
* @property {JQuery} el
|
|
|
|
|
* @property {boolean} hasThumbnail
|
2017-02-14 20:17:32 +00:00
|
|
|
|
* @property {Object} thumbnail
|
2018-07-13 13:07:35 +00:00
|
|
|
|
* @property {boolean} isTall Sugar around
|
2017-02-14 20:17:32 +00:00
|
|
|
|
* `preview.hasThumbnail && thumbnail.isTall`
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
2017-06-15 11:35:19 +00:00
|
|
|
|
* Renders a preview given data from the {@link gateway Gateway}.
|
2017-02-14 20:17:32 +00:00
|
|
|
|
* 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
|
2018-01-11 03:23:28 +00:00
|
|
|
|
* their keyboard or other assistive device.
|
2017-02-14 20:17:32 +00:00
|
|
|
|
*
|
|
|
|
|
* 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}
|
|
|
|
|
*/
|
2017-07-28 17:32:46 +00:00
|
|
|
|
export function render( model ) {
|
2018-03-19 19:39:41 +00:00
|
|
|
|
const preview = createPreviewWithType( model );
|
2017-02-14 20:17:32 +00:00
|
|
|
|
|
|
|
|
|
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).
|
2018-07-13 13:07:35 +00:00
|
|
|
|
* @param {string} token The unique token representing the link interaction
|
2017-04-10 17:28:24 +00:00
|
|
|
|
* that resulted in showing the preview
|
Update: cancel unused HTTP requests in flight
Whenever an HTTP request sequence is started, i.e. wait for the fetch
start time, issue a network request, and return the result, abort the
process if the results are known to no longer be needed. This occurs
when a user has dwelt upon one link and then abandoned it either during
the fetch start wait time or during the fetch network request itself.
This change is accomplished by preserving the pending promises in two
actions, LINK_DWELL and FETCH_START, and whenever the ABANDON_START
action is issued, it now aborts any previously pending XHR-like promise,
called a "AbortPromise" which is just a thenable with an abort() method.
There is a similar concept in Core:
https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/core/+/ecc812f06e7dff587b3f31dc18189adbf4616351/resources/src/mediawiki.api/index.js.
Aborting pending requests has big implications for client and server
logging as requests are quickly canceled, especially on slower
connections. These differences can be observed on the network tab of
DevTools and the log in Redux DevTools.
Consider, for instance, the scenario of dwelling upon and quickly
abandoning a single link prior to this patch:
BOOT EVENT_LOGGED LINK_DWELL FETCH_START ABANDON_START FETCH_END STATSV_LOGGED ABANDON_END EVENT_LOGGED FETCH_COMPLETE
And after this patch when the fetch timer is canceled (prior to an
actual network request):
BOOT EVENT_LOGGED LINK_DWELL ABANDON_START ABANDON_END EVENT_LOGGED
In the above sequence, FETCH_* and STATSV_LOGGED actions never occur.
And after this patch when the network request itself is canceled:
BOOT EVENT_LOGGED LINK_DWELL FETCH_START ABANDON_START FETCH_FAILED STATSV_LOGGED FETCH_COMPLETE ABANDON_END EVENT_LOGGED
FETCH_FAILED occurs intentionally, STATSV_LOGGED and FETCH_COMPLETE
still happen even though the fetch didn't complete successfully, and
FETCH_END doesn't.
Additionally, since less data is transmitted, it's possible that the
timing and success rate of logging will improve on low bandwidth
connections.
Also, this patch tries to revise the JSDocs where possible to support
type checking and fix a call to the missing assert.fail() function in
changeListener.test.js.
Bug: T197700
Change-Id: I9a73b3086fc8fb0edd897a347b5497d5362e20ef
2018-06-25 13:26:11 +00:00
|
|
|
|
* @return {JQuery.Promise<void>}
|
2017-02-14 20:17:32 +00:00
|
|
|
|
*/
|
2018-03-14 22:04:59 +00:00
|
|
|
|
show( event, boundActions, token ) {
|
2017-05-08 16:51:42 +00:00
|
|
|
|
return show(
|
|
|
|
|
preview, event, $( event.target ), boundActions, token,
|
2018-04-12 01:51:42 +00:00
|
|
|
|
document.body, document.documentElement.getAttribute( 'dir' )
|
2017-05-08 16:51:42 +00:00
|
|
|
|
);
|
2016-11-21 12:07:23 +00:00
|
|
|
|
},
|
|
|
|
|
|
2017-02-14 20:17:32 +00:00
|
|
|
|
/**
|
|
|
|
|
* Hides the preview.
|
|
|
|
|
*
|
|
|
|
|
* See `hide` for more detail.
|
|
|
|
|
*
|
Update: cancel unused HTTP requests in flight
Whenever an HTTP request sequence is started, i.e. wait for the fetch
start time, issue a network request, and return the result, abort the
process if the results are known to no longer be needed. This occurs
when a user has dwelt upon one link and then abandoned it either during
the fetch start wait time or during the fetch network request itself.
This change is accomplished by preserving the pending promises in two
actions, LINK_DWELL and FETCH_START, and whenever the ABANDON_START
action is issued, it now aborts any previously pending XHR-like promise,
called a "AbortPromise" which is just a thenable with an abort() method.
There is a similar concept in Core:
https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/core/+/ecc812f06e7dff587b3f31dc18189adbf4616351/resources/src/mediawiki.api/index.js.
Aborting pending requests has big implications for client and server
logging as requests are quickly canceled, especially on slower
connections. These differences can be observed on the network tab of
DevTools and the log in Redux DevTools.
Consider, for instance, the scenario of dwelling upon and quickly
abandoning a single link prior to this patch:
BOOT EVENT_LOGGED LINK_DWELL FETCH_START ABANDON_START FETCH_END STATSV_LOGGED ABANDON_END EVENT_LOGGED FETCH_COMPLETE
And after this patch when the fetch timer is canceled (prior to an
actual network request):
BOOT EVENT_LOGGED LINK_DWELL ABANDON_START ABANDON_END EVENT_LOGGED
In the above sequence, FETCH_* and STATSV_LOGGED actions never occur.
And after this patch when the network request itself is canceled:
BOOT EVENT_LOGGED LINK_DWELL FETCH_START ABANDON_START FETCH_FAILED STATSV_LOGGED FETCH_COMPLETE ABANDON_END EVENT_LOGGED
FETCH_FAILED occurs intentionally, STATSV_LOGGED and FETCH_COMPLETE
still happen even though the fetch didn't complete successfully, and
FETCH_END doesn't.
Additionally, since less data is transmitted, it's possible that the
timing and success rate of logging will improve on low bandwidth
connections.
Also, this patch tries to revise the JSDocs where possible to support
type checking and fix a call to the missing assert.fail() function in
changeListener.test.js.
Bug: T197700
Change-Id: I9a73b3086fc8fb0edd897a347b5497d5362e20ef
2018-06-25 13:26:11 +00:00
|
|
|
|
* @return {JQuery.Promise<void>}
|
2017-02-14 20:17:32 +00:00
|
|
|
|
*/
|
2018-03-14 22:04:59 +00:00
|
|
|
|
hide() {
|
2017-02-14 20:17:32 +00:00
|
|
|
|
return hide( preview );
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
2018-03-07 11:10:53 +00:00
|
|
|
|
/**
|
|
|
|
|
* Creates an instance of a Preview based on
|
|
|
|
|
* the type property of the PreviewModel
|
|
|
|
|
*
|
|
|
|
|
* @param {ext.popups.PreviewModel} model
|
|
|
|
|
* @return {ext.popups.Preview}
|
|
|
|
|
*/
|
|
|
|
|
export function createPreviewWithType( model ) {
|
|
|
|
|
switch ( model.type ) {
|
|
|
|
|
case previewTypes.TYPE_PAGE:
|
|
|
|
|
return createPagePreview( model );
|
|
|
|
|
case previewTypes.TYPE_DISAMBIGUATION:
|
|
|
|
|
return createDisambiguationPreview( model );
|
2019-01-17 11:59:51 +00:00
|
|
|
|
case previewTypes.TYPE_REFERENCE:
|
2018-12-12 18:37:19 +00:00
|
|
|
|
return createReferencePreview( model );
|
2018-03-07 11:10:53 +00:00
|
|
|
|
default:
|
|
|
|
|
return createEmptyPreview( model );
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-02-14 20:17:32 +00:00
|
|
|
|
|
2018-06-26 20:47:10 +00:00
|
|
|
|
export { defaultExtractWidth }; // for testing
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculates width of extract based on the associated thumbnail
|
|
|
|
|
*
|
|
|
|
|
* @param {ext.popups.Thumbnail} [thumbnail] model
|
|
|
|
|
* @return {string} representing the css width attribute to be
|
|
|
|
|
* used for the extract
|
|
|
|
|
*/
|
|
|
|
|
export function getExtractWidth( thumbnail ) {
|
|
|
|
|
return thumbnail && thumbnail.isNarrow ? `${defaultExtractWidth + thumbnail.offset}px` : '';
|
|
|
|
|
}
|
|
|
|
|
|
2017-02-14 20:17:32 +00:00
|
|
|
|
/**
|
|
|
|
|
* Creates an instance of the DTO backing a preview.
|
|
|
|
|
*
|
|
|
|
|
* @param {ext.popups.PreviewModel} model
|
|
|
|
|
* @return {ext.popups.Preview}
|
|
|
|
|
*/
|
2019-01-21 12:26:11 +00:00
|
|
|
|
function createPagePreview( model ) {
|
2018-03-19 19:39:41 +00:00
|
|
|
|
const thumbnail = createThumbnail( model.thumbnail ),
|
2017-02-14 20:17:32 +00:00
|
|
|
|
hasThumbnail = thumbnail !== null,
|
2018-03-19 19:39:41 +00:00
|
|
|
|
extract = model.extract;
|
2017-02-14 20:17:32 +00:00
|
|
|
|
|
2018-03-19 19:39:41 +00:00
|
|
|
|
const $el = $( $.parseHTML( renderPagePreview( model, hasThumbnail ) ) );
|
2017-02-14 20:17:32 +00:00
|
|
|
|
|
|
|
|
|
if ( hasThumbnail ) {
|
|
|
|
|
$el.find( '.mwe-popups-discreet' ).append( thumbnail.el );
|
2017-01-26 01:26:16 +00:00
|
|
|
|
}
|
2017-06-08 00:58:30 +00:00
|
|
|
|
if ( extract ) {
|
2018-06-26 20:47:10 +00:00
|
|
|
|
const extractWidth = getExtractWidth( thumbnail );
|
|
|
|
|
$el.find( '.mwe-popups-extract' )
|
|
|
|
|
.css( 'width', extractWidth )
|
|
|
|
|
.append( extract );
|
|
|
|
|
$el.find( 'footer' ).css( 'width', extractWidth );
|
2017-01-26 01:26:16 +00:00
|
|
|
|
}
|
2016-11-21 12:07:23 +00:00
|
|
|
|
|
2017-02-14 20:17:32 +00:00
|
|
|
|
return {
|
|
|
|
|
el: $el,
|
2018-03-14 19:44:22 +00:00
|
|
|
|
hasThumbnail,
|
|
|
|
|
thumbnail,
|
2017-02-14 20:17:32 +00:00
|
|
|
|
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}
|
|
|
|
|
*/
|
2019-01-21 12:26:11 +00:00
|
|
|
|
function createEmptyPreview( model ) {
|
2018-03-19 19:39:41 +00:00
|
|
|
|
const showTitle = false,
|
2018-03-13 22:03:49 +00:00
|
|
|
|
extractMsg = mw.msg( 'popups-preview-no-preview' ),
|
2018-03-19 19:39:41 +00:00
|
|
|
|
linkMsg = mw.msg( 'popups-preview-footer-read' );
|
2017-02-14 20:17:32 +00:00
|
|
|
|
|
2018-03-19 19:39:41 +00:00
|
|
|
|
const $el = $(
|
2018-03-13 22:03:49 +00:00
|
|
|
|
$.parseHTML( renderPreview( model, showTitle, extractMsg, linkMsg ) )
|
|
|
|
|
);
|
2018-03-07 11:10:53 +00:00
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
el: $el,
|
|
|
|
|
hasThumbnail: false,
|
|
|
|
|
isTall: false
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates an instance of the disambiguation preview.
|
|
|
|
|
*
|
|
|
|
|
* @param {ext.popups.PreviewModel} model
|
|
|
|
|
* @return {ext.popups.Preview}
|
|
|
|
|
*/
|
2019-01-21 12:26:11 +00:00
|
|
|
|
function createDisambiguationPreview( model ) {
|
2018-03-19 19:39:41 +00:00
|
|
|
|
const showTitle = true,
|
2018-03-13 22:03:49 +00:00
|
|
|
|
extractMsg = mw.msg( 'popups-preview-disambiguation' ),
|
2018-03-19 19:39:41 +00:00
|
|
|
|
linkMsg = mw.msg( 'popups-preview-disambiguation-link' );
|
2018-03-07 11:10:53 +00:00
|
|
|
|
|
2018-03-19 19:39:41 +00:00
|
|
|
|
const $el = $(
|
2018-03-13 22:03:49 +00:00
|
|
|
|
$.parseHTML( renderPreview( model, showTitle, extractMsg, linkMsg ) )
|
|
|
|
|
);
|
2017-02-14 20:17:32 +00:00
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
el: $el,
|
|
|
|
|
hasThumbnail: false,
|
|
|
|
|
isTall: false
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-12 18:37:19 +00:00
|
|
|
|
/**
|
|
|
|
|
* @param {ext.popups.PreviewModel} model
|
|
|
|
|
* @return {ext.popups.Preview}
|
|
|
|
|
*/
|
|
|
|
|
function createReferencePreview( model ) {
|
|
|
|
|
const $el = $(
|
|
|
|
|
$.parseHTML( renderReferencePreview( model ) )
|
|
|
|
|
);
|
|
|
|
|
|
2019-01-28 11:04:40 +00:00
|
|
|
|
// Make sure to not destroy existing targets, if any
|
|
|
|
|
$el.find( '.mwe-popups-extract a[href]:not([target])' ).each( ( i, a ) => {
|
2019-01-24 13:48:18 +00:00
|
|
|
|
a.target = '_blank';
|
2019-01-28 11:04:40 +00:00
|
|
|
|
// Don't let the external site access and possibly manipulate window.opener.location
|
|
|
|
|
a.rel = `${ a.rel ? `${ a.rel } ` : '' }noopener`;
|
2019-01-24 13:48:18 +00:00
|
|
|
|
} );
|
|
|
|
|
|
2018-12-12 18:37:19 +00:00
|
|
|
|
return {
|
|
|
|
|
el: $el,
|
|
|
|
|
hasThumbnail: false,
|
|
|
|
|
isTall: false
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2017-02-14 20:17:32 +00:00
|
|
|
|
/**
|
|
|
|
|
* Shows the preview.
|
|
|
|
|
*
|
|
|
|
|
* Extracted from `mw.popups.render.openPopup`.
|
|
|
|
|
*
|
|
|
|
|
* TODO: From the perspective of the client, there's no need to distinguish
|
2018-03-13 22:03:49 +00:00
|
|
|
|
* between rendering and showing a preview. Merge #render and Preview#show.
|
2017-02-14 20:17:32 +00:00
|
|
|
|
*
|
|
|
|
|
* @param {ext.popups.Preview} preview
|
|
|
|
|
* @param {Event} event
|
2018-07-13 13:07:35 +00:00
|
|
|
|
* @param {JQuery} $link event target
|
2017-02-14 20:17:32 +00:00
|
|
|
|
* @param {ext.popups.PreviewBehavior} behavior
|
2018-07-13 13:07:35 +00:00
|
|
|
|
* @param {string} token
|
2018-04-26 13:25:55 +00:00
|
|
|
|
* @param {Object} container DOM object to which pointer masks are appended
|
2018-04-12 01:51:42 +00:00
|
|
|
|
* @param {string} dir 'ltr' if left-to-right, 'rtl' if right-to-left.
|
Update: cancel unused HTTP requests in flight
Whenever an HTTP request sequence is started, i.e. wait for the fetch
start time, issue a network request, and return the result, abort the
process if the results are known to no longer be needed. This occurs
when a user has dwelt upon one link and then abandoned it either during
the fetch start wait time or during the fetch network request itself.
This change is accomplished by preserving the pending promises in two
actions, LINK_DWELL and FETCH_START, and whenever the ABANDON_START
action is issued, it now aborts any previously pending XHR-like promise,
called a "AbortPromise" which is just a thenable with an abort() method.
There is a similar concept in Core:
https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/core/+/ecc812f06e7dff587b3f31dc18189adbf4616351/resources/src/mediawiki.api/index.js.
Aborting pending requests has big implications for client and server
logging as requests are quickly canceled, especially on slower
connections. These differences can be observed on the network tab of
DevTools and the log in Redux DevTools.
Consider, for instance, the scenario of dwelling upon and quickly
abandoning a single link prior to this patch:
BOOT EVENT_LOGGED LINK_DWELL FETCH_START ABANDON_START FETCH_END STATSV_LOGGED ABANDON_END EVENT_LOGGED FETCH_COMPLETE
And after this patch when the fetch timer is canceled (prior to an
actual network request):
BOOT EVENT_LOGGED LINK_DWELL ABANDON_START ABANDON_END EVENT_LOGGED
In the above sequence, FETCH_* and STATSV_LOGGED actions never occur.
And after this patch when the network request itself is canceled:
BOOT EVENT_LOGGED LINK_DWELL FETCH_START ABANDON_START FETCH_FAILED STATSV_LOGGED FETCH_COMPLETE ABANDON_END EVENT_LOGGED
FETCH_FAILED occurs intentionally, STATSV_LOGGED and FETCH_COMPLETE
still happen even though the fetch didn't complete successfully, and
FETCH_END doesn't.
Additionally, since less data is transmitted, it's possible that the
timing and success rate of logging will improve on low bandwidth
connections.
Also, this patch tries to revise the JSDocs where possible to support
type checking and fix a call to the missing assert.fail() function in
changeListener.test.js.
Bug: T197700
Change-Id: I9a73b3086fc8fb0edd897a347b5497d5362e20ef
2018-06-25 13:26:11 +00:00
|
|
|
|
* @return {JQuery.Promise<void>} A promise that resolves when the promise has
|
|
|
|
|
* faded in.
|
2017-02-14 20:17:32 +00:00
|
|
|
|
*/
|
2018-04-12 01:51:42 +00:00
|
|
|
|
export function show(
|
|
|
|
|
preview, event, $link, behavior, token, container, dir
|
2018-02-08 22:11:44 +00:00
|
|
|
|
) {
|
2018-03-19 19:39:41 +00:00
|
|
|
|
const layout = createLayout(
|
2017-05-08 16:51:42 +00:00
|
|
|
|
preview.isTall,
|
|
|
|
|
{
|
|
|
|
|
pageX: event.pageX,
|
|
|
|
|
pageY: event.pageY,
|
|
|
|
|
clientY: event.clientY
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
clientRects: $link.get( 0 ).getClientRects(),
|
|
|
|
|
offset: $link.offset(),
|
|
|
|
|
width: $link.width(),
|
|
|
|
|
height: $link.height()
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
scrollTop: $window.scrollTop(),
|
|
|
|
|
width: $window.width(),
|
|
|
|
|
height: $window.height()
|
|
|
|
|
},
|
2018-04-26 13:25:55 +00:00
|
|
|
|
pointerSize,
|
2018-04-12 01:51:42 +00:00
|
|
|
|
dir
|
2017-05-08 16:51:42 +00:00
|
|
|
|
);
|
2017-02-14 20:17:32 +00:00
|
|
|
|
|
2017-05-08 16:51:42 +00:00
|
|
|
|
preview.el.appendTo( container );
|
2017-02-14 20:17:32 +00:00
|
|
|
|
|
2017-05-05 19:36:04 +00:00
|
|
|
|
layoutPreview(
|
|
|
|
|
preview, layout, getClasses( preview, layout ),
|
2018-04-26 13:25:55 +00:00
|
|
|
|
SIZES.landscapeImage.h, pointerSize
|
2017-05-05 19:36:04 +00:00
|
|
|
|
);
|
2017-02-14 20:17:32 +00:00
|
|
|
|
|
2017-03-22 10:45:37 +00:00
|
|
|
|
preview.el.show();
|
|
|
|
|
|
|
|
|
|
return wait( 200 )
|
2018-03-14 23:50:09 +00:00
|
|
|
|
.then( () => {
|
2017-03-22 10:45:37 +00:00
|
|
|
|
bindBehavior( preview, behavior );
|
2017-04-10 17:28:24 +00:00
|
|
|
|
behavior.previewShow( token );
|
|
|
|
|
} );
|
2017-03-22 10:45:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Binds the behavior to the interactive elements of the preview.
|
|
|
|
|
*
|
|
|
|
|
* @param {ext.popups.Preview} preview
|
|
|
|
|
* @param {ext.popups.PreviewBehavior} behavior
|
2018-07-09 16:56:45 +00:00
|
|
|
|
* @return {void}
|
2017-03-22 10:45:37 +00:00
|
|
|
|
*/
|
2017-07-28 17:32:46 +00:00
|
|
|
|
export function bindBehavior( preview, behavior ) {
|
2017-12-05 22:21:42 +00:00
|
|
|
|
preview.el.on( 'mouseenter', behavior.previewDwell )
|
|
|
|
|
.on( 'mouseleave', behavior.previewAbandon );
|
2017-02-14 20:17:32 +00:00
|
|
|
|
|
2018-08-08 21:22:50 +00:00
|
|
|
|
preview.el.click( behavior.click );
|
2017-03-22 14:54:04 +00:00
|
|
|
|
|
2017-02-14 20:17:32 +00:00
|
|
|
|
preview.el.find( '.mwe-popups-settings-icon' )
|
|
|
|
|
.attr( 'href', behavior.settingsUrl )
|
2018-03-14 23:50:09 +00:00
|
|
|
|
.click( ( event ) => {
|
2017-03-22 14:54:04 +00:00
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
2017-04-05 23:56:12 +00:00
|
|
|
|
behavior.showSettings( event );
|
2017-03-22 14:54:04 +00:00
|
|
|
|
} );
|
2017-02-14 20:17:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Extracted from `mw.popups.render.closePopup`.
|
|
|
|
|
*
|
|
|
|
|
* @param {ext.popups.Preview} preview
|
Update: cancel unused HTTP requests in flight
Whenever an HTTP request sequence is started, i.e. wait for the fetch
start time, issue a network request, and return the result, abort the
process if the results are known to no longer be needed. This occurs
when a user has dwelt upon one link and then abandoned it either during
the fetch start wait time or during the fetch network request itself.
This change is accomplished by preserving the pending promises in two
actions, LINK_DWELL and FETCH_START, and whenever the ABANDON_START
action is issued, it now aborts any previously pending XHR-like promise,
called a "AbortPromise" which is just a thenable with an abort() method.
There is a similar concept in Core:
https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/core/+/ecc812f06e7dff587b3f31dc18189adbf4616351/resources/src/mediawiki.api/index.js.
Aborting pending requests has big implications for client and server
logging as requests are quickly canceled, especially on slower
connections. These differences can be observed on the network tab of
DevTools and the log in Redux DevTools.
Consider, for instance, the scenario of dwelling upon and quickly
abandoning a single link prior to this patch:
BOOT EVENT_LOGGED LINK_DWELL FETCH_START ABANDON_START FETCH_END STATSV_LOGGED ABANDON_END EVENT_LOGGED FETCH_COMPLETE
And after this patch when the fetch timer is canceled (prior to an
actual network request):
BOOT EVENT_LOGGED LINK_DWELL ABANDON_START ABANDON_END EVENT_LOGGED
In the above sequence, FETCH_* and STATSV_LOGGED actions never occur.
And after this patch when the network request itself is canceled:
BOOT EVENT_LOGGED LINK_DWELL FETCH_START ABANDON_START FETCH_FAILED STATSV_LOGGED FETCH_COMPLETE ABANDON_END EVENT_LOGGED
FETCH_FAILED occurs intentionally, STATSV_LOGGED and FETCH_COMPLETE
still happen even though the fetch didn't complete successfully, and
FETCH_END doesn't.
Additionally, since less data is transmitted, it's possible that the
timing and success rate of logging will improve on low bandwidth
connections.
Also, this patch tries to revise the JSDocs where possible to support
type checking and fix a call to the missing assert.fail() function in
changeListener.test.js.
Bug: T197700
Change-Id: I9a73b3086fc8fb0edd897a347b5497d5362e20ef
2018-06-25 13:26:11 +00:00
|
|
|
|
* @return {JQuery.Promise<void>} A promise that resolves when the preview has
|
|
|
|
|
* faded out.
|
2017-02-14 20:17:32 +00:00
|
|
|
|
*/
|
2017-07-28 17:32:46 +00:00
|
|
|
|
export function hide( preview ) {
|
2017-02-14 20:17:32 +00:00
|
|
|
|
// FIXME: This method clearly needs access to the layout of the preview.
|
2018-03-19 19:39:41 +00:00
|
|
|
|
const fadeInClass = ( preview.el.hasClass( 'mwe-popups-fade-in-up' ) ) ?
|
2017-02-14 20:17:32 +00:00
|
|
|
|
'mwe-popups-fade-in-up' :
|
|
|
|
|
'mwe-popups-fade-in-down';
|
|
|
|
|
|
2018-03-19 19:39:41 +00:00
|
|
|
|
const fadeOutClass = ( fadeInClass === 'mwe-popups-fade-in-up' ) ?
|
2017-02-14 20:17:32 +00:00
|
|
|
|
'mwe-popups-fade-out-down' :
|
|
|
|
|
'mwe-popups-fade-out-up';
|
|
|
|
|
|
|
|
|
|
preview.el
|
|
|
|
|
.removeClass( fadeInClass )
|
|
|
|
|
.addClass( fadeOutClass );
|
|
|
|
|
|
2018-03-14 23:50:09 +00:00
|
|
|
|
return wait( 150 ).then( () => {
|
2017-02-14 20:17:32 +00:00
|
|
|
|
preview.el.remove();
|
|
|
|
|
} );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
2017-05-05 15:57:52 +00:00
|
|
|
|
* @property {number} offset.top
|
|
|
|
|
* @property {number} offset.left
|
2018-07-13 13:07:35 +00:00
|
|
|
|
* @property {boolean} flippedX
|
|
|
|
|
* @property {boolean} flippedY
|
2018-04-12 01:51:42 +00:00
|
|
|
|
* @property {string} dir 'ltr' if left-to-right, 'rtl' if right-to-left.
|
2017-02-14 20:17:32 +00:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
2018-07-13 13:07:35 +00:00
|
|
|
|
* @param {boolean} isPreviewTall
|
2018-01-18 18:48:16 +00:00
|
|
|
|
* @param {Object} eventData Data related to the event that triggered showing
|
|
|
|
|
* a popup
|
2017-05-05 15:57:52 +00:00
|
|
|
|
* @param {number} eventData.pageX
|
|
|
|
|
* @param {number} eventData.pageY
|
|
|
|
|
* @param {number} eventData.clientY
|
2018-01-18 18:48:16 +00:00
|
|
|
|
* @param {Object} linkData Data related to the link that’s used for showing
|
|
|
|
|
* a popup
|
|
|
|
|
* @param {ClientRectList} linkData.clientRects list of rectangles defined by
|
|
|
|
|
* four edges
|
2017-05-05 15:57:52 +00:00
|
|
|
|
* @param {Object} linkData.offset
|
|
|
|
|
* @param {number} linkData.width
|
|
|
|
|
* @param {number} linkData.height
|
|
|
|
|
* @param {Object} windowData Data related to the window
|
|
|
|
|
* @param {number} windowData.scrollTop
|
|
|
|
|
* @param {number} windowData.width
|
|
|
|
|
* @param {number} windowData.height
|
2018-04-26 13:25:55 +00:00
|
|
|
|
* @param {number} pointerSize Space reserved for the pointer
|
2018-04-12 01:51:42 +00:00
|
|
|
|
* @param {string} dir 'ltr' if left-to-right, 'rtl' if right-to-left.
|
2017-02-14 20:17:32 +00:00
|
|
|
|
* @return {ext.popups.PreviewLayout}
|
|
|
|
|
*/
|
2018-01-18 18:48:16 +00:00
|
|
|
|
export function createLayout(
|
2018-04-26 13:25:55 +00:00
|
|
|
|
isPreviewTall, eventData, linkData, windowData, pointerSize, dir
|
2018-01-18 18:48:16 +00:00
|
|
|
|
) {
|
2018-03-15 16:30:11 +00:00
|
|
|
|
let flippedX = false,
|
2017-02-14 20:17:32 +00:00
|
|
|
|
flippedY = false,
|
2017-10-09 14:56:15 +00:00
|
|
|
|
offsetTop = eventData.pageY ?
|
|
|
|
|
// If it was a mouse event, position according to mouse
|
2017-02-14 20:17:32 +00:00
|
|
|
|
// Since client rectangles are relative to the viewport,
|
|
|
|
|
// take scroll position into account.
|
|
|
|
|
getClosestYPosition(
|
2017-05-05 15:57:52 +00:00
|
|
|
|
eventData.pageY - windowData.scrollTop,
|
|
|
|
|
linkData.clientRects,
|
2017-02-14 20:17:32 +00:00
|
|
|
|
false
|
2018-04-26 13:25:55 +00:00
|
|
|
|
) + windowData.scrollTop + pointerSize :
|
2017-02-14 20:17:32 +00:00
|
|
|
|
// Position according to link position or size
|
2018-04-26 13:25:55 +00:00
|
|
|
|
linkData.offset.top + linkData.height + pointerSize,
|
2018-03-19 19:39:41 +00:00
|
|
|
|
offsetLeft = eventData.pageX ? eventData.pageX : linkData.offset.left;
|
|
|
|
|
const clientTop = eventData.clientY ? eventData.clientY : offsetTop;
|
2017-02-14 20:17:32 +00:00
|
|
|
|
|
|
|
|
|
// X Flip
|
2017-05-05 15:57:52 +00:00
|
|
|
|
if ( offsetLeft > ( windowData.width / 2 ) ) {
|
|
|
|
|
offsetLeft += ( !eventData.pageX ) ? linkData.width : 0;
|
|
|
|
|
offsetLeft -= !isPreviewTall ?
|
2018-03-08 20:38:23 +00:00
|
|
|
|
portraitPopupWidth :
|
|
|
|
|
landscapePopupWidth;
|
2017-02-14 20:17:32 +00:00
|
|
|
|
flippedX = true;
|
2016-11-21 12:07:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
2017-05-05 15:57:52 +00:00
|
|
|
|
if ( eventData.pageX ) {
|
2017-02-14 20:17:32 +00:00
|
|
|
|
offsetLeft += ( flippedX ) ? 20 : -20;
|
2016-11-21 12:07:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
2017-02-14 20:17:32 +00:00
|
|
|
|
// Y Flip
|
2017-05-05 15:57:52 +00:00
|
|
|
|
if ( clientTop > ( windowData.height / 2 ) ) {
|
2017-02-14 20:17:32 +00:00
|
|
|
|
flippedY = true;
|
|
|
|
|
|
|
|
|
|
// Mirror the positioning of the preview when there's no "Y flip": rest
|
2018-04-26 13:25:55 +00:00
|
|
|
|
// the pointer on the edge of the link's bounding rectangle. In this case
|
2017-02-14 20:17:32 +00:00
|
|
|
|
// the edge is the top-most.
|
2017-05-05 15:57:52 +00:00
|
|
|
|
offsetTop = linkData.offset.top;
|
2017-02-14 20:17:32 +00:00
|
|
|
|
|
|
|
|
|
// Change the Y position to the top of the link
|
2017-05-05 15:57:52 +00:00
|
|
|
|
if ( eventData.pageY ) {
|
2017-02-14 20:17:32 +00:00
|
|
|
|
// Since client rectangles are relative to the viewport,
|
|
|
|
|
// take scroll position into account.
|
|
|
|
|
offsetTop = getClosestYPosition(
|
2017-05-05 15:57:52 +00:00
|
|
|
|
eventData.pageY - windowData.scrollTop,
|
|
|
|
|
linkData.clientRects,
|
2017-02-14 20:17:32 +00:00
|
|
|
|
true
|
2017-05-05 15:57:52 +00:00
|
|
|
|
) + windowData.scrollTop;
|
2016-11-21 12:07:23 +00:00
|
|
|
|
}
|
2017-04-20 15:30:54 +00:00
|
|
|
|
|
2018-04-26 13:25:55 +00:00
|
|
|
|
offsetTop -= pointerSize;
|
2016-11-21 12:07:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
2017-02-14 20:17:32 +00:00
|
|
|
|
return {
|
|
|
|
|
offset: {
|
|
|
|
|
top: offsetTop,
|
|
|
|
|
left: offsetLeft
|
|
|
|
|
},
|
2018-04-12 01:51:42 +00:00
|
|
|
|
flippedX: dir === 'rtl' ? !flippedX : flippedX,
|
|
|
|
|
flippedY,
|
|
|
|
|
dir
|
2017-02-14 20:17:32 +00:00
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generates a list of declarative CSS classes that represent the layout of
|
|
|
|
|
* the preview.
|
|
|
|
|
*
|
|
|
|
|
* @param {ext.popups.Preview} preview
|
|
|
|
|
* @param {ext.popups.PreviewLayout} layout
|
2018-07-13 13:07:35 +00:00
|
|
|
|
* @return {string[]}
|
2017-02-14 20:17:32 +00:00
|
|
|
|
*/
|
2017-07-28 17:32:46 +00:00
|
|
|
|
export function getClasses( preview, layout ) {
|
2018-03-19 19:39:41 +00:00
|
|
|
|
const classes = [];
|
2017-02-14 20:17:32 +00:00
|
|
|
|
|
|
|
|
|
if ( layout.flippedY ) {
|
|
|
|
|
classes.push( 'mwe-popups-fade-in-down' );
|
|
|
|
|
} else {
|
|
|
|
|
classes.push( 'mwe-popups-fade-in-up' );
|
2016-11-21 12:07:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
2017-02-14 20:17:32 +00:00
|
|
|
|
if ( layout.flippedY && layout.flippedX ) {
|
2018-04-23 21:23:17 +00:00
|
|
|
|
classes.push( 'flipped-x-y' );
|
2018-04-11 20:40:20 +00:00
|
|
|
|
} else if ( layout.flippedY ) {
|
2018-04-23 21:23:17 +00:00
|
|
|
|
classes.push( 'flipped-y' );
|
2018-04-11 20:40:20 +00:00
|
|
|
|
} else if ( layout.flippedX ) {
|
2018-04-23 21:23:17 +00:00
|
|
|
|
classes.push( 'flipped-x' );
|
2017-02-14 20:17:32 +00:00
|
|
|
|
}
|
2016-11-21 12:07:23 +00:00
|
|
|
|
|
2018-04-13 17:29:57 +00:00
|
|
|
|
if ( ( !preview.hasThumbnail || preview.isTall && !layout.flippedX ) &&
|
|
|
|
|
!layout.flippedY ) {
|
2018-04-26 13:25:55 +00:00
|
|
|
|
classes.push( 'mwe-popups-no-image-pointer' );
|
2016-11-21 12:07:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-04-12 01:51:42 +00:00
|
|
|
|
if ( preview.hasThumbnail && !preview.isTall && !layout.flippedY ) {
|
2018-04-26 13:25:55 +00:00
|
|
|
|
classes.push( 'mwe-popups-image-pointer' );
|
2017-02-14 20:17:32 +00:00
|
|
|
|
}
|
2016-11-21 12:07:23 +00:00
|
|
|
|
|
2017-02-14 20:17:32 +00:00
|
|
|
|
if ( preview.isTall ) {
|
|
|
|
|
classes.push( 'mwe-popups-is-tall' );
|
|
|
|
|
} else {
|
|
|
|
|
classes.push( 'mwe-popups-is-not-tall' );
|
|
|
|
|
}
|
2016-11-21 12:07:23 +00:00
|
|
|
|
|
2017-02-14 20:17:32 +00:00
|
|
|
|
return classes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Lays out the preview given the layout.
|
|
|
|
|
*
|
|
|
|
|
* 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
|
2017-05-05 19:36:04 +00:00
|
|
|
|
* @param {string[]} classes class names used for layout out the preview
|
|
|
|
|
* @param {number} predefinedLandscapeImageHeight landscape image height
|
2018-04-26 13:25:55 +00:00
|
|
|
|
* @param {number} pointerSize
|
2018-04-13 17:29:57 +00:00
|
|
|
|
* @return {void}
|
2017-02-14 20:17:32 +00:00
|
|
|
|
*/
|
2018-01-18 18:48:16 +00:00
|
|
|
|
export function layoutPreview(
|
2018-04-26 13:25:55 +00:00
|
|
|
|
preview, layout, classes, predefinedLandscapeImageHeight, pointerSize
|
2018-01-18 18:48:16 +00:00
|
|
|
|
) {
|
2018-03-19 19:39:41 +00:00
|
|
|
|
const popup = preview.el,
|
2017-02-14 20:17:32 +00:00
|
|
|
|
isTall = preview.isTall,
|
|
|
|
|
hasThumbnail = preview.hasThumbnail,
|
|
|
|
|
thumbnail = preview.thumbnail,
|
2018-04-13 17:29:57 +00:00
|
|
|
|
flippedY = layout.flippedY;
|
2018-03-19 19:39:41 +00:00
|
|
|
|
let offsetTop = layout.offset.top;
|
2017-02-14 20:17:32 +00:00
|
|
|
|
|
2017-05-05 19:36:04 +00:00
|
|
|
|
if (
|
|
|
|
|
!flippedY && !isTall && hasThumbnail &&
|
|
|
|
|
thumbnail.height < predefinedLandscapeImageHeight
|
|
|
|
|
) {
|
|
|
|
|
popup.find( '.mwe-popups-extract' ).css(
|
2017-02-14 20:17:32 +00:00
|
|
|
|
'margin-top',
|
2018-04-26 13:25:55 +00:00
|
|
|
|
thumbnail.height - pointerSize
|
2017-02-14 20:17:32 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
2016-11-21 12:07:23 +00:00
|
|
|
|
|
2017-05-05 19:36:04 +00:00
|
|
|
|
popup.addClass( classes.join( ' ' ) );
|
2016-11-21 12:07:23 +00:00
|
|
|
|
|
2017-02-14 20:17:32 +00:00
|
|
|
|
if ( flippedY ) {
|
|
|
|
|
offsetTop -= popup.outerHeight();
|
|
|
|
|
}
|
2016-11-21 12:07:23 +00:00
|
|
|
|
|
2017-02-14 20:17:32 +00:00
|
|
|
|
popup.css( {
|
|
|
|
|
top: offsetTop,
|
2018-03-20 17:01:18 +00:00
|
|
|
|
left: `${ layout.offset.left }px`
|
2017-02-14 20:17:32 +00:00
|
|
|
|
} );
|
2016-11-21 12:07:23 +00:00
|
|
|
|
|
2018-04-13 17:29:57 +00:00
|
|
|
|
if ( hasThumbnail ) {
|
|
|
|
|
setThumbnailClipPath( preview, layout );
|
2016-11-21 12:07:23 +00:00
|
|
|
|
}
|
2018-04-13 17:29:57 +00:00
|
|
|
|
}
|
2016-11-21 12:07:23 +00:00
|
|
|
|
|
2018-04-13 17:29:57 +00:00
|
|
|
|
/**
|
|
|
|
|
* Sets the thumbnail SVG clip-path.
|
|
|
|
|
*
|
2018-04-26 13:25:55 +00:00
|
|
|
|
* If the preview should be oriented differently, then the pointer is updated,
|
|
|
|
|
* e.g. if the preview should be flipped vertically, then the pointer is
|
2018-04-13 17:29:57 +00:00
|
|
|
|
* removed.
|
|
|
|
|
*
|
|
|
|
|
* Note: SVG clip-paths are supported everywhere but clip-paths as CSS
|
2018-04-12 01:51:42 +00:00
|
|
|
|
* properties are not (https://caniuse.com/#feat=css-clip-path). For this
|
|
|
|
|
* reason, RTL flipping is handled in JavaScript instead of CSS.
|
2018-04-13 17:29:57 +00:00
|
|
|
|
*
|
|
|
|
|
* @param {ext.popups.Preview} preview
|
|
|
|
|
* @param {ext.popups.PreviewLayout} layout
|
|
|
|
|
* @return {void}
|
|
|
|
|
*/
|
2018-04-12 01:51:42 +00:00
|
|
|
|
export function setThumbnailClipPath(
|
|
|
|
|
{ el, isTall }, { flippedY, flippedX, dir }
|
|
|
|
|
) {
|
|
|
|
|
const maskID = getThumbnailClipPathID( isTall, flippedY, flippedX );
|
|
|
|
|
if ( maskID ) {
|
|
|
|
|
let entries; // = ⎡ a c tx ⎤
|
|
|
|
|
// ⎣ b d ty ⎦
|
|
|
|
|
if ( dir === 'rtl' ) {
|
|
|
|
|
// Flip and translate.
|
|
|
|
|
const tx = isTall ? SIZES.portraitImage.w : SIZES.landscapeImage.w;
|
|
|
|
|
entries = `-1 0 0 1 ${tx} 0`;
|
|
|
|
|
} else {
|
|
|
|
|
// Identity.
|
|
|
|
|
entries = '1 0 0 1 0 0';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Transform the clip-path not the image it is applied to.
|
|
|
|
|
const mask = document.getElementById( maskID );
|
|
|
|
|
mask.setAttribute( 'transform', `matrix(${entries})` );
|
|
|
|
|
|
|
|
|
|
el.find( 'image' )[ 0 ]
|
|
|
|
|
.setAttribute( 'clip-path', `url(#${maskID})` );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Gets the thumbnail SVG clip-path element ID.
|
|
|
|
|
*
|
2018-07-13 13:07:35 +00:00
|
|
|
|
* @param {boolean} isTall Sugar around
|
2018-04-12 01:51:42 +00:00
|
|
|
|
* `preview.hasThumbnail && thumbnail.isTall`
|
2018-07-13 13:07:35 +00:00
|
|
|
|
* @param {boolean} flippedY
|
|
|
|
|
* @param {boolean} flippedX
|
2018-04-12 01:51:42 +00:00
|
|
|
|
* @return {string|undefined}
|
|
|
|
|
*/
|
|
|
|
|
export function getThumbnailClipPathID( isTall, flippedY, flippedX ) {
|
2018-04-13 17:29:57 +00:00
|
|
|
|
if ( flippedX && !flippedY ) {
|
2018-04-12 01:51:42 +00:00
|
|
|
|
return isTall ? 'mwe-popups-landscape-mask' : 'mwe-popups-mask-flip';
|
2018-04-13 17:29:57 +00:00
|
|
|
|
} else if ( flippedY && flippedX && isTall ) {
|
2018-04-12 01:51:42 +00:00
|
|
|
|
return 'mwe-popups-landscape-mask-flip';
|
2018-04-13 17:29:57 +00:00
|
|
|
|
} else if ( !flippedY && !isTall ) {
|
2018-04-12 01:51:42 +00:00
|
|
|
|
return 'mwe-popups-mask';
|
2016-11-21 12:07:23 +00:00
|
|
|
|
}
|
2018-04-12 01:51:42 +00:00
|
|
|
|
return undefined;
|
2017-02-14 20:17:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*
|
2017-04-27 16:03:44 +00:00
|
|
|
|
* @private
|
2018-07-13 13:07:35 +00:00
|
|
|
|
* @param {number} y the point for which the closest location is being
|
2017-02-14 20:17:32 +00:00
|
|
|
|
* looked for
|
|
|
|
|
* @param {ClientRectList} rects list of rectangles defined by four edges
|
2018-07-13 13:07:35 +00:00
|
|
|
|
* @param {boolean} [isTop] should the resulting rectangle's top 'y'
|
2017-02-14 20:17:32 +00:00
|
|
|
|
* boundary be returned. By default the bottom 'y' value is returned.
|
2018-07-13 13:07:35 +00:00
|
|
|
|
* @return {number}
|
2017-02-14 20:17:32 +00:00
|
|
|
|
*/
|
2017-07-28 17:32:46 +00:00
|
|
|
|
export function getClosestYPosition( y, rects, isTop ) {
|
2018-03-19 19:39:41 +00:00
|
|
|
|
let minY = null, result;
|
2017-02-14 20:17:32 +00:00
|
|
|
|
|
2018-07-06 15:41:19 +00:00
|
|
|
|
Array.prototype.slice.call( rects ).forEach( ( rect ) => {
|
2018-03-19 19:39:41 +00:00
|
|
|
|
const deltaY = Math.abs( y - rect.top + y - rect.bottom );
|
2017-02-14 20:17:32 +00:00
|
|
|
|
|
|
|
|
|
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 );
|
|
|
|
|
}
|
|
|
|
|
} );
|
2016-11-21 12:07:23 +00:00
|
|
|
|
|
2017-02-14 20:17:32 +00:00
|
|
|
|
return result;
|
|
|
|
|
}
|