PMA.UI Documentation by Pathomation

components/js/gallery.js

import { Resources } from '../../resources/resources';
import { checkBrowserCompatibility } from '../../view/helpers';
import { GalleryRenderOptions, Events, DragDropMimeType, parseDragData, getBarcodeUrl, getSnapshotUrl, getThumbnailUrl } from './components';
import { default as Ps } from 'perfect-scrollbar';
import 'perfect-scrollbar/css/perfect-scrollbar.css';
import $ from 'jquery';

// find the currently visible images for horizontal scrolling and loads them
function loadVisibleX() {
    var rail = $(this.element).find(".ps__rail-x");
    var left = -Infinity;
    var right = Infinity;

    if (rail && rail.position()) {
        left = rail.position().left;
        right = left + rail.width();
    }

    if (left === 0 && right === 0) {
        left = -Infinity;
        right = Infinity;
    }

    var self = this;

    $(this.element).find("li.lazy").each(function () {
        var el = $(this);
        var elLeft = el.position().left;
        var elRight = elLeft + el.width();
        if (!(elLeft > right || elRight < left)) {
            loadImage.call(self, el);
        }
    });
}

// find the currently visible images for vertical scrolling and loads them
function loadVisibleY() {
    var rail = $(this.element).find(".ps__rail-y");
    var top = -Infinity;
    var bottom = Infinity;

    if (rail && rail.position()) {
        top = rail.position().top;
        bottom = top + rail.height();
    }

    if (top === bottom) {
        top = -Infinity;
        bottom = Infinity;
    }

    var self = this;

    $(this.element).find("li.lazy").each(function () {
        var el = $(this);
        var elTop = el.position().top;
        var elBottom = elTop + el.height();

        if (!(elTop > bottom || elBottom < top)) {
            loadImage.call(self, el);
        }
    });
}

// lazy loading background images
function loadImage(li) {
    var self = this;
    li.removeClass("lazy");
    var div = li.find("div[data-img]");
    var src = div.data("img");
    var rot = div.data("rotation");

    $("<img />").bind("load", function () {
        li.removeClass("loading");
        div.css("background-image", "url(\"" + src + "\")");

        if (self.mode === "vertical") {
            div.css("width", "100%");
        }
        else {
            div.css("width", self.thumbnailWidth + "px");
        }

        div.css("height", self.thumbnailHeight + "px");
        div.css("margin", "0 auto");
        if (rot) {
            var w = this.width;
            var h = this.height;
            var rad = rot / (180 / Math.PI);
            var factorWidth = w / (w * Math.abs(Math.cos(rad)) + h * Math.abs(Math.sin(rad)));
            var factorHeight = h / (w * Math.abs(Math.sin(rad)) + h * Math.abs(Math.cos(rad)));
            div.css("transform", "rotate(" + rot + "deg) scale(" + Math.min(factorWidth, factorHeight) + ")");
        }
    }).bind("error", function () {
        li.removeClass("loading");
        div.addClass("no-image");
        div.html(Resources.translate("Failed to load image"));

        if (self.mode === "vertical") {
            div.css("width", "100%");
        }
        else {
            div.css("width", self.thumbnailWidth + "px");
        }

        div.css("height", self.thumbnailHeight + "px");
        div.css("margin", "0 auto");
        div.css("padding-top", (self.thumbnailHeight / 3) + "px");

        var a = li.find("a");
        var serverUrl = a.data("server");
        var path = a.data("path");
        self.fireEvent(Events.SlideInfoError, { serverUrl: serverUrl, path: path });
    }).attr("src", src);

    var showBarcodeNormal = this.renderOptions == GalleryRenderOptions.All;
    if (!showBarcodeNormal) {
        return;
    }

    var a = li.find("a");
    var serverUrl = a.data("server");
    var path = a.data("path");

    this.context.getImageInfo.call(
        this.context,
        serverUrl,
        path,
        function (sessionId, imageInfo) {
            var hasBarcode = imageInfo.AssociatedImageTypes.indexOf("Barcode") > -1;
            if (!hasBarcode) {
                return;
            }

            // load barcode if it exists
            var barcodeImage = li.find("img.barcode");
            if (barcodeImage.length !== 0) {
                barcodeImage.bind("error", function () {
                    barcodeImage.hide();
                });

                barcodeImage.attr("src", barcodeImage.data("src"));
                barcodeImage.css("width", Math.round(self.thumbnailWidth * 0.4) + "px");
                barcodeImage.css("display", "block");
                barcodeImage.removeClass("hidden");
            }
        },
        function () {
        }
    );
}

function imageClick(element, fromUserInteraction) {
    var $el = $(element);

    var alreadySelected = $el.parent().hasClass("selected");
    if (alreadySelected) {
        $el.parent().removeClass("selected");
        this.fireEvent(Events.SlideDeSelected, { serverUrl: $el.data("server"), path: $el.data("path"), index: $el.parent().index(), userInteraction: fromUserInteraction === true });
    }

    if (!this.multiSelect) {
        $(this.element).find("ul li").removeClass("selected");
    }

    if (!alreadySelected) {
        $el.parent().addClass("selected");
        this.fireEvent(Events.SlideSelected, { serverUrl: $el.data("server"), path: $el.data("path"), index: $el.parent().index(), userInteraction: fromUserInteraction === true });
    }
}

function printMessage(message) {
    var style = " style=' ";
    if (this.thumbnailHeight > 0) {
        style += "height: " + this.thumbnailHeight + "px!important; ";
    }

    style += "' ";

    this.element.innerHTML = "<ul><li class='empty-message' " + style + "><span>" + message + "</span></li></ul>";
}

function renderImages(serverUrl, sessionId, images, doneCb, append) {
    var _this = this;
    if (images.length === 0) {
        printMessage.call(_this, Resources.translate("No images found"));
        if (typeof doneCb === "function") {
            doneCb();
        }

        return;
    }

    // var showBarcodeNormal = this.renderOptions == GalleryRenderOptions.All;
    var showBarcodeOnly = this.renderOptions == GalleryRenderOptions.Barcode;

    var imagesObj = [];
    for (var i = 0; i < images.length; i++) {
        var img = { path: images[i], hasBarcode: true, rotation: 0 };

        // if (typeof images[i] === 'object' && images[i].hasOwnProperty('path')) {
        if (typeof images[i] === 'object' && Object.prototype.hasOwnProperty.call(images[i], 'path')) {
            img.path = images[i].path;
        }

        // if (typeof images[i] === 'object' && images[i].hasOwnProperty('rotation')) {
        if (typeof images[i] === 'object' && Object.prototype.hasOwnProperty.call(images[i], 'rotation')) {
            img.rotation = images[i].rotation;
        }

        // if (typeof images[i] === 'object' && images[i].hasOwnProperty('snapshotParameters')) {
        if (typeof images[i] === 'object' && Object.prototype.hasOwnProperty.call(images[i], 'snapshotParameters')) {
            img.snapshotParameters = images[i].snapshotParameters;
        }

        img.hasBarcode = true;
        imagesObj.push(img);
    }

    if (showBarcodeOnly) {
        this.context.getImagesInfo({
            serverUrl: serverUrl, images: imagesObj.map(function (r) { return r.path; }), success: function (sessionId, imagesInfo) {
                for (var i = 0; i < imagesInfo.length; i++) {
                    var hasBarcode = imagesInfo[i].AssociatedImageTypes.indexOf("Barcode") > -1;
                    for (var j = 0; j < imagesObj.length; j++) {
                        if (imagesObj[j].path == imagesInfo[i].Filename) {
                            imagesObj[j].hasBarcode = hasBarcode;
                            break;
                        }
                    }
                }

                continueRenderImages.call(_this, serverUrl, sessionId, imagesObj, doneCb, append);
            },
            failure: function () {
                continueRenderImages.call(_this, serverUrl, sessionId, imagesObj, doneCb, append);
            }
        });
    }
    else {
        continueRenderImages.call(_this, serverUrl, sessionId, imagesObj, doneCb, append);
    }
}

function continueRenderImages(serverUrl, sessionId, imagesObj, doneCb, append) {
    var _this = this;

    var showBarcodeNormal = this.renderOptions == GalleryRenderOptions.All;
    var showBarcodeOnly = this.renderOptions == GalleryRenderOptions.Barcode;

    var loadingImgMarginX = Math.floor(_this.thumbnailWidth / 3);
    var loadingImgMarginY = Math.floor(_this.thumbnailHeight / 3);
    var imageStyle = "margin: " + loadingImgMarginY + "px " + (_this.mode === "horizontal" || _this.mode === "grid" ? loadingImgMarginX + "px" : "auto") + "; width: " + loadingImgMarginX + "px; height: " + loadingImgMarginY + "px;";
    var aStyle = "";
    var listStyle = "";
    var liStyle = "";
    var tw = _this.thumbnailWidth,
        th = _this.thumbnailHeight;

    if (_this.mode === "horizontal") {
        listStyle = " style='width: " + (imagesObj.length * _this.thumbnailWidth) + "px; height: " + _this.thumbnailHeight + "px; overflow: hidden;' ";
        aStyle = " style='max-width: " + _this.thumbnailWidth + "px;' ";
        tw = 0;
    }
    else if (_this.mode === "vertical") {
        th = 0;
    }
    else if (_this.mode === "grid") {
        // aStyle = " style='width: " + (_this.thumbnailWidth + 32) + "px;height: " + (_this.thumbnailHeight + 32) + "px;' ";
        aStyle = " style='width: auto;height: " + (_this.thumbnailHeight + 32) + "px;' ";
        liStyle = " style='width: " + (_this.thumbnailWidth + 32) + "px;' ";
        tw = 0;
    }

    var html = "<ul" + listStyle + ">";

    $(_this.element).find("ul li.emptyli").remove();
    if (append === true) {
        html = "";
    }

    for (var i = 0; i < imagesObj.length; i++) {
        var path = imagesObj[i].path;
        var extrastyle = '';
        var extradata = '';

        var thumbUrl = getThumbnailUrl(serverUrl, sessionId, path, 0, tw, th);

        // if (imagesObj[i].hasOwnProperty('snapshotParameters')) {
        if (Object.prototype.hasOwnProperty.call(imagesObj[i], 'snapshotParameters')) {
            thumbUrl = getSnapshotUrl(serverUrl, sessionId, path, imagesObj[i].snapshotParameters, tw, th, "jpg");
        }
        // else if (imagesObj[i].hasOwnProperty('rotation')) {v
        else if (Object.prototype.hasOwnProperty.call(imagesObj[i], 'rotation')) {
            var rInt = parseInt(imagesObj[i].rotation);
            if (!isNaN(rInt) && rInt != 0) {
                extrastyle = 'transform:rotate(' + rInt + 'deg);';
                extradata = 'data-rotation="' + rInt + '"';
            }
        }

        var barcodeUrl = getBarcodeUrl(serverUrl, sessionId, path, _this.barcodeRotation ? _this.barcodeRotation : 0);

        html +=
            "<li draggable='true' class='lazy loading'" + liStyle + "><a " + aStyle + " data-server='" + serverUrl + "' data-path='" + path + "' href='#'>";
        if (_this.showFileName) {
            if (typeof _this.filenameCallback === "function") {
                html += "<span>" + _this.filenameCallback(serverUrl, path) + "</span>";
            }
            else {
                html += "<span>" + path.split('/').pop() + "</span>";
            }
        }

        html += "<div " + extradata + " data-img='" + (showBarcodeOnly ? barcodeUrl : thumbUrl) + "' style=' " + imageStyle + extrastyle + "'></div>";

        if (showBarcodeNormal) {
            html += '<img class="barcode hidden" data-src="' + barcodeUrl + '" />';
        }

        html += '</a>';

        if (typeof _this.additionalHtmlCallback === "function") {
            html += _this.additionalHtmlCallback(imagesObj[i]);
        }

        html += '</li>';
    }

    if (this.mode === "grid") {
        for (var p = 0; p < 10; p++) {
            html += "<li class='emptyli' " + liStyle + " ></li>";
        }
    }

    if (append !== true) {
        html += "</ul>";
    }

    if (append === true) {
        $(_this.element).find("ul").append(html);
    }
    else {
        _this.element.innerHTML = html;
    }

    $(_this.element).find("ul li:not(.emptyli)").each(function (index, element) {
        element.addEventListener("dragstart", dragstart.bind(this, element), false);
    });

    if (_this.mode === "horizontal") {
        // fix width once loaded
        var horUl = $(_this.element).find("ul");
        var horWidth = horUl.outerWidth(true) - horUl.width();

        $(_this.element).find("ul li").each(function () {
            horWidth += $(this).outerWidth(true) + 2;
        });

        horUl.css("width", horWidth + "px");
        horUl.css("height", "");
        horUl.css("overflow", "");
    }

    $(_this.element).find("ul li a").click(function (ev) {
        ev.preventDefault();
        imageClick.call(_this, this, true);
    });

    if (append !== true) {
        _this.ps = new Ps(_this.element, { useBothWheelAxes: true, wheelPropagation: true, swipePropagation: true });
    }
    else {
        _this.ps.update();
    }

    // bind scroll events for lazy loading
    if (_this.mode === "horizontal") {
        $(_this.element).on('ps-scroll-x', function () {
            clearTimeout(_this.lazyLoadTimeOut);
            _this.lazyLoadTimeOut = setTimeout(loadVisibleX.bind(_this), 500);
        });

        loadVisibleX.call(_this);
    }
    else {
        $(_this.element).on('ps-scroll-y', function () {
            clearTimeout(_this.lazyLoadTimeOut);
            _this.lazyLoadTimeOut = setTimeout(loadVisibleY.bind(_this), 500);
        });

        loadVisibleY.call(_this);
    }

    if (typeof doneCb === "function") {
        doneCb();
    }
}
/**
 * A private function which load slides from one server only
 * @param  {string} serverUrl - The URL of the PMA.core server to get images from
 * @param  {string[]|Object[]} images - An array of strings that contains the paths of the images to load or an array of objects that contains the path and rotation of the images as desribed below
 * @param  {string} images.path - The path of the image to load
 * @param  {string} images.rotation - The rotation of the image in degrees
 * @param  {Components~snapshotParameters} images.snapshotParameters - Optional snapshot parameters to show 
 * @param  {function} [doneCb] - Called when image loading is complete
 * @ignore
 */
function loadSlides(serverUrl, images, doneCb) {
    if (this.ps) {
        this.ps.destroy();
    }

    var _this = this;
    _this.context.getSession(serverUrl, function (sessionId) {
        renderImages.call(_this, serverUrl, sessionId, images, doneCb);
    });
}

function refresh() {
    if (this.lastLoadedImages.length > 0) {
        for (var i = 0; i < this.lastLoadedImages.length; i++) {
            renderImages.call(this, this.lastLoadedImages[i].serverUrl, this.lastLoadedImages[i].sessionId, this.lastLoadedImages[i].images, null);
        }
    }
}

function ondragover(ev) {
    ev.preventDefault();
    var types = ev.dataTransfer.types;
    let hasData = false;
    if (types) {
        if (types.indexOf) {
            hasData = types.indexOf("application/x-fancytree-node") > -1 || types.indexOf(DragDropMimeType) > -1;
        }
        else if (types.contains) {
            // in IE and EDGE types is DOMStringList
            hasData = types.contains("application/x-fancytree-node") || types.contains(DragDropMimeType);
        }

        // var nodeData = PMA.UI.Components.parseDragData(ev.dataTransfer);
        if (hasData) {
            if (ev.altKey) {
                ev.dataTransfer.dropEffect = "move";
            }
            else {
                ev.dataTransfer.dropEffect = "copy";
            }

            return;
        }

        ev.dataTransfer.dropEffect = "none";
    }
}

function ondrop(ev) {
    ev.preventDefault();
    var self = this;
    var nodeData = parseDragData(ev.dataTransfer);
    var append = ev.altKey == false;
    if (nodeData && nodeData.path && nodeData.serverUrl && nodeData.source !== "gallery") {
        if (nodeData.isFolder) {
            this.context.getSlides({
                serverUrl: nodeData.serverUrl,
                path: nodeData.path,
                success: function (sessionId, files) {
                    if (files == null || files.length == 0) {
                        self.fireEvent(Events.Dropped, { serverUrl: nodeData.serverUrl, path: nodeData.path, isFolder: true, append: append && self.lastLoadedImages.length > 1 });
                        return;
                    }

                    self.lastLoadedImages.push([{ serverUrl: nodeData.serverUrl, sessionId: sessionId, images: files }]);
                    renderImages.call(self, nodeData.serverUrl, sessionId, files, function () {
                        self.fireEvent(Events.Dropped, { serverUrl: nodeData.serverUrl, path: nodeData.path, isFolder: true, append: append && self.lastLoadedImages.length > 1 });
                    }, append && self.lastLoadedImages.length > 1);
                },
                failure: function () {
                    console.error("Error loading slides from directory");
                }
            });
        }
        else {
            var imageArray = [{ serverUrl: nodeData.serverUrl, path: nodeData.path }];

            self.context.getSession(nodeData.serverUrl, function (sessionId) {
                self.lastLoadedImages.push({ serverUrl: nodeData.serverUrl, sessionId: sessionId, images: imageArray });
                renderImages.call(self, nodeData.serverUrl, sessionId, imageArray, function () {
                    self.fireEvent(Events.Dropped, { serverUrl: nodeData.serverUrl, path: nodeData.path, isFolder: false, append: append && self.lastLoadedImages.length > 1 });
                }, append && self.lastLoadedImages.length > 1);
            });

            return;
        }
    }
}

function initializeDropZone() {
    if (this.element) {
        this.element.addEventListener("drop", ondrop.bind(this), false);
        this.element.addEventListener("dragover", ondragover.bind(this), false);
    }
}

function dragstart(element, ev) {
    var link = $(element).find("a");
    if (link) {
        var d = JSON.stringify({
            serverUrl: link.data("server"),
            path: link.data("path"),
            isFolder: false,
            source: "gallery"
        });

        ev.dataTransfer.setData("text", d);
        ev.dataTransfer.setData(DragDropMimeType, d);
    }
}

/**
 * Function that returns a string to be displayed on top of a thumbnail
 * @callback Gallery~callback
 * @param {string} serverUrl - The serverUrl of the image
 * @param {string} filename - The virtual path of the image
 * @returns {string}
 */
/**
 * Function that returns a string to be displayed on below a thumbnail
 * @callback Gallery~additionalHtmlCallback
 * @param {object} image - The image object used to render this thumbnail
 * @returns {string}
 */

export
    /**
     * Represents a UI component that shows image thumbnails. Provides events to handle click and multiple selection, as well as built-in lazy loading functionality.
     * @memberof PMA.UI.Components
     * @alias Gallery
     * @param {Context} context
     * @param {object} options - Configuration options
     * @param {string|HTMLElement} options.element - The element that hosts the gallery. It can be either a valid CSS selector or an HTMLElement instance.
     * @param {Number} options.thumbnailWidth - The desired width of the displayed thumbnails.
     * @param {Number} options.thumbnailHeight - The desired height of the displayed thumbnails.
     * @param {string} [options.mode="horizontal"] - "horizontal", "vertical" or "grid"
     * @param {Gallery~callback} [options.filenameCallback] - Callback to override the displayed name of each image.
     * @param {Gallery~additionalHtmlCallback} [options.additionalHtmlCallback] - Callback to provide additional HTML to render on top of the thumbnail.
     * @param {boolean} [options.showFileName=false] - Whether or not to print each image's name
     * @param {GalleryRenderOptions} [options.renderOptions = PMA.UI.Components.GalleryRenderOptions.All] - Whether to render thumbnail only, barcode only or both
     * @param {Number} [options.barcodeRotation=0] - Rotation of the barcode in steps of 90 degrees
     * @param {boolean} [options.multiSelect=false] - Whether or not to allow multiple files to be selected
     * @fires PMA.UI.Components.Events.SlideInfoError
     * @fires PMA.UI.Components.Events.SlideSelected
     * @fires PMA.UI.Components.Events.SlideDeSelected
     * @fires PMA.UI.Components.Events.Dropped
     * @tutorial 03-gallery
     * @tutorial 04-tree
     */
    class Gallery {
    constructor(context, options) {
        if (!checkBrowserCompatibility()) {
            return;
        }

        if (options.element instanceof HTMLElement) {
            this.element = options.element;
        }
        else if (typeof options.element == "string") {
            var el = document.querySelector(options.element);
            if (!el) {
                console.error("Invalid selector for element");
            }
            else {
                this.element = el;
            }
        }
        else {
            console.error("Invalid element");
            return;
        }

        if (isNaN(options.thumbnailHeight) || isNaN(options.thumbnailWidth) || options.thumbnailHeight <= 0 || options.thumbnailWidth <= 0) {
            console.error("thumbnailWidth & thumbnailHeight must be positive integers");
            return;
        }

        this.listeners = {};
        this.listeners[Events.SlideDeSelected] = [];
        this.listeners[Events.SlideSelected] = [];
        this.listeners[Events.SlideInfoError] = [];
        this.listeners[Events.Dropped] = [];

        this.filenameCallback = options.filenameCallback;
        this.additionalHtmlCallback = options.additionalHtmlCallback;
        this.multiSelect = options.multiSelect === true;
        this.lazyLoadTimeOut = 0;
        this.showFileName = options.showFileName === true;
        this.barcodeRotation = options.barcodeRotation;
        this.context = context;
        this.thumbnailWidth = options.thumbnailWidth;
        this.thumbnailHeight = options.thumbnailHeight;

        // a helper array that holds the last loaded images as objects  { serverUrl, sessionId, imageArray }
        this.lastLoadedImages = [];

        if (options.mode !== "horizontal" && options.mode !== "vertical" && options.mode !== "grid") {
            options.mode = "horizontal";
        }

        this.renderOptions = options.renderOptions ? options.renderOptions : GalleryRenderOptions.All;

        // for backwards compatibility keep the showBarcode option
        if (options.showBarcode === false) {
            this.renderOptions = GalleryRenderOptions.Thumbnail;
        }
        this.mode = options.mode;

        $(this.element).addClass("pma-ui-gallery");
        if (this.mode === "vertical") {
            $(this.element).addClass("vertical");
        }
        else if (this.mode === "grid") {
            $(this.element).addClass("grid");
        }

        printMessage.call(this, Resources.translate("No images found"));

        initializeDropZone.call(this);
    }
    /**
     * Attaches an event listener
     * @param {PMA.UI.Components.Events} eventName - The name of the event to listen to
     * @param {function} callback - The function to call when the event occurs
     */
    listen(eventName, callback) {
        // if (!this.listeners.hasOwnProperty(eventName)) {
        if (!Object.prototype.hasOwnProperty.call(this.listeners, eventName)) {
            console.error(eventName + " is not a valid event");
        }

        this.listeners[eventName].push(callback);
    }
    // fires an event
    fireEvent(eventName, eventArgs) {
        // if (!this.listeners.hasOwnProperty(eventName)) {
        if (!Object.prototype.hasOwnProperty.call(this.listeners, eventName)) {
            console.error(eventName + " does not exist");
            return;
        }

        for (var i = 0, max = this.listeners[eventName].length; i < max; i++) {
            this.listeners[eventName][i].call(this, eventArgs);
        }
    }
    /**
     * Loads the thumbnails of all the images found directly under a given directory
     * @param  {string} serverUrl - The URL of the PMA.core server to get images from
     * @param  {string} directory - The path of a directory to load images from
     * @param  {function} [doneCb] - Called when image loading is complete
     */
    loadDirectory(serverUrl, directory, doneCb) {
        if (this.ps) {
            this.ps.destroy();
        }
        var _this = this;
        this.lastLoadedImages = [];

        printMessage.call(_this, Resources.translate("<i class='fa fa-spinner fa-spin'></i> Loading"));
        this.context.getSlides({
            serverUrl: serverUrl,
            path: directory,
            success: function (sessionId, files) {
                _this.lastLoadedImages = [{ serverUrl: serverUrl, sessionId: sessionId, images: files }];
                renderImages.call(_this, serverUrl, sessionId, files, doneCb);
            },
            failure: function (error) {
                _this.element.innerHTML = error.Message;
                _this.fireEvent(Events.SlideInfoError, { serverUrl: serverUrl, path: directory, message: error.Message });
            }
        });
    }

    /**
     * Loads the thumbnails of all the provided images
     * @param {Object[]} images - An array of image objects containing the path, rotation and server url for each image to load
     * @param {string} images.serverUrl - The URL of the PMA.core server to load this image from
     * @param {string} images.path - The path of the image to load
     * @param {string} images.rotation - The rotation of the image in degrees
     * @param {function} doneCb - Called when image loading is complete
     */
    loadSlides(images, doneCb) {
        if (typeof images === "string") {
            // the old deprecated function is used if the first parameter is of type string, corresponding to the server url
            // loadSlides = function(serverUrl, images, doneCb)
            loadSlides.call(this, arguments[0], arguments[1], arguments[2]);
            return;
        }

        if (this.ps) {
            this.ps.destroy();
        }

        var servers = {};
        var serverUrls = [];
        var _this = this;

        if (!images || images.length == 0) {
            // call with empty parameters to clear all 
            renderImages.call(this, "", "", [], doneCb);
            return;
        }

        for (var i = 0; i < images.length; i++) {
            if (images[i].serverUrl) {
                // if (!servers.hasOwnProperty(images[i].serverUrl)) {
                if (!Object.prototype.hasOwnProperty.call(servers, images[i].serverUrl)) {
                    servers[images[i].serverUrl] = [];
                    serverUrls.push(images[i].serverUrl);
                }

                servers[images[i].serverUrl].push({ path: images[i].path, rotation: images[i].rotation });
            }
        }

        _this.lastLoadedImages = [];
        var c = 0;
        var getSessionFunc = (function (serverUrl, imageArray, gallery, cb, count, done) {
            gallery.context.getSession(serverUrl, function (sessionId) {
                _this.lastLoadedImages.push({ serverUrl: serverUrl, sessionId: sessionId, images: imageArray });
                renderImages.call(gallery, serverUrl, sessionId, imageArray, function () {
                    if (done && typeof cb === "function") {
                        cb();
                    }
                }, count != 0);
            });
        });

        for (i = 0; i < serverUrls.length; i++) {
            getSessionFunc(serverUrls[i], servers[serverUrls[i]], _this, doneCb, c, ++c >= serverUrls.length);
        }
    }
    /**
     * Selects or deselects a slide
     * @param  {Number} index - The index of the slide to select
     * @fires PMA.UI.Components.Events.SlideSelected
     * @fires PMA.UI.Components.Events.SlideDeSelected
     */
    selectSlide(index) {
        if (index === undefined || index === null) {
            $(this.element).find("ul li").removeClass("selected");
            return;
        }

        var el = $(this.element).find("ul li:nth-child(" + (index + 1) + ") a")[0];
        imageClick.call(this, el, false);
    }
    /**
     * Highlights or unhighlights a slide
     * @param  {Number} index - The index of the slide to highlight
     * @param  {boolean} highlight - True to highlight, otherwise false
     */
    highlightSlide(index, highlight) {
        if (index === undefined || index === null) {
            $(this.element).find("ul li").removeClass("selected");
            return;
        }

        var el = $(this.element).find("ul li:nth-child(" + (index + 1) + ") a");
        if (highlight === true) {
            el.parent().addClass("selected");
        }
        else {
            el.parent().removeClass("selected");
        }
    }
    /**
     * Slide information
     * @typedef {Object} Gallery~slide
     * @property {string} server - The URL of the PMA.core server this slide has been loaded from
     * @property {string} path - The path of the slide
     * @property {Number} index - The index of the slide in the gallery
     */
    /**
     * Returns the first of the currently selected slides, or null
     * @return {Gallery~slide}
     */
    getSelectedSlide() {
        var el = $(this.element).find("ul li.selected a:first-child");
        if (el.length === 0) {
            return null;
        }
        else {
            return { server: el.data("server"), path: el.data("path"), index: el.parent().index() };
        }
    }
    /**
     * Returns the currently selected slides, or null
     * @return {Gallery~slide[]}
     */
    getSelectedSlides() {
        var el = $(this.element).find("ul li.selected a:first-child");
        if (el.length === 0) {
            return null;
        }
        else {
            var result = [];
            el.each(function () {
                result.push({ server: $(this).data("server"), path: $(this).data("path"), index: $(this).parent().index() });
            });

            return result;
        }
    }
    /**
     * Returns all the currently loaded slides
     * @return {Gallery~slide[]}
     */
    getSlides() {
        var el = $(this.element).find("ul li a:first-child");
        if (el.length === 0) {
            return null;
        }
        else {
            var result = [];
            el.each(function () {
                result.push({ server: $(this).data("server"), path: $(this).data("path"), index: $(this).parent().index() });
            });

            return result;
        }
    }
    /**
     * Sets the render options
     * @param {PMA.UI.Components.GalleryRenderOptions} option - The render option to set
     */
    setRenderOptions(option) {
        if (option && this.renderOptions != option) {
            this.renderOptions = option;
            refresh.call(this);
        }
    }
    /**
     * Toggles the mode of the gallery
     * @param {String} mode - The mode to set, one of  "horizontal", "vertical" or "grid"
     */
    setMode(mode) {
        if (this.mode !== mode) {
            this.mode = mode;
            $(this.element).removeClass("vertical grid");
            if (this.mode === "vertical") {
                $(this.element).addClass("vertical");
            }
            else if (this.mode === "grid") {
                $(this.element).addClass("grid");
            }
            refresh.call(this);
        }
    }
}