/**
* 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",
Upload: "Upload"
};
/**
* PMA.core scopes for the GetSlides API method
* @readonly
* @enum {string}
* @ignore
*/
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
* @ignore
*/
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
* @param {object} data
* @returns a www-encoded serialized string of the data object
* @ignore
*/
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;
};
/**
* @param {string} url
* @param {string} method
* @param {string} contentType
* @param {object} data
* @param {function} success
* @param {function} failure
* @ignore
*/
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
* @ignore
*/
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/";
if (options.webapi && httpMethod == "POST" && options.data) {
options.method += "?sessionId=" + options.data.sessionID;
}
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
* @ignore
*/
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]
* @ignore
*/
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
* @ignore
*/
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]
* @ignore
*/
export const getBarcodeUrl = function (serverUrl, sessionId, pathOrUid, rotation) {
return combinePath([serverUrl, "barcode"]) +
"?sessionID=" + encodeURIComponent(sessionId) +
"&pathOrUid=" + encodeURIComponent(pathOrUid) +
"&rotation=" + (rotation ? rotation : 0);
};