diff --git a/src/core/js/query_form.js b/src/core/js/query_form.js new file mode 100644 index 0000000000000000000000000000000000000000..6775fee677660b32387dcd82346753eb98c94da0 --- /dev/null +++ b/src/core/js/query_form.js @@ -0,0 +1,270 @@ +/** + * Extend the functionality of the pure html query panel. + * + * Deprecated. This is to be replaced by the query form of caosdb-webui-core-components. + * + * @deprecated + * @module queryForm + * @global + */ +var queryForm = (function () { + const init = function (form) { + queryForm.restoreLastQuery(form, () => window.sessionStorage.lastQuery); + queryForm.bindOnClick(form, (set) => { + window.sessionStorage.lastQuery = set; + }); + const BUILD_FREE_SEARCH_ROLE_NAME_FACET_OPTIONS = ""; + queryForm.initFreeSearch( + form, + `${BUILD_FREE_SEARCH_ROLE_NAME_FACET_OPTIONS}` + ); + }; + + const logger = log.getLogger("queryForm"); + var role_name_facet_select = undefined; + + const _isCql = function (query) { + query = query.toUpperCase().trim(); + return ( + query.startsWith("FIND") || + query.startsWith("COUNT") || + query.startsWith("SELECT") + ); + }; + + /** + * Initialize the free search to generate search queries which search only + * within one of several options of roles or entity names. + * + * E.g. when `options` is "Person, Experiment, Sample" the user can select + * one of these options before submitting the query. When the user types in + * something that doesn't looks like a CQL-query, a query is generated + * instead which goes like: FIND Persion WHICH HAS A PROPERTY LIKE + * "*something*". + * + * Note: This feature is disabled by default. Enable it by specifying the + * build variable BUILD_FREE_SEARCH_ROLE_NAME_FACET_OPTIONS as a + * comma-separated list of **properly quoted** expressions, e.g. + * "FILE 'numpy array', 'Plant Experiemnt', 'quotes_not_necessary'". + * Otherwise, the server will throw a syntax error. + * + * @function initFreeSearch + * @param {HTMLElement} form - the form which will be initialized for the + * free search. + * @param {string} options - comma-separated list of options. + * @return {boolean} - true if the initialization was successful, false + * otherwise. + */ + const initFreeSearch = function (form, options) { + const textArea = $(form).find(".caosdb-f-query-textarea"); + logger.trace("initFreeSearch", form, textArea, options); + if (textArea.length > 0 && options && options != "") { + const lastQuery = + window.localStorage["freeTextQuery:" + textArea[0].value]; + if (lastQuery) { + textArea[0].value = lastQuery; + } + + const selected = window.localStorage["role_name_facet_option"]; + const select = $(`<select class="btn btn-secondary"/>`); + for (let option of options.split(",")) { + select.append( + `<option ${ + selected === option.trim() ? "selected" : "" + }>${option}</option>` + ); + } + $(form).find(".input-group").prepend(select); + role_name_facet_select = select[0]; + + const switchFreeSearch = (text_area) => { + if (_isCql(text_area.value)) { + select.hide(); + $(text_area).css({ + "border-top-left-radius": "0.375rem", + "border-bottom-left-radius": "0.375rem", + }); + } else { + select.show(); + $(text_area).css({}); + } + }; + + switchFreeSearch(textArea[0]); + + textArea.on("keydown", (e) => { + switchFreeSearch(e.target); + }); + return true; + } + role_name_facet_select = undefined; + return false; + }; + + /** + * @function restoreLastQuery + */ + const restoreLastQuery = function (form, getter) { + if (form == null) { + throw new Error("form was null"); + } + if (getter()) { + form.query.value = getter(); + } + }; + + /** + * @function redirect + * @param {string} query - the query string. + * @param {string} paging - the paging string, e.g. 0L10. + */ + const redirect = function (query, paging) { + var pagingparam = ""; + if (paging && paging.length > 0) { + pagingparam = "P=" + paging + "&"; + } + location.href = + connection.getBasePath() + "Entity/?" + pagingparam + "query=" + encodeURIComponent(query); + }; + + /** + * Read out the selector for the role/name facet of the query. Return + * "RECORD" if the selector is disabled. + * @function getRoleNameFacet + */ + const getRoleNameFacet = function () { + if (role_name_facet_select) { + const result = role_name_facet_select.value; + window.localStorage["role_name_facet_option"] = result; + return result; + } + return "RECORD"; + }; + + const _splitSearchTermsPattern = + /"(?<dq>[^"]*)" |'(?<sq>[^']*)' |(?<nq>[^ ]+)/g; + + /** + * Split a query string into single terms. + * + * Terms are separated by white spaces. Terms which contain white spaces + * which are to be preserved must be enclosed in " or ' quotes. The + * enclosing quotation marks are being stripped. Currently no support for + * escape sequences for quotation marks. + * + * @function splitSearchTerms + * @param {string} query - complete query string. + * @return {string[]} array of the search terms. + */ + const splitSearchTerms = function (query) { + // add empty space at the end, so every matching group ends with it -> easier regex. Also, undefined is filtered out + return Array.from( + (query + " ").matchAll(_splitSearchTermsPattern), + (m) => m[1] || m[2] || m[3] + ).filter((word) => word); + }; + + /** + * Is the query a SELECT field,... FROM entity query? + * + * @function isSelectQuery + * @param {HTMLElement} query, the query to be tested. + * @return {Boolean} + */ + const isSelectQuery = function (query) { + return query.toUpperCase().startsWith("SELECT"); + }; + + /** + * @function bindOnClick + */ + const bindOnClick = function (form, setter) { + if (setter == null || typeof setter !== "function" || setter.length !== 1) { + throw new Error("setter must be a function with one param"); + } + + /* + Here a submit handler is created that is attached to both the form submit handler + and the click handler of the button. + See https://developer.mozilla.org/en-US/docs/Web/Events/submit why this is necessary. + */ + var submithandler = function () { + // store current query + var queryField = form.query; + var value = queryField.value; + if (typeof value == "undefined" || value.length == 0) { + return; + } + if (!_isCql(value)) { + // split words in query field at space and create query fragments + var words = splitSearchTerms(queryField.value).map( + (word) => `A PROPERTY LIKE '*${word.replaceAll("'", `\\'`)}*'` + ); + if (!words.length) { + return false; + } + const e = getRoleNameFacet(); + const query_string = `FIND ${e} WHICH HAS ` + words.join(" AND "); + queryField.value = query_string; + + // store original value of the text field + window.localStorage["freeTextQuery:" + query_string] = value; + } + setter(queryField.value); + + var paging = ""; + if (form.P && !isSelectQuery(queryField.value)) { + paging = form.P.value; + } + + queryForm.redirect(queryField.value.trim(), paging); + + var btn = $(form).find(".caosdb-search-btn"); + btn.html( + `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>` + ); + btn.prop("disabled", true); + + var textField = $(form).find(".caosdb-f-query-textarea"); + textField.blur(); + textField.prop("disabled", true); + }; + + // handler for the form + form.onsubmit = function (e) { + e.preventDefault(); + submithandler(); + return false; + }; + + // same handler for the button + form.getElementsByClassName("caosdb-search-btn")[0].onclick = function () { + submithandler(); + }; + }; + + /** + * @function getRoleNameFacetSelect + */ + + return { + init: init, + initFreeSearch: initFreeSearch, + isSelectQuery: isSelectQuery, + restoreLastQuery: restoreLastQuery, + redirect: redirect, + bindOnClick: bindOnClick, + splitSearchTerms: splitSearchTerms, + getRoleNameFacet: getRoleNameFacet, + getRoleNameFacetSelect: () => role_name_facet_select, + }; +})(); + +$(document).ready(function () { + if (`${BUILD_MODULE_LEGACY_QUERY_FORM}` == "ENABLED") { + var form = document.getElementById("caosdb-query-form"); + if (form != null) { + queryForm.init(form); + } + } +});