mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/MultimediaViewer
synced 2024-12-01 03:06:17 +00:00
77bfbf61aa
Bug: T111112 Change-Id: I1c3a31ddaefdfec0d86b80891671316741e53b0c
459 lines
14 KiB
JavaScript
459 lines
14 KiB
JavaScript
/*
|
||
* This file is part of the MediaWiki extension MediaViewer.
|
||
*
|
||
* MediaViewer is free software: you can redistribute it and/or modify
|
||
* it under the terms of the GNU General Public License as published by
|
||
* the Free Software Foundation, either version 2 of the License, or
|
||
* (at your option) any later version.
|
||
*
|
||
* MediaViewer is distributed in the hope that it will be useful,
|
||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
* GNU General Public License for more details.
|
||
*
|
||
* You should have received a copy of the GNU General Public License
|
||
* along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
|
||
*/
|
||
|
||
( function ( mw, $, oo ) {
|
||
var C;
|
||
|
||
/**
|
||
* UI component that contains the multimedia element to be displayed.
|
||
* This first version assumes an image but it can be extended to other
|
||
* media types (video, sound, presentation, etc.).
|
||
*
|
||
* @class mw.mmv.ui.Canvas
|
||
* @extends mw.mmv.ui.Element
|
||
* @constructor
|
||
* @param {jQuery} $container Canvas' container
|
||
* @param {jQuery} $imageWrapper
|
||
* @param {jQuery} $mainWrapper
|
||
*/
|
||
function Canvas( $container, $imageWrapper, $mainWrapper ) {
|
||
mw.mmv.ui.Element.call( this, $container );
|
||
|
||
/**
|
||
* @property {boolean}
|
||
* @private
|
||
*/
|
||
this.dialogOpen = false;
|
||
|
||
/**
|
||
* @property {mw.mmv.ThumbnailWidthCalculator}
|
||
* @private
|
||
*/
|
||
this.thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator();
|
||
|
||
/**
|
||
* Contains image.
|
||
* @property {jQuery}
|
||
*/
|
||
this.$imageDiv = $( '<div>' )
|
||
.addClass( 'mw-mmv-image' );
|
||
|
||
this.$imageDiv.appendTo( this.$container );
|
||
|
||
/**
|
||
* Container of canvas and controls, needed for canvas size calculations.
|
||
* @property {jQuery}
|
||
* @private
|
||
*/
|
||
this.$imageWrapper = $imageWrapper;
|
||
|
||
/**
|
||
* Main container of image and metadata, needed to propagate events.
|
||
* @property {jQuery}
|
||
* @private
|
||
*/
|
||
this.$mainWrapper = $mainWrapper;
|
||
|
||
/**
|
||
* Raw metadata of current image, needed for canvas size calculations.
|
||
* @property {mw.mmv.LightboxImage}
|
||
* @private
|
||
*/
|
||
this.imageRawMetadata = null;
|
||
}
|
||
oo.inheritClass( Canvas, mw.mmv.ui.Element );
|
||
C = Canvas.prototype;
|
||
|
||
/**
|
||
* Maximum blownup factor tolerated
|
||
* @property MAX_BLOWUP_FACTOR
|
||
* @static
|
||
*/
|
||
Canvas.MAX_BLOWUP_FACTOR = 11;
|
||
|
||
/**
|
||
* Blowup factor threshold at which blurring kicks in
|
||
* @property BLUR_BLOWUP_FACTOR_THRESHOLD
|
||
* @static
|
||
*/
|
||
Canvas.BLUR_BLOWUP_FACTOR_THRESHOLD = 2;
|
||
|
||
/**
|
||
* Clears everything.
|
||
*/
|
||
C.empty = function () {
|
||
this.$imageDiv.addClass( 'empty' ).removeClass( 'error' );
|
||
|
||
this.$imageDiv.empty();
|
||
};
|
||
|
||
/**
|
||
* Sets image on the canvas; does not resize it to fit. This is used to make the placeholder
|
||
* image available; it will be resized and displayed by #maybeDisplayPlaceholder().
|
||
* FIXME maybeDisplayPlaceholder() receives the placeholder so it is very unclear why this
|
||
* is necessary at all (apart from setting the LightboxImage, which is used in size calculations).
|
||
* @param {mw.mmv.LightboxImage} imageRawMetadata
|
||
* @param {jQuery} $imageElement
|
||
*/
|
||
C.set = function ( imageRawMetadata, $imageElement ) {
|
||
this.$imageDiv.removeClass( 'empty' );
|
||
|
||
this.imageRawMetadata = imageRawMetadata;
|
||
this.$image = $imageElement;
|
||
this.setUpImageClick();
|
||
|
||
this.$imageDiv.html( this.$image );
|
||
};
|
||
|
||
/**
|
||
* Resizes image to the given dimensions and displays it on the canvas.
|
||
* This is used to display the actual image; it assumes set function was already called before.
|
||
* @param {mw.mmv.model.Thumbnail} thumbnail thumbnail information
|
||
* @param {HTMLImageElement} imageElement
|
||
* @param {mw.mmv.model.ThumbnailWidth} imageWidths
|
||
*/
|
||
C.setImageAndMaxDimensions = function ( thumbnail, imageElement, imageWidths ) {
|
||
var $image = $( imageElement );
|
||
|
||
// we downscale larger images but do not scale up smaller ones, that would look ugly
|
||
if ( thumbnail.width > imageWidths.cssWidth ) {
|
||
imageElement.width = imageWidths.cssWidth;
|
||
imageElement.height = imageWidths.cssHeight;
|
||
}
|
||
|
||
if ( !this.$image.is( imageElement ) ) { // http://bugs.jquery.com/ticket/4087
|
||
this.$image.replaceWith( $image );
|
||
this.$image = $image;
|
||
|
||
// Since the image element got replaced, we need to rescue the dialog-open class.
|
||
this.$image.toggleClass( 'mw-mmv-dialog-is-open', this.dialogOpen );
|
||
|
||
this.setUpImageClick();
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Handles a "dialog open/close" event from dialogs on the page.
|
||
*/
|
||
C.handleDialogEvent = function ( e ) {
|
||
switch ( e.type ) {
|
||
case 'mmv-download-opened':
|
||
this.downloadOpen = true;
|
||
break;
|
||
case 'mmv-download-closed':
|
||
this.downloadOpen = false;
|
||
break;
|
||
case 'mmv-reuse-opened':
|
||
this.reuseOpen = true;
|
||
break;
|
||
case 'mmv-reuse-closed':
|
||
this.reuseOpen = false;
|
||
break;
|
||
case 'mmv-options-opened':
|
||
this.optionsOpen = true;
|
||
break;
|
||
case 'mmv-options-closed':
|
||
this.optionsOpen = false;
|
||
break;
|
||
}
|
||
|
||
this.dialogOpen = this.reuseOpen || this.downloadOpen || this.optionsOpen;
|
||
this.$image.toggleClass( 'mw-mmv-dialog-is-open', this.dialogOpen );
|
||
};
|
||
|
||
/**
|
||
* Registers click listener on the image.
|
||
*/
|
||
C.setUpImageClick = function () {
|
||
var canvas = this;
|
||
|
||
this.handleEvent( 'mmv-reuse-opened', $.proxy( this.handleDialogEvent, this ) );
|
||
this.handleEvent( 'mmv-reuse-closed', $.proxy( this.handleDialogEvent, this ) );
|
||
this.handleEvent( 'mmv-download-opened', $.proxy( this.handleDialogEvent, this ) );
|
||
this.handleEvent( 'mmv-download-closed', $.proxy( this.handleDialogEvent, this ) );
|
||
this.handleEvent( 'mmv-options-opened', $.proxy( this.handleDialogEvent, this ) );
|
||
this.handleEvent( 'mmv-options-closed', $.proxy( this.handleDialogEvent, this ) );
|
||
|
||
this.$image.on( 'click.mmv-canvas', function ( e ) {
|
||
// ignore clicks if the metadata panel or one of the dialogs is open - assume the intent is to
|
||
// close it in this case; that will be handled elsewhere
|
||
if (
|
||
!canvas.dialogOpen
|
||
// FIXME a UI component should not know about its parents
|
||
&& canvas.$container.closest( '.metadata-panel-is-open' ).length === 0
|
||
) {
|
||
e.stopPropagation(); // don't let $imageWrapper handle this
|
||
mw.mmv.actionLogger.log( 'view-original-file' ).always( function () {
|
||
$( document ).trigger( 'mmv-viewfile' );
|
||
} );
|
||
}
|
||
} );
|
||
|
||
// open the download panel on right clicking the image
|
||
this.$image.on( 'mousedown.mmv-canvas', function ( e ) {
|
||
if ( e.which === 3 ) {
|
||
mw.mmv.actionLogger.log( 'right-click-image' );
|
||
if ( !canvas.downloadOpen ) {
|
||
$( document ).trigger( 'mmv-download-open', e );
|
||
e.stopPropagation();
|
||
}
|
||
}
|
||
} );
|
||
};
|
||
|
||
/**
|
||
* Registers listeners.
|
||
*/
|
||
C.attach = function () {
|
||
var canvas = this;
|
||
|
||
$( window ).on( 'resize.mmv-canvas', $.debounce( 100, function () {
|
||
canvas.$mainWrapper.trigger( $.Event( 'mmv-resize-end' ) );
|
||
} ) );
|
||
|
||
this.$imageWrapper.on( 'click.mmv-canvas', function () {
|
||
if ( canvas.$container.closest( '.metadata-panel-is-open' ).length > 0 ) {
|
||
canvas.$mainWrapper.trigger( 'mmv-panel-close-area-click' );
|
||
}
|
||
} );
|
||
};
|
||
|
||
/**
|
||
* Clears listeners.
|
||
*/
|
||
C.unattach = function () {
|
||
this.clearEvents();
|
||
|
||
$( window ).off( 'resize.mmv-canvas' );
|
||
|
||
this.$imageWrapper.off( 'click.mmv-canvas' );
|
||
};
|
||
|
||
/**
|
||
* Sets page thumbnail for display if blowupFactor <= MAX_BLOWUP_FACTOR. Otherwise thumb is not set.
|
||
* The image gets also blured to avoid pixelation if blowupFactor > BLUR_BLOWUP_FACTOR_THRESHOLD.
|
||
* We set SVG files to the maximum screen size available.
|
||
* Assumes set function called before.
|
||
*
|
||
* @param {{width: number, height: number}} size
|
||
* @param {jQuery} $imagePlaceholder Image placeholder to be displayed while the real image loads.
|
||
* @param {mw.mmv.model.ThumbnailWidth} imageWidths
|
||
* @returns {boolean} Whether the image was blured or not
|
||
*/
|
||
C.maybeDisplayPlaceholder = function ( size, $imagePlaceholder, imageWidths ) {
|
||
var targetWidth,
|
||
targetHeight,
|
||
blowupFactor,
|
||
blurredThumbnailShown = false;
|
||
|
||
// Assume natural thumbnail size¸
|
||
targetWidth = size.width;
|
||
targetHeight = size.height;
|
||
|
||
// If the image is bigger than the screen we need to resize it
|
||
if ( size.width > imageWidths.cssWidth ) { // This assumes imageInfo.width in CSS units
|
||
targetWidth = imageWidths.cssWidth;
|
||
targetHeight = imageWidths.cssHeight;
|
||
}
|
||
|
||
blowupFactor = targetWidth / $imagePlaceholder.width();
|
||
|
||
// If the placeholder is too blown up, it's not worth showing it
|
||
if ( blowupFactor > Canvas.MAX_BLOWUP_FACTOR ) {
|
||
return blurredThumbnailShown;
|
||
}
|
||
|
||
$imagePlaceholder.width( targetWidth );
|
||
$imagePlaceholder.height( targetHeight );
|
||
|
||
// Only blur the placeholder if it's blown up significantly
|
||
if ( blowupFactor > Canvas.BLUR_BLOWUP_FACTOR_THRESHOLD ) {
|
||
this.blur( $imagePlaceholder );
|
||
blurredThumbnailShown = true;
|
||
}
|
||
|
||
this.set( this.imageRawMetadata, $imagePlaceholder.show() );
|
||
|
||
return blurredThumbnailShown;
|
||
};
|
||
|
||
/**
|
||
* Blur image
|
||
* @param {jQuery} $image Image to be blurred.
|
||
*/
|
||
C.blur = function ( $image ) {
|
||
// We have to apply the SVG filter here, it doesn't work when defined in the .less file
|
||
// We can't use an external SVG file because filters can't be accessed cross-domain
|
||
// We can't embed the SVG file because accessing the filter inside of it doesn't work
|
||
$image.addClass( 'blurred' ).css( 'filter', 'url("#gaussian-blur")' );
|
||
};
|
||
|
||
/**
|
||
* Animates the image into focus
|
||
*/
|
||
C.unblurWithAnimation = function () {
|
||
var self = this,
|
||
animationLength = 300;
|
||
|
||
// The blurred class has an opacity < 1. This animated the image to become fully opaque
|
||
this.$image
|
||
.addClass( 'blurred' )
|
||
.animate( { opacity: 1.0 }, animationLength );
|
||
|
||
// During the same amount of time (animationLength) we animate a blur value from 3.0 to 0.0
|
||
// We pass that value to an inline CSS Gaussian blur effect
|
||
$( { blur: 3.0 } ).animate( { blur: 0.0 }, {
|
||
duration: animationLength,
|
||
step: function ( step ) {
|
||
self.$image.css( { '-webkit-filter' : 'blur(' + step + 'px)',
|
||
'filter' : 'blur(' + step + 'px)' } );
|
||
},
|
||
complete: function () {
|
||
// When the animation is complete, the blur value is 0, clean things up
|
||
self.unblur();
|
||
}
|
||
} );
|
||
};
|
||
|
||
C.unblur = function () {
|
||
// We apply empty CSS values to remove the inline styles applied by jQuery
|
||
// so that they don't get in the way of styles defined in CSS
|
||
this.$image.css( { '-webkit-filter' : '', 'opacity' : '', 'filter' : '' } )
|
||
.removeClass( 'blurred' );
|
||
};
|
||
|
||
/**
|
||
* Displays a message and error icon when loading the image fails.
|
||
* @param {string} error error message
|
||
*/
|
||
C.showError = function ( error ) {
|
||
var errorDetails, description, errorUri, retryLink, reportLink,
|
||
canvasDimensions = this.getDimensions(),
|
||
thumbnailDimensions = this.getCurrentImageWidths(),
|
||
htmlUtils = new mw.mmv.HtmlUtils();
|
||
|
||
errorDetails = [
|
||
'error: ' + error,
|
||
'URL: ' + location.href,
|
||
'user agent: ' + navigator.userAgent,
|
||
'screen size: ' + screen.width + 'x' + screen.height,
|
||
'canvas size: ' + canvasDimensions.width + 'x' + canvasDimensions.height,
|
||
'image size: ' + this.imageRawMetadata.originalWidth + 'x' + this.imageRawMetadata.originalHeight,
|
||
'thumbnail size: CSS: ' + thumbnailDimensions.cssWidth + 'x' + thumbnailDimensions.cssHeight
|
||
+ ', screen width: ' + thumbnailDimensions.screen + ', real width: ' + thumbnailDimensions.real
|
||
];
|
||
// ** is bolding in Phabricator
|
||
description = '**' + mw.message( 'multimediaviewer-errorreport-privacywarning' ).text() + '**\n\n\n'
|
||
+ 'Error details:\n\n' + errorDetails.join( '\n' );
|
||
errorUri = mw.msg( 'multimediaviewer-report-issue-url', encodeURIComponent( description ) );
|
||
|
||
retryLink = $( '<a>' ).addClass( 'mw-mmv-retry-link' ).text(
|
||
mw.msg( 'multimediaviewer-thumbnail-error-retry' ) );
|
||
reportLink = $( '<a>' ).attr( 'href', errorUri ).text(
|
||
mw.msg( 'multimediaviewer-thumbnail-error-report' ) );
|
||
|
||
this.$imageDiv.empty()
|
||
.addClass( 'error' )
|
||
.append(
|
||
$( '<div>' ).addClass( 'error-box' ).append(
|
||
$( '<div>' ).addClass( 'mw-mmv-error-text' ).text(
|
||
mw.msg( 'multimediaviewer-thumbnail-error' )
|
||
)
|
||
).append(
|
||
$( '<div>' ).addClass( 'mw-mmv-error-description' ).append(
|
||
mw.msg( 'multimediaviewer-thumbnail-error-description',
|
||
htmlUtils.jqueryToHtml( retryLink ),
|
||
error,
|
||
htmlUtils.jqueryToHtml( reportLink )
|
||
)
|
||
)
|
||
)
|
||
);
|
||
this.$imageDiv.find( '.mw-mmv-retry-link' ).click( function () {
|
||
location.reload();
|
||
} );
|
||
};
|
||
|
||
/**
|
||
* Returns width and height of the canvas area (i.e. the space available for the image).
|
||
* @param {boolean} forFullscreen if true, return size in fullscreen mode; otherwise, return current size
|
||
* (which might still be fullscreen mode).
|
||
* @return {{width: Number, height: Number}} width and height in CSS pixels
|
||
*/
|
||
C.getDimensions = function ( forFullscreen ) {
|
||
var $window = $( window ),
|
||
$aboveFold = $( '.mw-mmv-above-fold' ),
|
||
isFullscreened = !!$aboveFold.closest( '.jq-fullscreened' ).length,
|
||
// Don't rely on this.$imageWrapper's sizing because it's fragile.
|
||
// Depending on what the wrapper contains, its size can be 0 on some browsers.
|
||
// Therefore, we calculate the available space manually
|
||
availableWidth = $window.width(),
|
||
availableHeight = $window.height() - ( isFullscreened ? 0 : $aboveFold.outerHeight() );
|
||
|
||
if ( forFullscreen ) {
|
||
return {
|
||
width: screen.width,
|
||
height: screen.height
|
||
};
|
||
} else {
|
||
return {
|
||
width: availableWidth,
|
||
height: availableHeight
|
||
};
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Gets the widths for a given lightbox image.
|
||
* @param {mw.mmv.LightboxImage} image
|
||
* @returns {mw.mmv.model.ThumbnailWidth}
|
||
*/
|
||
C.getLightboxImageWidths = function ( image ) {
|
||
var thumb = image.thumbnail,
|
||
canvasDimensions = this.getDimensions();
|
||
|
||
return this.thumbnailWidthCalculator.calculateWidths(
|
||
canvasDimensions.width, canvasDimensions.height, thumb.width, thumb.height );
|
||
};
|
||
|
||
/**
|
||
* Gets the fullscreen widths for a given lightbox image.
|
||
* Intended for use before the viewer is in fullscreen mode
|
||
* (in fullscreen mode getLightboxImageWidths() works fine).
|
||
* @param {mw.mmv.LightboxImage} image
|
||
* @returns {mw.mmv.model.ThumbnailWidth}
|
||
*/
|
||
C.getLightboxImageWidthsForFullscreen = function ( image ) {
|
||
var thumb = image.thumbnail,
|
||
canvasDimensions = this.getDimensions( true );
|
||
|
||
return this.thumbnailWidthCalculator.calculateWidths(
|
||
canvasDimensions.width, canvasDimensions.height, thumb.width, thumb.height );
|
||
};
|
||
|
||
/**
|
||
* Gets the widths for the current lightbox image.
|
||
* @returns {mw.mmv.model.ThumbnailWidth}
|
||
*/
|
||
C.getCurrentImageWidths = function () {
|
||
return this.getLightboxImageWidths( this.imageRawMetadata );
|
||
};
|
||
|
||
|
||
mw.mmv.ui.Canvas = Canvas;
|
||
}( mediaWiki, jQuery, OO ) );
|