/**
* See (http://jquery.com/).
* @name jQuery
* @class
* See the jQuery Library (http://jquery.com/) for full details. This just
* documents the function and classes that are added to jQuery by this plug-in.
*/
/**
* See (http://jquery.com/)
* @name fn
* @class
* See the jQuery Library (http://jquery.com/) for full details. This just
* documents the function and classes that are added to jQuery by this plug-in.
* @memberOf jQuery
*/
/**
* imageViewer - This is is a jQuery plugin and as such uses an Immediately Invoked Function Expression (IIFE)
* @name imageViewer
* @class
* @memberOf jQuery.fn
*/
(function(jQuery) {
/** Main imageViewer namespace
* imageViewer - This is is a jQuery plugin and as such uses an Immediately Invoked Function Expression (IIFE)
* @name imageViewer
* @class
* @memberOf jQuery.fn
* @param {object} method - Usually init options such as the initial array of images
*/
jQuery.fn.imageViewer = function(method) {
var settings = {
'navLinks':true,
'zoomDirection': 'width',
'zoomLevel': 100,
'increment' : 50,
'images' : null,
'imageOverlay': null,
'mainDiv' : this,
'imageViewerImg': null,
'imageIndex': null,
'currentImageDiv': null,
'keyBindings' : {},
'suppressedKeys' : [],
'calculateHeightWithHeader' : true,
'calculateHeightWithFooter' : false
};
var self = jQuery.fn.imageViewer;
/** Method by which the next or previous image is retrieved and set to the current image (from the initial sequence of images)
* @param {integer} increment - Usually 1 or -1 for next and previous page, but can be any integer.
*/
jQuery.fn.imageViewer.scrollPage = function(increment){
if(settings["imageIndex"] === 0 && increment === -1) {
settings["imageIndex"] = (settings["images"].length - 1);
} else if(settings["imageIndex"] == (settings["images"].length - 1) && increment == 1){
settings["imageIndex"] = 0;
} else {
settings["imageIndex"] += increment;
}
showPage(settings["imageIndex"]);
};
/** Essentially just an public accessor for getting information about the internal settings variable.
@param {string} str - String of setting you'd like to view.
*/
jQuery.fn.imageViewer.settings = function(str) {
if (settings[str] !== undefined) {
return settings[str];
}
};
/** Public method to remove keybindings. Especially useful when displaying multiple image_viewers on a single page.
*/
jQuery.fn.imageViewer.teardownKeyBindings = function() { teardownKeyBindings(); };
/** Public method to setup keybindings via the setupKeyBindings() method */
jQuery.fn.imageViewer.setupKeyBindings = function() { setupKeyBindings(); };
/** Public method to display the legend modal dialog box as an overlay. */
jQuery.fn.imageViewer.displayLegend = function(disableTransition) {
var id = $('#dialog');
var maskHeight = $(document).height();
var maskWidth = $(window).width();
$('#image-viewer-mask').css({'width':maskWidth,'height':maskHeight});
$('#image-viewer-mask').fadeIn(600);
$('#image-viewer-mask').fadeTo("slow",0.8);
var winH = $(window).height();
var winW = $(window).width();
$(id).css('top', winH/2-$(id).height()/2);
$(id).css('left', winW/2-$(id).width()/2);
$(id).fadeIn(2000);
};
/** Public method to hide the modal dialog box */
jQuery.fn.imageViewer.hideLegend = function() {
$('#image-viewer-mask, #dialog').hide();
};
/** Public method for manipulating the top and left dimensions of an image. This is used to move the image up/left/right/down. See tests for examples
* @param {integer} left - Desired left position of current image.
* @param {integer} top - Desired top position of current image.
*/
jQuery.fn.imageViewer.scroll = function(left, top){
settings["currentImageDiv"].scrollTop(settings["currentImageDiv"].scrollTop() + top);
settings["currentImageDiv"].scrollLeft(settings["currentImageDiv"].scrollLeft() + left);
};
/** Public method for inreasing the width dimension of the current image.
* @param {integer} increment - Increment you'd like to increase by. If specified as an integer will increase current increment
* @param {string} increment - if Increment is given as a string suffixed by the percent sign (%) will set that percentage absolutely.
* @param {string} zoomDirection - Defaults to 'width' if specified as 'height' will change the height percentage instead of width.
*/
jQuery.fn.imageViewer.zoom = function(increment,zoomDirection){
var newZoomLevel = settings['zoomLevel']
if (typeof increment === 'string' && increment.match(/%$/))
newZoomLevel = parseInt(increment)
else
newZoomLevel += increment
//prevent zooming out past reasonable level
if (newZoomLevel < settings['increment'])
newZoomLevel = settings['increment']
if (zoomDirection)
settings['zoomDirection'] = zoomDirection
zoomAbsolute(newZoomLevel);
};
/** Rotates all images currently in the images var
* @param {integer} increment - Angle to be rotated by. Given the integer 90 will rotate current image by 90 degrees to the right.
*/
jQuery.fn.imageViewer.rotate_all = function(increment){
jQuery.each(settings["images"], function(index, image) {
rotate(increment, index);
});
};
/** Rotates current image by increment to the right
* @param {integer} increment - Angle to be rotated by. Given the integer 90 will rotate current image by 90 degrees to the right.
*/
jQuery.fn.imageViewer.rotate = function(increment){
rotate(increment);
};
/** Will load all images currently in the images variable and loads them in a new tab. Once in a new tab will open the print dialog box for printing. Images will each be printed separated by a page break. Closes new tab when print dialog is dismissed (by printing or by cancelling)
*/
jQuery.fn.imageViewer.print = function() {
var myWindow = window.open("", '_newtab');
var image_tags = "";
jQuery.each(settings["images"], function(index, image) {
image_tags += '<img style="clear:both;width:100%;page-break-after:always;max-width:none;" src="' + image + '" />';
});
var print_script = '<script type=\'text/javascript\'>' +
'function PrintWindow() {CheckWindowState(); }' +
'function CheckWindowState(){' +
'if(document.readyState=="complete")' +
'{ window.print();window.close();}' +
'else{setTimeout("CheckWindowState()", 2000)}}' +
'PrintWindow();' +
'</script>';
myWindow.document.write(image_tags + print_script);
myWindow.document.close();
};
/** initializes the image viewer's internal state. Any settings passed to options are merged into the default settings object. Calls a number of functions to initialize the viewer including setting up containers, listeners, et al
* @param {array} image_path_array - Array of image paths. These paths will be inserted as the src attribute on img tags later, so ensure that these are valid paths.
* @param {object} options - options that will be merged into the default settings object.
* @private
*/
function init(image_path_array, options){
if ( options )
jQuery.extend( settings, options );
this.data("settings", settings);
setupContainers();
setupHeight();
setupKeyBindings();
if (settings['navLinks'] === true) {createNavTable();}
setupImages(image_path_array);
setupLegend();
setupMaskListener();
handleWindowResize();
}
/** Builds and prepends the the legend dialog box to the #image-viewer-key-bindings.
* @private
*/
function setupLegend() {
var modal_container = "<div id='image-viewer-key-bindings'></div>";
var mask_div = "<div id='image-viewer-mask'></div>";
var key_binding_div = "<div class='window' id='dialog'>" +
"<a href='#' class='image-viewer-close'>" + addGlyphIcon('icon-remove') + "</a>" +
"<table>" +
" <thead>" +
" <tr>" +
" <th>Keystroke</th>" +
" <th>Description</th>" +
" </tr>" +
" </thead>" +
" <tbody>" +
" <tr><td>'\\'</td><td>Toggle Command Mode</td></tr>" +
" <tr><td>'i'</td><td>Zoom In</td></tr>" +
" <tr><td>'k', 'o'</td><td>Zoom Out</td></tr>" +
" <tr><td>'l', 'n'</td><td>Next</td></tr>" +
" <tr><td>'ctrl+l', 'h'</td><td>Fit Height</td></tr>" +
" <tr><td>'w'</td><td>Fit Width</td></tr>" +
" <tr><td>'j', 'p'</td><td>Previous</td></tr>" +
" <tr><td>'e', 'up arrow'</td><td>Scroll Up</td></tr>" +
" <tr><td>'d', 'down arrow'</td><td>Scroll Down</td></tr>" +
" <tr><td>'s', 'left arrow'</td><td>Scroll Left</td></tr>" +
" <tr><td>'f', 'right arrow'</td><td>Scroll Right</td></tr>" +
" <tr><td>'r'</td><td>Rotate Clockwise</td></tr>" +
" <tr><td>'t'</td><td>Rotate All Clockwise</td></tr>" +
" <tr><td>';', 'page down'</td><td>Page Down</td></tr>" +
" <tr><td>'a', 'page up'</td><td>Page Up</td></tr>" +
" </tbody>" +
"</table>" +
"</div>";
$('body').prepend(modal_container);
$('#image-viewer-key-bindings').append(key_binding_div);
$('#image-viewer-key-bindings').append(mask_div);
}
function setupMaskListener(){
$('.window .image-viewer-close').click(function (e) {
//Cancel the link behavior
e.preventDefault();
settings["mainDiv"].imageViewer.hideLegend();
});
$('#image-viewer-mask').click(function () {
$(this).hide();
$('.window').hide();
});
}
/** resets settings object and reinitializes the image_viewer.
* @private
*/
function reload(){
image_array = settings["images"];
settings["zoomLevel"] = 100;
settings["images"] = null;
settings["mainDiv"] = null;
settings["imageViewerImg"] = null;
settings["imageIndex"] = null;
settings["currentImageDiv"] = null;
init(image_array);
}
/** Builds the navLinks table. Each call to createNavLink creates an a tag with a contained icon tag. These link tags contain the onclick events
* @private
*/
function createNavTable() {
var id = '#navlinks-for-' + settings['mainDiv'].attr('id')
table = '<table id="' + id + '" class="image-viewer-nav-links">' +
'<tr>' +
'<td>' + createNavLink('scrollPage(-1)', 'Previous Page', 'icon-backward') + '</td>' +
'<td>' + createNavLink('scrollPage(1)', 'Next Page', 'icon-forward') + '</td>' +
'<td>' + createNavLink('scroll(-1 * ' + settings["increment"] + ",0)", 'Left', 'icon-arrow-left') + '</td>' +
'<td>' + createNavLink('scroll(' + settings["increment"] + ",0)", 'Right', 'icon-arrow-right') + '</td>' +
'<td>' + createNavLink('scroll(0, -1 * ' + settings["increment"] + ")", 'Up', 'icon-arrow-up') + '</td>' +
'<td>' + createNavLink('scroll(0,' + settings["increment"] + ")", 'Down', 'icon-arrow-down') + '</td>' +
'<td id="' +settings["mainDiv"].attr('id') + '-nav-info' + '" class="image-viewer-nav-info"></td>' +
'<td>' + createNavLink('zoom(' + settings["increment"] + ")", 'Zoom In', 'icon-plus') + '</td>' +
'<td>' + createNavLink('zoom(-1 * ' + settings["increment"] + ")", 'Zoom Out', 'icon-minus') + '</td>' +
'<td>' + createNavLink('rotate(90)', 'Rotate Page', 'icon-repeat') + '</td>' +
'<td>' + createNavLink('rotate_all(90)', 'Rotate All Pages', 'icon-refresh') + '</td>' +
'<td>' + createNavLink('print()', 'Print', 'icon-print') + '</td>' +
'<td>' + createNavLink('displayLegend()', 'Display Legend', 'icon-info-sign') + '</td>' +
'</tr>' +
'</table>';
settings["mainDiv"].prepend(table);
}
/** builds an i tag with a class of glyph
* @param {string} glyph - name of glyph class from css. View image_viewer_rails.css.scss to see options.
* @private
*/
function addGlyphIcon(glyph) {
return '<i class="' + glyph + '"></i>'
}
/** builds link tag with title of name, and the onclick function of call
* @param {string} call - the imageViewer call that will be assigned to the onclick even of this link.
* @param {string} name - inserted directly into title text of link.
* @param {string} glyph - name of css class glyphicon (see addGlyphIcon)
* @private
*/
function createNavLink( call, name, glyph) {
var div_id = '#' + settings["mainDiv"].attr("id");
return '<a href="#" title="' + name + '" class="image-viewer-nav-link" onclick="' +
'$(\'' + div_id + '\')' +
'.imageViewer.' + call + ';return false;">' +
addGlyphIcon(glyph) +
'</a>';
}
/** Clears mainDiv (which is _this_ upon initialization). Assigns width to mainDiv from settings var
* @private
*/
function setupContainers(){
settings["mainDiv"].empty();
settings["mainDiv"].addClass('image-viewer-container');
settings["mainDiv"].css("width", settings["width"]);
}
/** Creates the img tags on the page and assigns them unique ids
* @param {array} images - This function is called from init() and accepts the array of images passed there.
* @private
*/
function setupImages(images){
settings["images"] = images;
var style = "";
jQuery.each(images, function(index, image) {
if(index !== 0)
style += "display: none;";
settings["mainDiv"].append('<div id="' + settings["mainDiv"].attr("id") + '-image-viewer-' + index + '" ' +
'class="image-viewer" ' +
'style="' + style + '">' +
'<img id="' + settings["mainDiv"].attr("id") + '-full-image-' + index + '" ' +
'src="' + image + '" ' +
'alt="Full Image" ' +
'style="' + settings["zoomDirection"] + ':' + settings["zoomLevel"] + '%;max-width:none;" ' +
'angle="0"/>' +
'</div>');
});
settings["currentImageDiv"] = $('#' + settings["mainDiv"].attr("id") + '-image-viewer-0');
settings["imageViewerImg"] = $('#' + settings["mainDiv"].attr("id") + '-full-image-0');
settings["imageIndex"] = 0;
updateOverlay();
}
/** Redirects window to root path
* @private
*/
function delayedRedirect(){
window.location = "/";
}
/** Removes keybindings from keymaster. */
function teardownKeyBindings(){
key.deleteScope('imageviewer');
}
/** Convenience method for adding keybindings to imageViewer through keymaster.js
* @param {string} keyString - String of the key to be bound to func. Listing of keys can be found: https://github.com/madrobby/keymaster
* @param {string} keyScope - Scope for current keybinds. This scope can be deleted (and is with teardownKeyBindings().
* @param {func} func - function to be bound ot the keyString.
* @private
*/
function addKeyToKeyMaster(keyString, keyScope, func){
var keys = typeof(keyString) === 'string' ? [keyString] : keyString
jQuery.each(keys, function(index, k) {
if (jQuery.inArray(k, settings['suppressedKeys']) === -1) {
key(k, keyScope, func);
}
});
}
/** Assigns the various key combinations to their actual function calls.
* @private
*/
function setupKeyBindings(){
teardownKeyBindings();
key.setScope('imageviewer');
keymasterCommandModeCallback = updateOverlay;
/** prevent alt+f4 from closing browser
addKeyToKeyMaster('alt+f4', 'imageviewer', function(){ alert('NO WAY JOSE! (Don\'t hit Alt+F4!!!)'); return false; });
/** zoom out */
addKeyToKeyMaster(['k','shift+k','o','shift+o'], 'imageviewer', function(){ self.zoom(-1 * settings["increment"]); return false;});
/** zoom in */
addKeyToKeyMaster(['i','shift+i'], 'imageviewer', function(){ self.zoom(settings["increment"]); return false;});
/** scroll up */
addKeyToKeyMaster(['e','shift+e','up'], 'imageviewer', function(){ self.scroll(0,-1 * settings["increment"]); return false;});
/** scroll down */
addKeyToKeyMaster(['d','shift+d','down'], 'imageviewer', function(){ self.scroll(0,settings["increment"]); return false;});
/** page up */
addKeyToKeyMaster(['a','shift+a','pageup'], 'imageviewer', function(){ self.scroll(0,-1 * (settings["increment"] * 5)); return false;});
/** page down */
addKeyToKeyMaster([';','pagedown'], 'imageviewer', function(){ self.scroll(0,(settings["increment"] * 5)); return false;});
/** scroll right */
addKeyToKeyMaster(['f','shift+f','right'], 'imageviewer', function(){ self.scroll(settings["increment"],0); return false;});
/** scroll left */
addKeyToKeyMaster(['s','shift+s','left'], 'imageviewer', function(){ self.scroll((-1 * settings["increment"]),0); return false; });
/** previous page */
addKeyToKeyMaster(['j','shift+j','p','shift+p'], 'imageviewer', function(){ self.scrollPage(-1); return false; });
/** next page */
addKeyToKeyMaster(['l','shift+l','n','shift+n'], 'imageviewer', function(){ self.scrollPage(1); return false; });
/** rotate */
addKeyToKeyMaster(['r','shift+r'], 'imageviewer', function(){ self.rotate(90); return false; });
/** rotate all */
addKeyToKeyMaster(['t','shift+t'], 'imageviewer', function(){ self.rotate_all(90); return false; });
/** zoom to fit width */
addKeyToKeyMaster('w', 'imageviewer', function(){ self.zoom('100%','width'); return false; });
/** zoom to fit height */
addKeyToKeyMaster(['h','ctrl+l','ctrl+shift+l'], 'imageviewer', function(){ self.zoom('100%','height'); return false; });
}
/** Calculates the height of the view port and sets the main div to fit.
* @private
*/
function setupHeight(){
var window_height = $(window).height();
var footer = $('#footer');
var navlinks = $('#navlinks-for-' + settings['mainDiv'].attr("id"));
var footer_height, menu_offset;
if (settings['calculateHeightWithHeader']) {
menu_offset = settings["mainDiv"].offset().top;
} else {
menu_offset = 0;
}
if(footer && settings['calculateHeightWithFooter']){
footer_height = footer.height() + 7;
} else {
footer_height = 0;
}
var new_height = window_height - menu_offset - footer_height - 20;
settings["mainDiv"].css('height', new_height + 'px');
settings["mainDiv"].css('top', '0px');
$('.image-viewer').css('height', (settings["mainDiv"].height() - navlinks.height()) + 'px');
}
/** call setupHeight() on resize */
function handleWindowResize(){
$(window).bind('resize', setupHeight);
}
/** Zooms image's 'height' or 'width' to a given percentage. Height or Width is decided by setting the zoomDirection setting var.
* @param {integer} zoomLevel - Percentage you'd like the image to be sized to.
* @private
*/
function zoomAbsolute(zoomLevel){
previous_zoomLevel = settings["zoomLevel"];
settings["zoomLevel"] = zoomLevel;
object_to_zoom = $('#' + settings["mainDiv"].attr("id") + '-full-image-' + settings["imageIndex"]);
object_to_zoom.css('width', '')
object_to_zoom.css('height','')
object_to_zoom.css(settings['zoomDirection'], settings["zoomLevel"] + '%');
self.scroll(0,0);
}
function showPage(page){
settings["imageIndex"] = page;
settings["currentImageDiv"].hide();
settings["currentImageDiv"] = $('#' + settings["mainDiv"].attr("id") + '-image-viewer-' + page);
settings["imageViewerImg"] = $('#' + settings["mainDiv"].attr("id") + '-full-image-' + page);
settings["currentImageDiv"].show();
updateOverlay();
}
/** Updates the nav-info to display both command mode toggle and "page of pages"
* @param {bool} commandMode - Sets comand mode to true or false.
* @private
*/
function updateOverlay(commandMode){
overlay = $('#' + settings["mainDiv"].attr("id") + '-nav-info');
if (settings['images'].length === 0)
return;
var s = (settings["imageIndex"] + 1) + ' of ' + settings["images"].length;
if(commandMode === undefined)
commandMode = overlay.html().search(/CM/) !== -1;
if(commandMode === true)
s += ' CM';
overlay.html(s);
}
/** Calls jQuery.rotate.js's rotate function.
* @param {integer} increment - number of degrees to rotate image.
* @param {integer} imageIndex - Set which image to rotate. Defaults to current image based of settings["imageIndex"]
* @private
*/
function rotate(increment, imageIndex){
if (imageIndex === undefined) imageIndex = settings["imageIndex"];
var image = $('#' + settings["mainDiv"].attr("id") + '-full-image-' + imageIndex);
var current_angle = parseInt(image.attr('angle'),10);
image.rotate(current_angle + increment);
current_angle = parseInt(image.getRotateAngle(),10) % 360;
if (current_angle === 90 || current_angle === 270) {
var offset = image.height()/2 - image.width()/2;
image.css('margin-top', -1 * offset);
image.css('margin-left', offset);
} else {
image.css('margin-top', 0);
image.css('margin-left', 0);
}
image.attr('angle',current_angle);
}
/** if argument passed to IIFE aren't a method defined on imageViewer then pass the arguments to initialize. */
if ( typeof method === 'object' || ! method )
return init.apply(this, arguments);
else
jQuery.error( 'Method ' + method + ' does not exist on jQuery.imageViewer' );
};
})(jQuery);