MediaWiki:Jquery.zoomImage.js

Note: After saving, 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: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Go to Menu → Settings (Opera → Preferences on a Mac) and then to Privacy & security → Clear browsing data → Cached images and files.
/* 
 * @description Plugin for modalLayer functionality to get scaleable images. 
 * Using jquery.mousewheeel.js and jquery.ivierwer.js jQuery plugins (see below)
 * Final function modalLayer_ShowIviewerZoomImage() creates zoomable image + controls on a div#modal-fg
 * @requires $.resource()
 * @requires MediaWiki:Common.css CSS for jQuery plugin iviewer, popup image
 * @requires $.jI18n {}
 * @requires initImageZooming() and the following functions
 * @requires zoomImage()
 * @requires modalLayer_Create()
 * @requires modalLayer_ShowImage()
 */

/* Check dependencies */
if (!$.jI18n) {
  console.log("Global resource dictionary $.jI18n is missing. Please have it defined it in MediaWiki:Commons.js");
}
if (!$.resource) {
  console.log("Global resource function $.resource('reource-key') is missing. Please make sure this function is defined in MediaWiki:Commons.js");
}

/**
 * file jquery.mousewheeel.js
 * 
 * Copyright (c) 2013 Brandon Aaron (http://brandon.aaron.sh)
 * Licensed under the MIT License (LICENSE.txt).
 * 
 * @version 3.1.9
 * @requires: jQuery 1.2.2+
 * @param {object} factory
 * @returns {undefined}
 */
(function (factory) {
    if ( typeof define === 'function' && define.amd ) {
        // AMD. Register as an anonymous module.
        define(['jquery'], factory);
    } else if (typeof exports === 'object') {
        // Node/CommonJS style for Browserify
        module.exports = factory;
    } else {
        // Browser globals
        factory(jQuery);
    }
}(function ($) {

    var toFix  = ['wheel', 'mousewheel', 'DOMMouseScroll', 'MozMousePixelScroll'],
        toBind = ( 'onwheel' in document || document.documentMode >= 9 ) ?
                    ['wheel'] : ['mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'],
        slice  = Array.prototype.slice,
        nullLowestDeltaTimeout, lowestDelta;

    if ( $.event.fixHooks ) {
        for ( var i = toFix.length; i; ) {
            $.event.fixHooks[ toFix[--i] ] = $.event.mouseHooks;
        }
    }

    var special = $.event.special.mousewheel = {
        version: '3.1.9',

        setup: function() {
            if ( this.addEventListener ) {
                for ( var i = toBind.length; i; ) {
                    this.addEventListener( toBind[--i], handler, false );
                }
            } else {
                this.onmousewheel = handler;
            }
            // Store the line height and page height for this particular element
            $.data(this, 'mousewheel-line-height', special.getLineHeight(this));
            $.data(this, 'mousewheel-page-height', special.getPageHeight(this));
        },

        teardown: function() {
            if ( this.removeEventListener ) {
                for ( var i = toBind.length; i; ) {
                    this.removeEventListener( toBind[--i], handler, false );
                }
            } else {
                this.onmousewheel = null;
            }
        },

        getLineHeight: function(elem) {
            return parseInt($(elem)['offsetParent' in $.fn ? 'offsetParent' : 'parent']().css('fontSize'), 10);
        },

        getPageHeight: function(elem) {
            return $(elem).height();
        },

        settings: {
            adjustOldDeltas: true
        }
    };

    $.fn.extend({
        mousewheel: function(fn) {
            return fn ? this.bind('mousewheel', fn) : this.trigger('mousewheel');
        },

        unmousewheel: function(fn) {
            return this.unbind('mousewheel', fn);
        }
    });


    function handler(event) {
        var orgEvent   = event || window.event,
            args       = slice.call(arguments, 1),
            delta      = 0,
            deltaX     = 0,
            deltaY     = 0,
            absDelta   = 0;
        event = $.event.fix(orgEvent);
        event.type = 'mousewheel';

        // Old school scrollwheel delta
        if ( 'detail'      in orgEvent ) { deltaY = orgEvent.detail * -1;      }
        if ( 'wheelDelta'  in orgEvent ) { deltaY = orgEvent.wheelDelta;       }
        if ( 'wheelDeltaY' in orgEvent ) { deltaY = orgEvent.wheelDeltaY;      }
        if ( 'wheelDeltaX' in orgEvent ) { deltaX = orgEvent.wheelDeltaX * -1; }

        // Firefox < 17 horizontal scrolling related to DOMMouseScroll event
        if ( 'axis' in orgEvent && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) {
            deltaX = deltaY * -1;
            deltaY = 0;
        }

        // Set delta to be deltaY or deltaX if deltaY is 0 for backwards compatabilitiy
        delta = deltaY === 0 ? deltaX : deltaY;

        // New school wheel delta (wheel event)
        if ( 'deltaY' in orgEvent ) {
            deltaY = orgEvent.deltaY * -1;
            delta  = deltaY;
        }
        if ( 'deltaX' in orgEvent ) {
            deltaX = orgEvent.deltaX;
            if ( deltaY === 0 ) { delta  = deltaX * -1; }
        }

        // No change actually happened, no reason to go any further
        if ( deltaY === 0 && deltaX === 0 ) { return; }

        // Need to convert lines and pages to pixels if we aren't already in pixels
        // There are three delta modes:
        //   * deltaMode 0 is by pixels, nothing to do
        //   * deltaMode 1 is by lines
        //   * deltaMode 2 is by pages
        if ( orgEvent.deltaMode === 1 ) {
            var lineHeight = $.data(this, 'mousewheel-line-height');
            delta  *= lineHeight;
            deltaY *= lineHeight;
            deltaX *= lineHeight;
        } else if ( orgEvent.deltaMode === 2 ) {
            var pageHeight = $.data(this, 'mousewheel-page-height');
            delta  *= pageHeight;
            deltaY *= pageHeight;
            deltaX *= pageHeight;
        }

        // Store lowest absolute delta to normalize the delta values
        absDelta = Math.max( Math.abs(deltaY), Math.abs(deltaX) );

        if ( !lowestDelta || absDelta < lowestDelta ) {
            lowestDelta = absDelta;

            // Adjust older deltas if necessary
            if ( shouldAdjustOldDeltas(orgEvent, absDelta) ) {
                lowestDelta /= 40;
            }
        }

        // Adjust older deltas if necessary
        if ( shouldAdjustOldDeltas(orgEvent, absDelta) ) {
            // Divide all the things by 40!
            delta  /= 40;
            deltaX /= 40;
            deltaY /= 40;
        }

        // Get a whole, normalized value for the deltas
        delta  = Math[ delta  >= 1 ? 'floor' : 'ceil' ](delta  / lowestDelta);
        deltaX = Math[ deltaX >= 1 ? 'floor' : 'ceil' ](deltaX / lowestDelta);
        deltaY = Math[ deltaY >= 1 ? 'floor' : 'ceil' ](deltaY / lowestDelta);

        // Add information to the event object
        event.deltaX = deltaX;
        event.deltaY = deltaY;
        event.deltaFactor = lowestDelta;
        // Go ahead and set deltaMode to 0 since we converted to pixels
        // Although this is a little odd since we overwrite the deltaX/Y
        // properties with normalized deltas.
        event.deltaMode = 0;

        // Add event and delta to the front of the arguments
        args.unshift(event, delta, deltaX, deltaY);

        // Clearout lowestDelta after sometime to better
        // handle multiple device types that give different
        // a different lowestDelta
        // Ex: trackpad = 3 and mouse wheel = 120
        if (nullLowestDeltaTimeout) { clearTimeout(nullLowestDeltaTimeout); }
        nullLowestDeltaTimeout = setTimeout(nullLowestDelta, 200);

        return ($.event.dispatch || $.event.handle).apply(this, args);
    }

    function nullLowestDelta() {
        lowestDelta = null;
    }

    function shouldAdjustOldDeltas(orgEvent, absDelta) {
        // If this is an older event and the delta is divisable by 120,
        // then we are assuming that the browser is treating this as an
        // older mouse wheel event and that we should divide the deltas
        // by 40 to try and get a more usable deltaFactor.
        // Side note, this actually impacts the reported scroll distance
        // in older browsers and can cause scrolling to be slower than native.
        // Turn this off by setting $.event.special.mousewheel.settings.adjustOldDeltas to false.
        return special.settings.adjustOldDeltas && orgEvent.type === 'mousewheel' && absDelta % 120 === 0;
    }

}));

/**
 * @description file jquery.ivierwer.js:
 * 
 * allows to view image in selected div with zoom controls and
 * possibility to move image in area by mouse.
 *
 * Plugin is dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
 * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
 *
 * Docs available at http://wiki.github.com/can3p/iviewer
 * See MediaWiki:Common.js → modalLayer_ShowImage()
 * @param {object} $
 * @returns {undefined}
 */
(function ($) {
  'use strict';
  $.fn.iviewer  = function (o) {
    return this.each(function () {
      $(this).data('viewer', new $iv(this, o));
    });
  };

  var defaults = {
    /**
    * start zoom value for image, not used now
    * may be equal to "fit" to fit image into container or scale in %
    **/
    zoom: "fit",
    /**
    * base value to scale image
    **/
    zoom_base: 100,
    /**
    * initial zomm when image is loaded (AP: added 2010-12-07 15:09:16)
    **/
    zoom_init: 100,
    /**
    * maximum zoom
    **/
    zoom_max: 800,
    /**
    * minimum zoom
    **/
    zoom_min: 25,
    /**
    * base of rate multiplier.
    * zoom is calculated by formula: zoom_base * zoom_delta^rate
    * default: 1.4
    **/
    zoom_delta: 1.2,
    /**
    * if true plugin doesn't add its own controls
    **/
    ui_disabled: false,
    /**
    * if false, plugin doesn't bind resize event on window and this must
    * be handled manually
    **/
    update_on_resize: true,
    /**
    * event is triggered when zoom value is changed
    * @param int new zoom value
    * @return boolean if false zoom action is aborted
    **/
    onZoom: null,
    /**
    * callback is fired after plugin setup
    **/
    initCallback: null,
    /**
    * event is fired on drag begin
    * @param object coords mouse coordinates on the image
    * @return boolean if false is returned, drag action is aborted
    **/
    onStartDrag: null,
    /**
    * event is fired on drag action
    * @param object coords mouse coordinates on the image
    **/
    onDrag: null,
    /**
    * event is fired when mouse moves over image
    * @param object coords mouse coordinates on the image
    **/
    onMouseMove: null,
    /**
    * mouse click event
    * @param object coords mouse coordinates on the image
    **/
    onClick: null,
    /**
    * event is fired when image starts to load
    */
    onStartLoad: null,
    /**
    * event is fired, when image is loaded and initially positioned
    */
    onFinishLoad: null
  };

  $.iviewer = function (e, o) {
    var me = this;

    /* object containing actual information about image
    *  @img_object.object - jquery img object
    *  @img_object.orig_{width|height} - original dimensions
    *  @img_object.display_{width|height} - actual dimensions
    */
    this.img_object = {};
    this.zoom_object = {}; //object to show zoom status
    this.image_loaded = false;

    //drag variables
    this.dx = 0;
    this.dy = 0;
    this.dragged = false;

    this.settings = $.extend({}, defaults, o || {});
    this.current_zoom = this.settings.zoom;

    if (this.settings.src === null) {
      return;
    }
    this.container = $(e);

    this.update_container_info();

    //init container
    this.container.css("overflow", "hidden");

    if (this.settings.update_on_resize === true) {
      $(window).resize(function () {
        me.update_container_info();
      });
    }

    this.img_object.x = 0;
    this.img_object.y = 0;

    //init object
    this.img_object.object = $("<img>")
      .css({position: "absolute", top: "0px", left: "0px"}) //this is needed, because chromium sets them auto otherwise
      //bind mouse events
      .mousedown(function (e) {return me.drag_start(e); })
      .mousemove(function (e) {return me.drag(e); })
      .mouseup(function (e) {return me.drag_end(e); })
      .click(function (e) {return me.click(e); })
      .mouseleave(function (e) {return me.drag_end(e); })
      .mousewheel(function (ev, delta) {
        //this event is there instead of containing div, because
        //at opera it triggers many times on div
        var zoom = (delta > 0) ? 1 : -1;
        me.zoom_by(zoom);
        return false;
      });

    this.img_object.object.prependTo(me.container);
    this.loadImage(this.settings.src);

    if (!this.settings.ui_disabled) {
      this.createui();
    }

    if (this.settings.initCallback) {
      this.settings.initCallback.call(this);
    }
  };


  var $iv = $.iviewer;

  $iv.fn = $iv.prototype = {
    iviewer : "0.4.2"
  };
  $iv.fn.extend = $iv.extend = $.extend;

  $iv.fn.extend({
    loadImage: function (src) {
      this.current_zoom = this.settings.zoom;
      this.image_loaded = false;
      var me = this;

      if (this.settings.onStartLoad) {
        this.settings.onStartLoad.call(this);
      }

      this.img_object.object.unbind('load')
        .removeAttr("src")
        .removeAttr("width")
        .removeAttr("height")
        .css({top: 0, left: 0})
        .load(function () {
          me.image_loaded = true;
          me.img_object.display_width = me.img_object.orig_width = this.width;
          me.img_object.display_height = me.img_object.orig_height = this.height;

          if (!me.container.hasClass("iviewer_cursor")) {
            me.container.addClass("iviewer_cursor");
          }

          if (me.settings.zoom === "fit") {
            me.fit();
          } else {
            me.set_zoom(me.settings.zoom);
          }

          if (me.settings.onFinishLoad) {
            me.settings.onFinishLoad.call(me);
          }

          //src attribute is after setting load event, or it won't work
        }).attr("src", src);
    },

    /**
    * fits image in the container
    **/
    fit: function () {
      var aspect_ratio = this.img_object.orig_width / this.img_object.orig_height,
        window_ratio = this.settings.width / this.settings.height,
        choose_left = (aspect_ratio > window_ratio),
        new_zoom = 0;

      if (choose_left) {
        new_zoom = this.settings.width / this.img_object.orig_width * 100;
      } else {
        new_zoom = this.settings.height / this.img_object.orig_height * 100;
      }
      this.set_zoom(new_zoom * this.settings.zoom_init / 100);
    },

    /**
    * center image in container
    **/
    center: function () {
      this.setCoords(-Math.round((this.img_object.display_height - this.settings.height) / 2),
                     -Math.round((this.img_object.display_width - this.settings.width) / 2));
    },

    /**
    * move a point in container to the center of display area
    * @param x a point in container
    * @param y a point in container
    **/
    moveTo: function (x, y) {
      var dx = x - Math.round(this.settings.width / 2),
        dy = y - Math.round(this.settings.height / 2),
        new_x = this.img_object.x - this.dx,
        new_y = this.img_object.y - this.dy;

      this.setCoords(new_x, new_y);
    },

    /**
    * set coordinates of upper left corner of image object
    **/
    setCoords: function (x, y) { // do nothing while image is still loading
      if (!this.image_loaded) {
        return;
      }

      //check new coordinates to be correct (to be in rect)
      if (y > 0) {
        y = 0;
      }
      if (x > 0) {
        x = 0;
      }
      if (y + this.img_object.display_height < this.settings.height) {
        y = this.settings.height - this.img_object.display_height;
      }
      if (x + this.img_object.display_width < this.settings.width) {
        x = this.settings.width - this.img_object.display_width;
      }
      if (this.img_object.display_width <= this.settings.width) {
        x = -(this.img_object.display_width - this.settings.width) / 2;
      }
      if (this.img_object.display_height <= this.settings.height) {
        y = -(this.img_object.display_height - this.settings.height) / 2;
      }

      this.img_object.x = x;
      this.img_object.y = y;

      this.img_object.object.css("top", y + "px").css("left", x + "px");
    },


    /**
    * convert coordinates on the container to the coordinates on the image (in original size)
    *
    * @return object with fields x, y according to coordinates or false
    * if initial coords are not inside image
    **/
    containerToImage : function (x, y) {
      if (x < this.img_object.x || y < this.img_object.y ||
          x > this.img_object.x + this.img_object.display_width ||
          y > this.img_object.y + this.img_object.display_height) {
        return false;
      }

      return {
        x : $iv.descaleValue(x - this.img_object.x, this.current_zoom),
        y : $iv.descaleValue(y - this.img_object.y, this.current_zoom)
      };
    },

    /**
    * convert coordinates on the image (in original size) to the coordinates on the container
    *
    * @return object with fields x, y according to coordinates or false
    * if initial coords are not inside image
    **/
    imageToContainer : function (x, y) {
      if (x > this.img_object.orig_width || y > this.img_object.orig_height) {
        return false;
      }
      return {
        x : this.img_object.x + $iv.scaleValue(x, this.current_zoom),
        y : this.img_object.y + $iv.scaleValue(y, this.current_zoom)
      };
    },

    /**
    * get mouse coordinates on the image
    * @param e - object containing pageX and pageY fields, e.g. mouse event object
    *
    * @return object with fields x, y according to coordinates or false
    * if initial coords are not inside image
    **/
    getMouseCoords : function (e) {
      var img_offset = this.img_object.object.offset();
      return {
        x : $iv.descaleValue(e.pageX - img_offset.left, this.current_zoom),
        y : $iv.descaleValue(e.pageY - img_offset.top, this.current_zoom)
      };
    },

    /**
    * set image scale to the new_zoom
    * @param new_zoom image scale in %
    **/
    set_zoom: function (new_zoom) {
      if (this.settings.onZoom && this.settings.onZoom.call(this, new_zoom) === false) {
        return;
      }

      //do nothing while image is being loaded
      if (!this.image_loaded) {
        return;
      }

      if (new_zoom <  this.settings.zoom_min) {
        new_zoom = this.settings.zoom_min;
      } else if (new_zoom > this.settings.zoom_max) {
        new_zoom = this.settings.zoom_max;
      }

      /* we fake these values to make fit zoom properly work */
      var old_x, old_y;
      if (this.current_zoom === "fit") {
        old_x = Math.round(this.settings.width / 2 + this.img_object.orig_width / 2);
        old_y = Math.round(this.settings.height / 2 + this.img_object.orig_height / 2);
        this.current_zoom = 100;
      } else {
        old_x = -parseInt(this.img_object.object.css("left"), 10) +
          Math.round(this.settings.width / 2);
        old_y = -parseInt(this.img_object.object.css("top"), 10) +
          Math.round(this.settings.height / 2);
      }

      var new_width = $iv.scaleValue(this.img_object.orig_width, new_zoom),
        new_height = $iv.scaleValue(this.img_object.orig_height, new_zoom),
        new_x = $iv.scaleValue($iv.descaleValue(old_x, this.current_zoom), new_zoom),
        new_y = $iv.scaleValue($iv.descaleValue(old_y, this.current_zoom), new_zoom);

      new_x = this.settings.width / 2 - new_x;
      new_y = this.settings.height / 2 - new_y;

      this.img_object.object.attr("width", new_width).attr("height", new_height);
      this.img_object.display_width = new_width;
      this.img_object.display_height = new_height;

      this.setCoords(new_x, new_y);

      this.current_zoom = new_zoom;

      $.isFunction(this.settings.onAfterZoom) && this.settings.onAfterZoom.call(this, new_zoom);
      this.update_status();
    },

    /**
    * changes zoom scale by delta
    * zoom is calculated by formula: zoom_base * zoom_delta^rate
    * @param Integer delta number to add to the current multiplier rate number
    **/
    zoom_by: function (delta) {
      var closest_rate = this.find_closest_zoom_rate(this.current_zoom),
        next_rate = closest_rate + delta,
        next_zoom = this.settings.zoom_base * Math.pow(this.settings.zoom_delta, next_rate);
        
      if (delta > 0 && next_zoom < this.current_zoom) {
        next_zoom *= this.settings.zoom_delta;
      }
      if (delta < 0 && next_zoom > this.current_zoom) {
        next_zoom /= this.settings.zoom_delta;
      }
      this.set_zoom(next_zoom);
    },

    /**
    * finds closest multiplier rate for value
    * basing on zoom_base and zoom_delta values from settings
    * @param Number value zoom value to examine
    **/
    find_closest_zoom_rate: function (value) {
      if (value === this.settings.zoom_base) {
        return 0;
      }

      function div(val1, val2) {return val1 / val2; }
      function mul(val1, val2) {return val1 * val2; }

      var func = (value > this.settings.zoom_base) ? mul : div,
        sgn = (value > this.settings.zoom_base) ? 1 : -1,
        mltplr = this.settings.zoom_delta,
        rate = 1;

      while (Math.abs(func(this.settings.zoom_base, Math.pow(mltplr, rate)) - value) >
          Math.abs(func(this.settings.zoom_base, Math.pow(mltplr, rate + 1)) - value)) {
        rate++;
      }
      return sgn * rate;
    },

    /* update scale info in the container */
    update_status: function () {
      if (!this.settings.ui_disabled) {
        var percent = Math.round(100 * this.img_object.display_height / this.img_object.orig_height);
        if (percent) {
          if (this.zoom_object.length) {// added to call on a created object
            this.zoom_object.html(percent + "%");
          }
        }
      }
    },

    update_container_info: function () {
      this.settings.height = this.container.height();
      this.settings.width = this.container.width();
    },

    /**
    *   callback for handling mousdown event to start dragging image
    **/
    drag_start: function (e) {
      if (this.settings.onStartDrag &&
        this.settings.onStartDrag.call(this, this.getMouseCoords(e)) === false) {
        return false;
      }

      /* start drag event*/
      this.dragged = true;
      this.container.addClass("iviewer_drag_cursor");

      this.dx = e.pageX - this.img_object.x;
      this.dy = e.pageY - this.img_object.y;
      return false;
    },

    /**
    *   callback for handling mousmove event to drag image
    **/
    drag: function (e) {
      this.settings.onMouseMove && this.settings.onMouseMove.call(this, this.getMouseCoords(e));
      if (this.dragged) {
        this.settings.onDrag && this.settings.onDrag.call(this, this.getMouseCoords(e));
        var ltop =  e.pageY - this.dy;
        var lleft = e.pageX - this.dx;
        this.setCoords(lleft, ltop);
        return false;
      }
    },

    /**
    *   callback for handling stop drag
    **/
    drag_end: function (e) {
      this.container.removeClass("iviewer_drag_cursor");
      this.dragged=false;
    },

    click: function (e) {
      this.settings.onClick && this.settings.onClick.call(this, this.getMouseCoords(e));
    },

    /**
    *   create zoom buttons info box
    **/
    createui: function () {
      var me = this;
      $("<div>").addClass("iviewer_zoom_in").addClass("iviewer_common")
        .addClass("iviewer_button")
        .mousedown(function () {me.zoom_by(1); return false; }).appendTo(this.container);
      $("<div>").addClass("iviewer_zoom_out").addClass("iviewer_common")
        .addClass("iviewer_button")
        .mousedown(function () {me.zoom_by(-1); return false; }).appendTo(this.container);
      $("<div>").addClass("iviewer_zoom_zero").addClass("iviewer_common")
        .addClass("iviewer_button")
        .mousedown(function () {me.set_zoom(100); return false; }).appendTo(this.container);
      $("<div>").addClass("iviewer_zoom_fit").addClass("iviewer_common")
        .addClass("iviewer_button")
        .mousedown(function (){me.fit(this); return false; }).appendTo(this.container);
      this.zoom_object = $("<div>").addClass("iviewer_zoom_status").addClass("iviewer_common")
        .appendTo(this.container);
      this.update_status(); //initial status update
    }
  });

  $iv.extend({
    scaleValue: function (value, toZoom) {
      return value * toZoom / 100;
    },
    descaleValue: function (value, fromZoom) {
      return value * 100 / fromZoom;
    }
  });
})(jQuery);


/*
 * @description create zoomable image control on the current div#modal-fg
 */
function modalLayer_ShowIviewerZoomImage() {
  var modalFG = $("#modal-fg");
  // get the image object
  var newImg = modalFG.find("#modal-fg-wrapper img").get(0);
  // remove #iviewer_zoom_icon
  modalFG.find('#modal-fg-wrapper, #iviewer_zoom_icon').remove();
  // modify zoomcaption
  modalFG.find("#zoomcaption").attr({
      style: "text-align:center; position:absolute;bottom:0px;left:0px;font-weight:bold;width:100%;background: rgba(255, 255, 255, 0.5);"
    })
    // add (&ndash;) to remove the caption
    .prepend("<span id='removeCaption' style='color:#0645AD;float:right;font-size:small;font-weight:normal;cursor:pointer;' title='" + $.resource("ImageZoom2nd_removeCaption") + "'>&nbsp;(&ndash;)</span>")
    .parent().find('#removeCaption').click(function () {
      $('#zoomcaption').slideUp('slow');
    });
  // execute iviewer plugin
  modalFG.iviewer({
    zoom_min: 2, // minimal zoom
    zoom_init: 95,// initial image scale (1:1)
    // ┌─  http://species-id.net/o/media/thumb/a/a7/Lamium_album_plants.jpg/200px-Lamium_album_plants.jpg 
    // └─> http://species-id.net/o/media/a/a7/Lamium_album_plants.jpg
    src: newImg.src.replace(/(.+)\/thumb(\/[^\/]+\/[^\/]+\/[^\/]+)\/.+/, '$1$2'),
    initCallback: function (){
      var object = this;
      $("<div id='iviewer_zoom_in'/>").click(function () {object.zoom_by(1); });
      $("<div id='iviewer_zoom_out'/>").click(function () {object.zoom_by(-1); });
      $("<div id='iviewer_zoom_fit'/>").click(function () {object.fit(); });
      $("<div id='iviewer_zoom_orig'/>").click(function () {object.set_zoom(100); });
      $("<div id='iviewer_zoom_update'/>").click(function () {object.update_container_info(); });
    },
    onStartLoad: function () {
      // console.log("onStartLoad: try to load resource ImageZoom2nd_iconLoader to div.iviewer_zoom_status");
      modalFG.append('<img src="' + $.resource('ImageZoom2nd_iconLoader') + '"  id="loaderIconZoomImage" style="position:absolute;top:50%;left:50%;" />');
    },
    onZoom: function () {
      var layerMaxWidth = $(window).width() - (2 * 7 /* 7px */),
        zoomedImageWidth = $('#modal-fg img').width(),
        layerWidthSetToMax = Math.min(layerMaxWidth, zoomedImageWidth);
        
      if($('#modal-fg').width() < layerMaxWidth) {
        if (layerWidthSetToMax < layerMaxWidth && zoomedImageWidth > $('#modal-fg').width()) {
          // console.log('onZoom: set width of modal-fg anew');
          $('#modal-fg').css({width: layerWidthSetToMax + "px", "margin-left": - (layerWidthSetToMax/2)});
          // set image properly to resized container again
          this.update_container_info();
          // console.log('onZoom: try this center');
        }
      }
      // console.log('onZoom: width of image: ' + zoomedImageWidth + ' layerMaxWidth:' + layerMaxWidth + ' layerWidthSetToMax:' + layerWidthSetToMax);
    },
    onFinishLoad: function () {
      // console.log("onFinishLoad: loading is finished, set title attr of caption");
      // add tool tips
      modalFG.find("img").first()
        .attr("title", $.resource("ImageZoom2nd_toolTipMoveable"));
      // Next searching for CLASS, because iviewer generates class-div from IDs above!
      modalFG.find(".iviewer_zoom_in").attr("title", $.resource("ImageZoom2nd_zoomIn"));
      modalFG.find(".iviewer_zoom_out").attr("title", $.resource("ImageZoom2nd_zoomOut"));
      modalFG.find(".iviewer_zoom_fit").attr("title", $.resource("ImageZoom2nd_fitToWindow"));
      modalFG.find(".iviewer_zoom_zero").attr("title", $.resource("ImageZoom2nd_zoomToOrigSize"));
      modalFG.find(".iviewer_zoom_status").attr("title", $.resource("ImageZoom2nd_zoomedTo"));
      $('#loaderIconZoomImage').remove();
    }
  });
}// END modalLayer_ShowIviewerZoomImage()

$(document).ready(function () {// add to the resource string dictionary
  $.extend(true, $.jI18n, {
    en: {
      ImageZoom2nd_toolTipMoveable : "After zooming into (e.g. using the mouse wheel), images can be moved.",
      ImageZoom2nd_zoomedTo :        "Current zoom factor",
      ImageZoom2nd_fitToWindow :     "Fit into the window",
      ImageZoom2nd_zoomIn :          "Zoom in",
      ImageZoom2nd_zoomOut :         "Zoom out",
      ImageZoom2nd_zoomToOrigSize :  "Set to original size (1:1)",
      ImageZoom2nd_removeCaption:    "Hide the image caption"
    },
    de: {
      ImageZoom2nd_toolTipMoveable : "Nach Vergrößerung (z.B. durch Mausrad) kann man das Bild verschieben.",
      ImageZoom2nd_zoomedTo :        "Vergrößerungsfaktor",
      ImageZoom2nd_fitToWindow :     "In Fenster einpassen",
      ImageZoom2nd_zoomIn :          "Vergrößern",
      ImageZoom2nd_zoomOut :         "Verkleinern",
      ImageZoom2nd_zoomToOrigSize :  "Zeige in Originalgröße (1:1)",
      ImageZoom2nd_removeCaption:    "Bildbeschriftung verbergen"
    }
  });
});