import { Resources } from '../../resources/resources';
import { Events } from './components';
import { checkBrowserCompatibility } from '../../view/helpers';
/**
* Represents a component that searches in form meta data for slides. It has a visual representation as a text field and also provides programmatic methods to search.
* @param {PMA.UI.Components.Context} context
* @param {object} [options=null] - Configuration options. If not provided, the component has no visual representation.
* @param {string|HTMLElement} options.element - The container in which an input field will be added to function as a search box. It can be either a valid CSS selector or an HTMLElement instance.
* @fires PMA.UI.Components.Events#SearchStarted
* @fires PMA.UI.Components.Events#SearchFinished
* @constructor
* @memberof PMA.UI.Components
* @tutorial 08-meta-search
*/
export class MetadataSearch {
constructor(context, options) {
if (!checkBrowserCompatibility()) {
return;
}
if (!options) {
options = {};
}
this.context = context;
this.servers = {};
this.currentFocus = null;
this.searchTimeout = 0;
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 {
this.element = null;
}
this.listeners = {};
this.listeners[Events.SearchStarted] = [];
this.listeners[Events.SearchFinished] = [];
this.options = options;
this.query = "";
this.position = 0;
if (this.element) {
initializeTextBox.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)) {
console.error(eventName + " is not a valid event");
}
this.listeners[eventName].push(callback);
}
// fires an event
fireEvent(eventName, eventArgs) {
if (!this.listeners.hasOwnProperty(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);
}
}
/*************** Parsing & searching *************/
/**
* Searches for slides in meta data entries
* @param {string} serverUrl - The url of the PMA.core search to search against
* @param {string} queryString - The query to search with
* @returns {Promise}
*/
search(serverUrl, queryString) {
var self = this;
var searchToken = (new Date()).getTime();
self.searchToken = searchToken;
return initServer.call(this, serverUrl).
then(function () {
return parse.call(self, queryString);
}).
then(function (query) {
if (query.expressions.length === 0) {
return getFormSuggestions.call(self, serverUrl, query);
}
return getSuggestions.call(self, serverUrl, query);
}).
then(function (processedQuery) {
if (!processedQuery || self.searchToken !== searchToken) {
return null;
}
var searchTerms = {
"sessionID": self.servers[serverUrl].sessionId,
"expressions": []
};
for (var i = 0; i < processedQuery.expressions.length; i++) {
var expr = processedQuery.expressions[i];
if (!isExpressionValid.call(self, expr)) {
continue;
}
var form = findForm.call(self, serverUrl, expr[0].value);
if (!form) {
continue;
}
var field = findField.call(self, form, expr[1].value);
if (!field) {
continue;
}
searchTerms.expressions.push({
"FormID": form.FormID,
"FieldID": field.FieldID,
"Operator": 0,
"Value": field.FormList ? listLabelToID(field.FormList.FormListValues, expr[3].value) : expr[3].value
});
}
if (self.searchToken === searchToken) {
if (searchTerms.expressions.length === 0) {
return { serverUrl: serverUrl, query: processedQuery, results: [] };
}
var searchTermsJson = JSON.stringify(searchTerms);
if (self.lastSearchTerms == searchTermsJson) {
return { serverUrl: serverUrl, query: processedQuery, results: self.lastSearchResults.slice() };
}
self.lastSearchTerms = "";
self.lastSearchResults = [];
return searchWithDelay.call(self, serverUrl, processedQuery, searchTerms, searchToken);
}
});
}
}
function initServer(serverUrl) {
var self = this;
if (self.inputBox) {
self.inputBox.readonly = true;
}
return new Promise(function (resolve, reject) {
self.context.getVersionInfo(serverUrl, function (version) {
if (!version || version.startsWith("1.")) {
reject();
return;
}
self.context.getSession(serverUrl, function (sessionId) {
self.servers[serverUrl] = self.servers[serverUrl] || {};
self.servers[serverUrl].sessionId = sessionId;
loadForms.call(self, serverUrl, function () {
if (self.inputBox) {
self.inputBox.readonly = false;
}
resolve(sessionId);
}, reject);
}, reject);
}, reject);
});
}
function loadForms(serverUrl, resolve, reject) {
if (this.servers[serverUrl].forms) {
resolve();
return;
}
var self = this;
this.context.getFormDefinitions(serverUrl, [], "", function (sessionId, definitions) {
self.servers[serverUrl].forms = definitions; // .filter(function (x) { return !x.ReadOnly; });
self.servers[serverUrl].forms.push({ FormID: -1, FormName: "Slide", FormFields: [{ FieldID: 0, Label: "Path" }, { FieldID: 1, Label: "Barcode" }/*, { FieldID: 0, Label: "Extension" }*/] });
resolve();
}, reject);
}
function searchWithDelay(serverUrl, processedQuery, searchTerms, searchToken) {
var self = this;
clearTimeout(self.searchTimeout);
var searchTermsJson = JSON.stringify(searchTerms);
var slideSearchExpressions = searchTerms.expressions.filter(function (item) {
return item.FormID === -1 && item.FieldID === 0;
});
var searchFilesOnly = slideSearchExpressions.length > 0 && slideSearchExpressions.length === searchTerms.expressions.length;
var metaSearchQuery = searchTerms;
return new Promise(function (resolve) {
self.searchTimeout = setTimeout(function () {
self.fireEvent(PMA.UI.Components.Events.SearchStarted, { serverUrl: serverUrl, queryString: processedQuery.getText() });
if (searchFilesOnly) {
searchPaths.call(self, serverUrl, slideSearchExpressions).
then(function (pathResults) {
var results = [];
if (pathResults instanceof Array) {
results = pathResults;
}
if (self.searchToken !== searchToken) {
return;
}
self.lastSearchTerms = searchTermsJson;
self.lastSearchResults = results;
self.fireEvent(PMA.UI.Components.Events.SearchFinished, results);
resolve({ serverUrl: serverUrl, query: processedQuery, results: results });
});
}
else {
fetch(serverUrl + "query/json/Metadata", {
method: "POST",
mode: "cors",
credentials: "same-origin",
headers: { "Content-Type": "application/json; charset=utf-8" },
body: JSON.stringify(metaSearchQuery)
}).
then(function (response) {
if (response.ok) {
return response.json();
}
else {
return [];
}
}).
then(function (metaResults) {
if (self.searchToken !== searchToken) {
return;
}
self.lastSearchTerms = searchTermsJson;
self.lastSearchResults = metaResults;
self.fireEvent(PMA.UI.Components.Events.SearchFinished, metaResults);
resolve({ serverUrl: serverUrl, query: processedQuery, results: metaResults });
});
}
}, 1000);
});
}
function searchPaths(serverUrl, expressions) {
var self = this;
if (!expressions || expressions.length === 0) {
return Promise.resolve(null);
}
return new Promise(function (resolve, reject) {
var q = expressions.map(function (item) { return item.Value; }).join("|");
self.context.queryFilename(serverUrl, "", q, function (sessionId, results) { resolve(results); }, reject);
});
}
function intersect(a, b) {
var t;
if (b.length > a.length) {
// indexOf to loop over shorter
t = b;
b = a;
a = t;
}
return a.filter(function (e) {
return b.indexOf(e) > -1;
}).
filter(function (e, i, c) { // extra step to remove duplicates
return c.indexOf(e) === i;
});
}
function parse(queryString) {
if (typeof (queryString) !== "string") {
throw "Query must be a string";
}
this.query = queryString;
this.position = 0;
var tokens = [];
while (canRead.call(this)) {
skipWhitespace.call(this);
var t = parseToken.call(this);
if (t) {
tokens.push(t);
}
}
var query = {
getText: function () {
if (!this.expressions || this.expressions.length === 0) {
return "";
}
var result = "";
for (var i = 0; i < this.expressions.length; i++) {
if (i > 0) {
result += ",";
}
var expr = this.expressions[i];
for (var t = 0; t < expr.length; t++) {
if (result.length > 0) {
result += " ";
}
if (expr[t].quoted) {
result += '"';
}
result += expr[t].value;
if (expr[t].quoted) {
result += '"';
}
}
}
return result;
},
getLastToken: function () {
if (!this.expressions || this.expressions.length === 0) {
return null;
}
var lastExpr = this.expressions[this.expressions.length - 1];
if (lastExpr.length === 0) {
return null;
}
return lastExpr[lastExpr.length - 1];
},
expressions: [],
suggestions: []
};
var i = 0;
while (i < tokens.length) {
if (i <= tokens.length - 4 &&
tokens[i + 0].type === tokenTypeEnum.literal &&
tokens[i + 1].type === tokenTypeEnum.literal &&
tokens[i + 2].operator &&
tokens[i + 3].type === tokenTypeEnum.literal) {
query.expressions.push([
tokens[i + 0],
tokens[i + 1],
tokens[i + 2],
tokens[i + 3]
]);
i += 4;
}
else if (i === tokens.length - 3 &&
tokens[i + 0].type === tokenTypeEnum.literal &&
tokens[i + 1].type === tokenTypeEnum.literal &&
tokens[i + 2].operator) {
query.expressions.push([
tokens[i + 0],
tokens[i + 1],
tokens[i + 2]
]);
i += 3;
}
else if (i === tokens.length - 2 &&
tokens[i + 0].type === tokenTypeEnum.literal &&
tokens[i + 1].type === tokenTypeEnum.literal) {
query.expressions.push([
tokens[i + 0],
tokens[i + 1]
]);
i += 2;
}
else if (tokens[i].type === tokenTypeEnum.literal) {
query.expressions.push([tokens[i]]);
i++;
}
else {
i++;
}
}
return new Promise(function (resolve, reject) { resolve(query); });
}
function canRead() {
return this.position < this.query.length;
}
function readChar() {
if (canRead.call(this)) {
return this.query[this.position++];
}
else {
return "";
}
}
function peekChar() {
if (canRead.call(this)) {
return this.query[this.position];
}
else {
return "";
}
}
function isExpressionValid(expression) {
return expression &&
expression.length === 4 &&
expression[0].type === tokenTypeEnum.literal &&
expression[1].type === tokenTypeEnum.literal &&
expression[2].operator &&
expression[3].type === tokenTypeEnum.literal;
}
function isTokenComplete(token) {
if (!token || (!token.quoted && isWhitespace.call(this, this.query[this.query.length - 1]))) {
return true;
}
else if (token.quoted) {
var trimmed = this.query.trimRight();
if (trimmed.length >= 2 && trimmed[trimmed.length - 1] === '"' && trimmed[trimmed.length - 1] === '\\') {
return false;
}
else if (trimmed[trimmed.length - 1] === '"') {
return true;
}
}
return false;
}
function getSuggestions(serverUrl, processedQuery) {
var lastExpression = processedQuery.expressions[processedQuery.expressions.length - 1];
if (lastExpression.length === 1) {
// either suggest forms or fields
if (isTokenComplete.call(this, lastExpression[0])) {
return getFieldSuggestions.call(this, serverUrl, processedQuery, lastExpression[0]);
}
else {
return getFormSuggestions.call(this, serverUrl, processedQuery);
}
}
else if (lastExpression.length === 2) {
// either suggest fields or operators
if (isTokenComplete.call(this, lastExpression[1])) {
return getOperatorSuggestions.call(this, processedQuery);
}
else {
return getFieldSuggestions.call(this, serverUrl, processedQuery, lastExpression[0]);
}
}
else if (lastExpression.length === 3) {
// either suggest operators or values
if (isTokenComplete.call(this, lastExpression[2])) {
return getValueSuggestions.call(this, serverUrl, processedQuery, lastExpression[0], lastExpression[1]);
}
else {
return getOperatorSuggestions.call(this, processedQuery);
}
}
else {
// either suggest forms or fields
if (isTokenComplete.call(this, lastExpression[3])) {
return getFormSuggestions.call(this, serverUrl, processedQuery);
}
else {
return getValueSuggestions.call(this, serverUrl, processedQuery, lastExpression[0], lastExpression[1]);
}
}
}
function findForm(serverUrl, name) {
if (!this.servers[serverUrl] || !this.servers[serverUrl].forms) {
return null;
}
return this.servers[serverUrl].forms.find(function (x) { return x.FormName.localeCompare(name, {}, { sensitivity: "base" }) === 0; });
}
function findField(form, name) {
if (!form) {
return null;
}
return form.FormFields.find(function (x) { return x.Label.localeCompare(name, {}, { sensitivity: "base" }) === 0; });
}
function getFormSuggestions(serverUrl, processedQuery) {
processedQuery.suggestions = this.servers[serverUrl].forms.map(function (x) { return x.FormName; });
return new Promise(function (resolve, reject) {
resolve(processedQuery);
});
}
function getFieldSuggestions(serverUrl, processedQuery, form) {
var formObj = findForm.call(this, serverUrl, form.value);
if (formObj) {
processedQuery.suggestions = formObj.FormFields.map(function (x) { return x.Label; });
}
return new Promise(function (resolve, reject) {
resolve(processedQuery);
});
}
function getOperatorSuggestions(processedQuery) {
var self = this;
return new Promise(function (resolve, reject) {
processedQuery.suggestions = getOperators.call(self);
resolve(processedQuery);
});
}
function listLabelToID(list, label) {
var result = [];
for (var j = 0; j < list.length; j++) {
if (list[j].Value == label) {
return list[j].ValueID;
}
}
return "";
}
function valuesToListLabels(list, values) {
var result = [];
var i, j;
for (i = 0; i < values.length; i++) {
for (j = 0; j < list.length; j++) {
if (list[j].ValueID == values[i]) {
result.push(list[j].Value);
break;
}
}
}
return result;
}
function getValueSuggestions(serverUrl, processedQuery, form, field) {
var self = this;
var formObj = findForm.call(this, serverUrl, form.value);
if (!formObj) {
return new Promise(function (resolve, reject) {
resolve(processedQuery);
});
}
var fieldObject = formObj.FormFields.find(function (x) { return x.Label.localeCompare(field.value, {}, { sensitivity: "base" }) === 0; });
if (!fieldObject) {
return new Promise(function (resolve, reject) {
resolve(processedQuery);
});
}
if (fieldObject.values) {
processedQuery.suggestions = fieldObject.values;
return new Promise(function (resolve, reject) {
resolve(processedQuery);
});
}
return fetch(serverUrl + "query/json/DistinctValues?formID=" + formObj.FormID + "&fieldID=" + fieldObject.FieldID + "&sessionID=" + self.servers[serverUrl].sessionId, {
method: "GET",
mode: "cors",
credentials: "same-origin",
headers: {
"Content-Type": "application/json; charset=utf-8"
}
}).
then(function (response) {
if (response.ok) {
return response.json();
}
else {
return [];
}
}).
then(function (values) {
if (fieldObject.FormList) {
fieldObject.values = valuesToListLabels(fieldObject.FormList.FormListValues, values);
}
else {
fieldObject.values = values.map(function (x) { return x + ""; });
}
processedQuery.suggestions = fieldObject.values;
return processedQuery;
});
}
function getOperators() {
return ["="];
////return ["=", ">", "<>", "<", ">=", "<=", "~="];
}
function isWhitespace(char) {
return [' ', ','].indexOf(char) !== -1;
}
function skipWhitespace() {
while (isWhitespace.call(this, peekChar.call(this))) {
readChar.call(this);
}
}
function isOperatorChar(char) {
return ['=', '>', '<', '~'].indexOf(char) !== -1;
}
function isTokenChar(char) {
return !isWhitespace.call(this, char) && !isOperatorChar.call(this, char);
}
function parseToken() {
if (!canRead.call(this)) {
return null;
}
var tokenChars = [];
var isQuoted = false;
if (peekChar.call(this) === '"') {
readChar.call(this);
isQuoted = true;
}
else if (isOperatorChar.call(this, peekChar.call(this))) {
return parseOperator.call(this);
}
while (canRead.call(this)) {
var c = readChar.call(this);
if (isQuoted) {
if (c === "\\" && peekChar.call(this) === '"') {
tokenChars.push(readChar.call(this));
continue;
}
else if (c === '"') {
break;
}
}
else if (!isTokenChar.call(this, c)) {
this.position--;
break;
}
tokenChars.push(c);
}
if (tokenChars.length === 0) {
return null;
}
return {
value: tokenChars.join(""),
type: tokenTypeEnum.literal,
operator: false,
quoted: isQuoted
};
}
function parseOperator() {
var tokenChars = [];
while (canRead.call(this)) {
if (isOperatorChar.call(this, peekChar.call(this))) {
tokenChars.push(readChar.call(this));
}
else {
break;
}
}
if (tokenChars.length === 0) {
return null;
}
var tokenString = tokenChars.join("");
var tokenType = tokenTypeEnum.literal;
switch (tokenString) {
case "=":
tokenType = tokenTypeEnum.equalOperator;
break;
case "<>":
tokenType = tokenTypeEnum.differentOperator;
break;
case ">":
tokenType = tokenTypeEnum.greaterThanOperator;
break;
case ">=":
tokenType = tokenTypeEnum.greaterThanOrEqualToOperator;
break;
case "<":
tokenType = tokenTypeEnum.lessThanOperator;
break;
case "<=":
tokenType = tokenTypeEnum.lessThanOrEqualToOperator;
break;
case "~=":
tokenType = tokenTypeEnum.similarOperator;
break;
default:
throw "Unknown operator";
}
return {
value: tokenString,
type: tokenType,
operator: true,
quoted: false
};
}
tokenTypeEnum = {
literal: "literal",
equalOperator: "equal to",
greaterThanOperator: "greater than",
greaterThanOrEqualToOperator: "greater than or equal to",
lessThanOperator: "less than",
lessThanOrEqualToOperator: "less than or equal to",
differentOperator: "different than",
similarOperator: "similar to"
};
/*************** End of parsing & searching *************/
/*************** Autocomplete UI *************/
function initializeTextBox() {
this.element.classList.add("pma-ui-meta-data-search");
this.inputBox = document.createElement("input", { type: "text", autocomplete: "off" });
this.element.appendChild(this.inputBox);
this.inputBox.addEventListener("input", inputInput.bind(this));
this.inputBox.addEventListener("keydown", inputKeyDown.bind(this));
this.inputBox.addEventListener("focus", inputInput.bind(this));
var self = this;
document.addEventListener("click", function (e) {
closeAllLists.call(self, e.target);
});
var context = new PMA.UI.Components.Context({ caller: "" });
context.getVersionInfo(
this.options.serverUrl,
function (version) {
if (version != null && version.startsWith("2.0")) {
//self.inputBox.disabled = false;
}
else {
console.warn("Metadata search only works with PMA.core version 2.x. Version reported: " + version);
}
},
function () {
console.warn("Cannot reach PMA.core server. Metadata search only works with PMA.core version 2.x.");
});
}
function inputInput(e) {
var self = this;
var val = self.inputBox.value;
closeAllLists.call(self);
self.search(self.options.serverUrl, val).
then(function (result) {
showSuggestions.call(self, result.query);
},
function () {
});
}
function showSuggestions(query) {
var a, b, i;
a = document.createElement("DIV");
a.setAttribute("class", "autocomplete-items");
var self = this;
this.element.appendChild(a);
var arr = query.suggestions;
var lastToken = query.getLastToken();
var val = "";
if (lastToken && !isTokenComplete.call(self, lastToken)) {
val = lastToken.value;
}
function itemSelected() {
var itemValue = this.getElementsByTagName("input")[0].value;
if (lastToken && !isTokenComplete.call(self, lastToken)) {
lastToken.value = itemValue;
lastToken.quoted = lastToken.value.indexOf(' ') !== -1;
self.inputBox.value = query.getText();
}
else {
self.inputBox.value = query.getText() + " " + (itemValue.indexOf(' ') !== -1 ? '"' + itemValue + '"' : itemValue);
}
self.search(self.options.serverUrl, self.inputBox.value);
closeAllLists.call(self);
}
var io;
for (i = 0; i < arr.length; i++) {
if (val.length === 0) {
b = document.createElement("DIV");
b.innerHTML = arr[i];
}
else {
io = arr[i].toUpperCase().indexOf(val.toUpperCase());
if (io === -1) {
continue;
}
b = document.createElement("DIV");
// make the matching letters bold
b.innerHTML = arr[i].substr(0, io);
b.innerHTML += "<strong>" + arr[i].substr(io, val.length) + "</strong>";
b.innerHTML += arr[i].substr(io + val.length);
}
// insert a input field that will hold the current array item's value
b.innerHTML += "<input type='hidden' value='" + arr[i] + "'>";
b.addEventListener("click", itemSelected);
a.appendChild(b);
}
}
function inputKeyDown(e) {
var x = this.element.querySelectorAll(".autocomplete-items div");
if (x.length === 0) {
return;
}
var curActive = this.element.querySelector(".autocomplete-active");
var index = -1;
if (curActive) {
curActive.classList.remove("autocomplete-active");
for (var count = 0; count < x.length; count++) {
if (x[count] === curActive) {
index = count;
break;
}
}
}
if (e.keyCode == 40) {
index++;
index = index >= x.length ? 0 : index;
x[index].classList.add("autocomplete-active");
}
else if (e.keyCode == 38) {
index--;
index = index < 0 ? x.length - 1 : index;
x[index].classList.add("autocomplete-active");
}
else if (e.keyCode == 13) {
e.preventDefault();
if (index !== -1) {
x[index].click();
}
closeAllLists.call(this);
}
else if (e.keyCode == 27) {
closeAllLists.call(this);
}
}
function closeAllLists(element) {
var x = document.getElementsByClassName("autocomplete-items");
for (var i = 0; i < x.length; i++) {
if (element != x[i] && element != this.inputBox) {
x[i].parentNode.removeChild(x[i]);
}
}
}