MediaWiki:Gadget-ZoomableMap.js

From Halopedia, the Halo wiki

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/* <pre>
 * ZoomableMap v1.6
 * Copyright (c) 2022 Jesús Martínez (User:Ciencia_Al_Poder)
 *
 * Permite hacer scroll y zoom en una imagen grande dentro de una página
 * 
 * This program 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
*/
(function( $ ) {
'use strict';

var _sensibility = 2,
 _init = function() {
	$( '.zoomablemap' ).each( function() {
		$( this ).data( 'zoomablemap', new ZoomableMap( this ) );
	} );
},
_initZoomableMap = function() {
	var $controls, $zoomin, $zoomout, $contents, $toRemove = null;
	$contents = this.$el.children();
	// imagemap wraps everything in a div.noresize, which causes problems when dragging. Remove the extra element
	if ( $contents.length === 1 && $contents.is( '.noresize' ) ) {
		$toRemove = $contents;
		$contents = $contents.children();
	}
	this.$mapCont = $( '<div class="mapcontent"></div>' ).append( $('<div class="mapinner"></div>').append( $contents ) );
	if ( $toRemove ) {
		$toRemove.remove();
	}
	$controls = $( '<div class="mapcontrols"></div>' );
	$zoomin = $( '<div class="control zoomin" tabindex="0" role="button"></div>' ).text( '+' ).on( 'click', _eventDelegate( this, _zoomIn ) );
	$zoomout = $( '<div class="control zoomout" tabindex="0" role="button"></div>' ).text( '-' ).on( 'click', _eventDelegate( this, _zoomOut ) );
	$controls.append( $zoomin, $zoomout );
	this.$el.append( this.$mapCont, $controls );
	this.$mapCont.on( {
		pointerdown: _eventDelegate( this, _pointerDown ),
		wheel: _eventDelegate( this, _wheel )
	} );
	_updateScale.call( this, -1 );
	this.$el.addClass( 'loadcomplete' );
},
_eventDelegate = function( thisArg, fn ) {
	return function( e ) {
		return fn.call( thisArg, e );
	};
},
_zoomIn = function() {
	_updateScale.call( this, 0.1 );
	return false;
},
_zoomOut = function() {
	_updateScale.call( this, -0.1 );
	return false;
},
_wheel = function( e ) {
	_updateScale.call( this, e.originalEvent.deltaY > 0 ? -0.1 : 0.1 );
	return false;
},
_updateScale = function( delta ) {
	var rect, newScale, minScale, innerDimensions;
	newScale = this.scale + delta;
	if ( newScale < 0.1 ) {
		newScale = 0.1;
	}
	innerDimensions = _getInnerDimensions.call( this );
	// If the element is hidden (collapsible sections) this will be an empty array
	rect = this.$mapCont[0].getClientRects();
	if ( !rect || rect.length === 0 ) {
		return;
	}
	// Lower limit is to fit the contents on the current width
	minScale = Math.min( rect[0].width / innerDimensions.width, rect[0].height / innerDimensions.height );
	if ( newScale < minScale ) {
		newScale = minScale;
	}
	if ( newScale > 1 ) {
		newScale = 1;
	}
	this.scale = newScale;
	this.$mapCont.find( '> .mapinner' ).eq( 0 ).css( {
		transform: 'scale(' + String( newScale ) + ')',
		height: String( innerDimensions.height * newScale ) + 'px'
	} );
},
_pointerDown = function( e ) {
	var pos = _getEventCoordinates( e, this.$mapCont[0] );
	e = e.originalEvent;
	// Retarget all pointer events (until pointerup)
	this.$mapCont[0].setPointerCapture( e.pointerId );
	if ( e.isPrimary ) {
		this.dragStartPoint = pos;
		this.pointerCache = [];
		this.initialPointerDistance = 0;
		this.$mapCont.on( {
			'pointermove': _eventDelegate( this, _dragMove ),
			'pointerup': _eventDelegate( this, _dragEnd )
		} );
		this.$mapCont.addClass( 'pointerdown' );
	}
	// Allow max 2 pointers
	if ( this.pointerCache !== null && this.pointerCache.length < 2 ) {
		this.pointerCache.push( { id: e.pointerId, x: pos.x, y: pos.y } );
		// On 2nd pointer, set initial zoom position
		if ( !e.isPrimary ) {
			this.initialPointerDistance = _getPointDistance.call( this );
			this.initialPinchZoomScale = this.scale;
		}
	}
	return false;
},
_dragMove = function( e ) {
	var newPos, newProps, newDistance;
	newPos = _getEventCoordinates( e, this.$mapCont[0] );
	e = e.originalEvent;
	// This should never happen
	if ( this.pointerCache === null ) {
		return;
	}
	// Update point cache
	for ( var i = 0; i < this.pointerCache.length; i++ ) {
		if ( this.pointerCache[ i ].id == e.pointerId ) {
			this.pointerCache[ i ].x = newPos.x;
			this.pointerCache[ i ].y = newPos.y;
			break;
		}
	}
	if ( e.isPrimary ) {
		newProps = {
			scrollLeft: this.dragStartPoint.scrollLeft - newPos.x + this.dragStartPoint.x,
			scrollTop: this.dragStartPoint.scrollTop - newPos.y + this.dragStartPoint.y
		};
		this.$mapCont.prop( newProps );
	}
	if ( this.pointerCache.length == 2 && this.initialPointerDistance > 0 ) {
		newDistance = _getPointDistance.call( this );
		_updateScale.call( this, this.initialPinchZoomScale * newDistance / this.initialPointerDistance - this.scale );
	}
	if ( !this.dragging ) {
		this.$mapCont.addClass( 'dragging' );
		this.dragging = true;
	}
},
_dragEnd = function( e ) {
	var newPos, propagate = false;
	e = e.originalEvent;
	if ( !this.pointerCache ) {
		return;
	}
	// Remove pointer from cache
	for ( var i = 0; i < this.pointerCache.length; i++ ) {
		if ( this.pointerCache[ i ].id == e.pointerId ) {
			this.pointerCache.splice( i, 1 );
			break;
		}
	}
	if ( !e.isPrimary ) {
		return;
	}
	newPos = _getEventCoordinates( e, this.$mapCont[0] );
	// If it hasn't scrolled, propagate the event
	if (
		Math.abs( newPos.x - this.dragStartPoint.x ) <= _sensibility &&
		Math.abs( newPos.y - this.dragStartPoint.y ) <= _sensibility
	) {
		propagate = true;
	}
	this.$mapCont.off( 'pointermove pointerup' ).removeClass( 'pointerdown' );
	this.dragStartPoint = null;
	this.dragging = false;
	this.$mapCont.removeClass( 'dragging' );
	this.pointerCache = null;
	if ( propagate ) {
		this.$el.trigger( $.Event( 'mapclick', { clientX: e.clientX, clientY: e.clientY } ) );
	}
},
_getPointDistance = function() {
	var xx, yy;
	xx = this.pointerCache[ 0 ].x - this.pointerCache[ 1 ].x;
	yy = this.pointerCache[ 0 ].y - this.pointerCache[ 1 ].y;
	return Math.sqrt( xx * xx + yy * yy );
},
/* Given an event and element, returns pointer coordinates and scroll position of element */
_getEventCoordinates = function( event, el ) {
	return {
		x: event.clientX,
		y: event.clientY,
		scrollLeft: el.scrollLeft,
		scrollTop: el.scrollTop
	};
},
_getInnerDimensions = function() {
	var innerEl;
	if ( !this.innerDimensions ) {
		innerEl = this.$mapCont.find( '> .mapinner' )[0];
		if ( innerEl.scrollWidth === 0 || innerEl.scrollHeight === 0 ) {
			return null;
		}
		this.innerDimensions = { width: innerEl.scrollWidth, height: innerEl.scrollHeight };
	}
	return this.innerDimensions;
};


// Class ZoomableMap
function ZoomableMap( el ) {
	this.$el = $( el );
	this.$mapCont = null;
	this.dragStartPoint = null;
	this.dragging = false;
	this.scale = 1;
	this.pointerCache = null;
	this.innerDimensions = null;
	this.initialPointerDistance = 0;
	this.initialPinchZoomScale = 0;
	_initZoomableMap.call( this );
}

$(_init);

})( jQuery );