PMA.UI Documentation by Pathomation

components/js/components.js

/**
 * PMA.UI.Components contains UI and utility components that interact with a viewport and PMA.core
 * @namespace PMA.UI.Components
 */

import { Resources } from "../../resources/resources";
import { loginSupportsPost } from "../../view/version";

/**
 * Events fired by components
 * @readonly
 * @enum {string}
 * @namespace Components.Events
 */
export const Events = {
    /**
     * Fires when a directory has been selected by a PMA.UI.Components.Tree instance
     * @event Components.Events#DirectorySelected
     * @param {Object} args
     * @param {string} args.serverUrl
     * @param {string} args.path
     */
    DirectorySelected: "directorySelected",

    /**
     * Fires when a slide image has been selected by a PMA.UI.Components.Tree or PMA.UI.Components.Gallery or a PMA.UI.Components.MetadataTree instance
     * @event Components.Events#SlideSelected
     * @param {Object} args
     * @param {string} args.serverUrl - The server url this slide belongs to
     * @param {string} args.path - The path to the slide
     * @param {Number} [args.index] - The index of the slide selected in the gallery components (PMA.UI.Components.Gallery only)
     * @param {boolean} [args.userInteraction] - Whether this event was fired by a user interaction or programmatically (PMA.UI.Components.Gallery only)
     */
    SlideSelected: "slideSelected",

    /**
     * Fires when a server node has been selected by a PMA.UI.Components.Tree instance
     * @event Components.Events#ServerSelected
     * @param {Object} args
     * @param {string} args.serverUrl
     */
    ServerSelected: "serverSelected",

    /**
     * Fires when a server node has been expanded by a PMA.UI.Components.Tree instance
     * @event Components.Events#ServerExpanded
     * @param {Object} args
     * @param {string} args.serverUrl
     */
    ServerExpanded: "serverExpanded",

    /**
     * Fires when a folder node has been expanded by a PMA.UI.Components.Tree instance
     * @event Components.Events#DirectoryExpanded
     * @param {Object} args
     * @param {string} args.serverUrl
     * @param {string} args.path
     */
    DirectoryExpanded: "directoryExpanded",

    /**
     * Fires when slide images have been checked by a PMA.UI.Components.Tree
     * @event Components.Events#MultiSelectionChanged
     * @param {Object[]} args
     * @param {string} args.serverUrl
     * @param {string} args.path
     */
    MultiSelectionChanged: "multiSelectionChanged",

    /**
     * Fires when a slide image has been deselected by a PMA.UI.Components.Tree or PMA.UI.Components.Gallery instance
     * @event Components.Events#SlideDeSelected
     */
    SlideDeSelected: "slideDeSelected",

    /**
     * Fires when a form has been saved to PMA.core
     * @event Components.Events#TreeNodeDoubleClicked
     * @param {Object} args
     * @param {string} args.serverUrl
     * @param {string} args.path
     * @param {boolean} isSlide
     */
    TreeNodeDoubleClicked: "treeNodeDoubleClicked",

    /**
     * Fires when image info could not be loaded
     * @event Components.Events#SlideInfoError
     */
    SlideInfoError: "SlideInfoError",

    /**
     * Fires before a viewport attempts to load an image
     * @event Components.Events#BeforeSlideLoad
     */
    BeforeSlideLoad: "BeforeSlideLoad",

    /**
     * Fires after a viewport has finished loading an image
     * @event Components.Events#SlideLoaded
     * @param {Object} args - The slide loaded arguments
     * @param {string} args.serverUrl - The server the slide belongs to
     * @param {string} args.path - The slide path
     * @param {boolean} args.dropped - Whether the slide was loaded via a drag and drop operation
     */
    SlideLoaded: "SlideLoaded",

    /**
     * Fires after when a new annotation has been added
     * @event Components.Events#AnnotationAdded
     * @param {Object} args
     * @param {Object} args.feature - The annotation object
     */
    AnnotationAdded: "annotationAdded",

    /**
     * Fires when annotation drawing begins
     * @event Components.Events#AnnotationDrawing
     */
    AnnotationDrawing: "annotationDrawing",

    /**
     * Fires when an annotation has been deleted
     * @event Components.Events#AnnotationDeleted
     * @param {Object}  args
     * @param {Number}  args.annotationId
     * @param {Object}  args.feature
     */
    AnnotationDeleted: "annotationDeleted",

    /**
     * Fires when an annotation has been modified
     * @event Components.Events#AnnotationModified
     */
    AnnotationModified: "annotationModified",

    /**
     * Fires when the annotations have been saved to PMA.core
     * @event Components.Events#AnnotationsSaved
     * @param {Object} args
     * @param {boolean} args.success
     */
    AnnotationsSaved: "annotationsSaved",

    /**
     * Fires when the currently selected annotation(s) have changed
     * @event Components.Events#AnnotationsSelectionChanged
     * @param {Object[]} args - The selected annotation objects
     */
    AnnotationsSelectionChanged: "annotationsSelectionChanged",

    /**
     * Fires when annotation editing begins
     * @event Components.Events#annotationEditingStarted
     */
    AnnotationEditingStarted: "annotationEditingStarted",

    /**
     * Fires when annotation editing ends
     * @event Components.Events#annotationEditingEnded
     */
    AnnotationEditingEnded: "annotationEditingEnded",

    /**
     * Fires when a form has been saved to PMA.core
     * @event Components.Events#FormSaved
     * @param {Object} args - The result of the save operation
     * @param {string} args.message - The error message if any
     * @param {boolean} args.success - True if save was successful
     */
    FormSaved: "formSaved",

    /**
     * Fires when a the edit button of a read only form is clicked
     * @event Components.Events#FormEditClick
     */
    FormEditClick: "formEditClick",

    /**
     * Fires when the state of the synchronization of viewports has changed (enabled/disabled)
     * @event Components.Events#SyncChanged
     * @param {boolean} enabled - Whether syncronization was enabled
     */
    SyncChanged: "syncChanged",

    /**
     * Fires when a search has started
     * @event Components.Events#SearchStarted
     */
    SearchStarted: "searchStarted",

    /**
     * Fires when a search has finished
     * @event Components.Events#SearchFinished
     * @param {Object.<string, string[]>} results - The object containing the result for each server available
     */
    SearchFinished: "searchFinished",

    /**
     * Fires when a search has failed
     * @event Components.Events#SearchFailed
     * @param {Tree~server} server - The object containing the server failed to fetch search results for
     */
    SearchFailed: "searchFailed",

    /** 
     * Fires when a form value is expanded by a PMA.UI.Components.MetadataTree
     * @event Components.Events#ValueExpanded
     * @param {Object} args - The arguments passed to this event
     * @param {string} args.serverUrl - The server url that the form value belongs to
     * @param {string[]} args.slides - An array of the paths for the slides belonging to the formvalue expanded
     */
    ValueExpanded: "valueExpanded",

    /** 
     * Fires when data are dropped on a PMA.UI.Components.Gallery
     * @event Components.Events#Dropped
     * @param {Object} args - The arguments passed to this event
     * @param {string} args.serverUrl - The server url
     * @param {string} args.path - The path to a slide or directory dropped 
     * @param {boolean} args.isFolder - Whether a directory was dropped  
     * @param {boolean} args.append - Whether the slide/directory was appended or replaced(alt key was pressed)
     */
    Dropped: "dropped",

    /**
     * Fires when a session id authentication fails
     * @event Components.Events#SessionIdLoginFailed
     * @param {Object} args - The arguments passed to this event
     * @param {string} args.serverUrl - The server url
     */
    SessionIdLoginFailed: "sessionIdLoginFailed",

    /** 
     * Fires when a slide is dropped in the {@link PMA.UI.Components.SlideLoader}
     * @event Components.Events#BeforeDrop
     * @param {Object} args - The arguments passed to this event
     * @param {string} args.serverUrl - The serverUrl
     * @param {string} args.path - The path to the slide  dropped
     * @param {Object} args.node - The node metadata object
     * @returns {boolean} - Whether to cancel the slide loading or not
     */
    BeforeDrop: "beforeDrop"
};

/**
 * PMA.core API methods
 * @readonly
 * @enum {string}
 */
export const ApiMethods = {
    Authenticate: "Authenticate",
    GetFiles: "GetFiles",
    GetDirectories: "GetDirectories",
    GetImageInfo: "GetImageInfo",
    GetImagesInfo: "GetImagesInfo",
    DeAuthenticate: "DeAuthenticate",
    GetForms: "GetForms",
    GetFormDefinitions: "GetFormDefinitions",
    GetFormSubmissions: "GetFormSubmissions",
    GetForm: "GetForm",
    SaveFormDefinition: "SaveFormDefinition",
    DeleteFormDefinition: "DeleteFormDefinition",
    SaveFormData: "SaveFormData",
    GetFormData: "GetFormData",
    GetAnnotations: "GetAnnotations",
    AddAnnotation: "AddAnnotation",
    UpdateAnnotation: "UpdateAnnotation",
    DeleteAnnotation: "DeleteAnnotation",
    GetVersionInfo: "GetVersionInfo",
    QueryFilename: "Filename",
    SaveAnnotations: "SaveAnnotations",
    DistinctValues: "DistinctValues",
    Metadata: "Metadata",
    GetEvents: "GetEvents",
    RunScripts: "Run",
};

/**
 * PMA.core scopes for the GetSlides API method
 * @readonly
 * @enum {string}
 */
export const GetSlidesScope = {
    Normal: 0,
    OneLevel: 1,
    Recursive: 2
};

/**
 * Available components to render
 * @readonly
 * @enum {string}
 * @memberof PMA.UI.Components
 */
export const GalleryRenderOptions = {
    /** Render both thumbnail and barcode*/
    All: "all",
    /** Render thumbnail only */
    Thumbnail: "thumbnail",
    /** Render barcode only */
    Barcode: "barcode",
};

/** 
 * The mime type used for drag n drop data transfer
 * @typedef {string} PMA.UI.Components~DragDropMimeType
 */
export const DragDropMimeType = "application/x-pma-node";

/**
 * An object expected for drag and drop features 
 * @typedef {Object} PMA.UI.Components~dragDropObject
 * @property {string} serverUrl - The serverUrl
 * @property {string} path - The path to a slide or directory
 * @property {boolean} isFolder - Whether this is a path or a directory
 */

// static variables

//export let _sessionList = {};
export const _sessionList = {
    _list: {},
    set(key, value) { this._list[key] = value; },
    get() { return this._list; },
    clear() { this._list = {}; },
};
var alertedForIncompatibility = false;

function combinePath(parts) {
    var rx = /^\/+|\/+$/g; // trim left and right trailing slash
    var rxLeftOnly = /^\/+/g; // trim left trailing slash
    for (var i = 0, max = parts.length; i < max; i++) {
        if (i === max - 1) {
            parts[i] = parts[i].replace(rxLeftOnly, "");
        } else {
            parts[i] = parts[i].replace(rx, "");
        }
    }

    return parts.join("/");
}

export const parseJson = function(response) {
    if (response === null || response === undefined || response === "") {
        return null;
    }

    var obj = JSON.parse(response);

    if (obj.hasOwnProperty("d")) {
        return obj.d;
    } else {
        return obj;
    }
};

/**
 * Tries to parse the data passed from a drag and drop operation
 * @param {Object} dataTransfer - The browser dataTransfer object
 * @returns {PMA.UI.Components~dragDropObject} - The parsed drag and drop object or null
 */
export const parseDragData = function(dataTransfer) {
    try {
        var d1 = dataTransfer.getData(PMA.UI.Components.DragDropMimeType);
        return JSON.parse(d1);
    } catch (e) {}

    //Try the text for IE compatibility
    try {
        var d2 = dataTransfer.getData("text");
        return JSON.parse(d2);
    } catch (e) {}

    return null;
};

// encodes an object so that it can be passed as an ajax data object
export const formify = function(data) {
    if (data && typeof data === "object") {
        var sData = "";

        var i = 0;
        for (var property in data) {
            if (data.hasOwnProperty(property)) {
                if (i > 0) {
                    sData += "&";
                }

                sData += encodeURIComponent(property) + "=" + encodeURIComponent(data[property]);
                i++;
            }
        }

        return sData;
    }

    return data;
};

export const ajax = function(url, method, contentType, data, success, failure) {
    method = method.toUpperCase();
    var sData = null;

    if (contentType && contentType.toLowerCase && contentType.toLowerCase() === "application/json") {
        sData = JSON.stringify(data);
    } else {
        sData = formify(data);
    }

    if (sData && method == "GET") {
        url = url + "?" + sData;
        sData = null;
    }

    var http = new XMLHttpRequest();
    http.open(method, url, true);

    if (method == "POST" && !contentType) {
        http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    } else if (contentType) {
        http.setRequestHeader('Content-Type', contentType);
    }

    http.onreadystatechange = function() {
        if (http.readyState === 4) {
            if (http.status === 200) {
                if (typeof success === "function") {
                    success(http);
                }
            } else if (typeof failure === "function") {
                failure(http);
            }
        }
    };

    if (sData) {
        http.send(sData);
    } else {
        http.send();
    }
};

/**
 * Invokes a PMA.core API method. This method will properly encode all provided parameters.
 * @param {Object} options
 * @param {string} options.serverUrl - The URL of the PMA.core instance
 * @param {string} options.method - The API method to call
 * @param {string} options.httpMethod - The HTTP verb
 * @param {object} options.data - The parameters to pass to the API method
 * @param {string} [options.apiPath="api"] - The API path to append to the server URL
 * @param {string} [options.contentType=""]
 * @param {boolean} [options.webapi=false] - Whether the api call is a webapi call
 * @param {function} [options.success] - Function to call upon successful method invocation
 * @param {function} [options.failure] - Function to call upon unsuccessful method invocation
 */
export const callApiMethod = function(options) {
    var httpMethod = "GET";
    if (options.httpMethod) {
        httpMethod = options.httpMethod;
    }

    if (!options.apiPath) {
        options.apiPath = "api";
    }

    let infix = options.webapi === true ? "/" : "/json/";

    ajax(combinePath([options.serverUrl, options.apiPath + infix, options.method]), httpMethod, options.contentType, options.data, options.success, options.failure);
};

export const callApiMethodPromise = function(options) {
    return new Promise(function(resolve, reject) {
        const localOptions = {
            ...options,
            success: resolve,
            failure: reject
        };

        callApiMethod(localOptions)
    });
}

/**
 * Authenticates against a PMA.core server
 * @param  {string} serverUrl
 * @param  {string} username
 * @param  {string} password
 * @param  {string} caller
 * @param  {function} success
 * @param  {function} failure
 */
export const login = function(serverUrl, username, password, caller, success, failure) {
    if (typeof caller !== "string") {
        throw "Caller parameter not supplied";
    }

    function authFailure(http) {
        if (typeof failure === "function") {
            if (!http.responseText || http.responseText.length === 0 || http.responseType === "") {
                failure({ Message: Resources.translate("Authentication failed") });
            } else {
                try {
                    const response = parseJson(http.responseText);
                    failure(response);
                } catch {
                    failure({ Message: Resources.translate("Authentication failed") });
                }
            }
        }
    }

    callApiMethodPromise({
            serverUrl: serverUrl,
            method: ApiMethods.GetVersionInfo
        })
        .then((versionHttp) => {
            let usePost = loginSupportsPost(JSON.parse(versionHttp.responseText));

            callApiMethod({
                serverUrl: serverUrl,
                method: ApiMethods.Authenticate,
                httpMethod: usePost ? "POST" : "GET",
                contentType: usePost ? "application/json" : null,
                data: { username: username, password: password, caller: caller },
                success: function(http) {
                    var response = parseJson(http.responseText);
                    if (response && response.Success === true) {
                        // store session ID for this server in global cache
                        _sessionList.set(serverUrl, response);

                        if (typeof success === "function") {
                            success(response.SessionId);
                        }
                    } else {
                        authFailure(http);
                    }
                },
                failure: authFailure
            });
        }, authFailure);
};
/**
 * Returns a URL that points to the thumbnail of a slide image
 * @param  {string} serverUrl
 * @param  {string} sessionId
 * @param  {string} pathOrUid
 * @param  {Number} [orientation=0]
 * @param  {Number} [width=0]
 * @param  {Number} [height=0]
 */
export const getThumbnailUrl = function(serverUrl, sessionId, pathOrUid, orientation, width, height) {
    return combinePath([serverUrl, "thumbnail"]) +
        "?sessionID=" + encodeURIComponent(sessionId) +
        "&pathOrUid=" + encodeURIComponent(pathOrUid) +
        "&orientation=" + (orientation ? orientation : 0) +
        "&w=" + (width ? width : 0) +
        "&h=" + (height ? height : 0);
};

/**
 * snapshot parameters
 * @typedef {Object} Components~snapshotParameters
 * @property {number} x - The x coordinate of the top left point
 * @property {number} y - The y coordinate of the top left point
 * @property {number} width - The width of the viewport
 * @property {number} height - The height of the viewport
 * @property {number} rotation - The rotation of the viewport (in degrees)
 * @property {boolean} flipHorizontally - Wheter the snapshot is flipped horizontally
 * @property {boolean} flipVertically - Wheter the snapshot is flipped vertically
 * @property {Array} [channels] - The selected channels
 * @property {number} [layer=0] - The selected layer
 * @property {number} [timeframe=0] - The selected timeframe
 */

/**
 * Returns a URL that points to the snapshot of a slide image
 * @param  {string} serverUrl
 * @param  {string} sessionId
 * @param  {string} pathOrUid
 * @param {Components~snapshotParameters} snapshotParameters - The parameters required for the snapshot
 * @param {Number} thumbWidth - The final snapshot width (this may be smaller due to aspect ratio)
 * @param {Number} thumbHeight - The final snapshot height (this may be smaller due to aspect ratio)
 * @param {string} [format=jpg] - The snapshot image format
 */
export const getSnapshotUrl = function(serverUrl, sessionId, pathOrUid, snapshotParameters, thumbWidth, thumbHeight, format) {
    if (!format) {
        format = "jpg";
    }

    thumbWidth = thumbWidth ? thumbWidth : 150;
    thumbHeight = thumbHeight ? thumbHeight : 150;

    var channelsString = "0";
    if (snapshotParameters.channels) {
        channelsString = snapshotParameters.channels.join(",");
    }
    var selectedTimeFrame = snapshotParameters.timeframe ? snapshotParameters.timeframe : 0;
    var selectedLayer = snapshotParameters.layer ? snapshotParameters.layer : 0;

    var scale = Math.max(thumbWidth / snapshotParameters.width, thumbHeight / snapshotParameters.height);
    if (snapshotParameters.width * scale < 1 || snapshotParameters.height < 1) {
        scale = 1 / Math.max(snapshotParameters.width, snapshotParameters.height);
    }

    var rotation = snapshotParameters.rotation ? snapshotParameters.rotation : 0;

    return combinePath([serverUrl, "region"]) +
        "?sessionID=" + encodeURIComponent(sessionId) +
        "&pathOrUid=" + encodeURIComponent(pathOrUid) +
        "&format=" + encodeURIComponent(format) +
        "&timeframe=" + selectedTimeFrame +
        "&layer=" + selectedLayer +
        "&channels=" + channelsString +
        "&x=" + Math.floor(snapshotParameters.x) +
        "&y=" + Math.floor(snapshotParameters.y) +
        "&width=" + Math.floor(snapshotParameters.width) +
        "&height=" + Math.floor(snapshotParameters.height) +
        "&scale=" + scale +
        "&rotation=" + rotation +
        "&flipHorizontal=" + snapshotParameters.flipHorizontally +
        "&flipVertical=" + snapshotParameters.flipVertically;
};

/**
 * Returns a URL that points to the barcode of a slide image. This method doesn't guarantee that a barcode image actually exists.
 * @param  {string} serverUrl
 * @param  {string} sessionId
 * @param  {string} pathOrUid
 * @param  {Number} [rotation=0]
 */
export const getBarcodeUrl = function(serverUrl, sessionId, pathOrUid, rotation) {
    return combinePath([serverUrl, "barcode"]) +
        "?sessionID=" + encodeURIComponent(sessionId) +
        "&pathOrUid=" + encodeURIComponent(pathOrUid) +
        "&rotation=" + (rotation ? rotation : 0);
};