diff --git a/CHANGELOG.md b/CHANGELOG.md index d5c225054c8b172c20d38ebf8a1c1561ecb55ded..bba61d8e1fac23c4aaeecac2ca0bd16d4e40827e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added (for new features, dependecies etc.) +* new `navbar.add_tool` method which adds a button to submenu of the navbar. * new CSS classes for the styling of the default image and video preview of the `ext_bottom_line` module: `caosdb-v-bottom-line-image-preview` and `caosdb-v-bottom-line-video-preview`. @@ -17,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `edit_mode` prevents the user from editing old versions of an entity. ### Changed (for changes in existing functionality) +- The `navbar.add_button` signatur changed to `add_button(button, options)`. + See the doc string of that method for more information. - added a layout argument to the create_plot function of ext_bottom_line ### Deprecated (for soon-to-be removed features) diff --git a/makefile b/makefile index d9091f32004cfe9c25e28c9e75a3cc18471ac73b..aa0b045badaf4d923a0f425b8a7358373e7a3e51 100644 --- a/makefile +++ b/makefile @@ -155,7 +155,7 @@ cp-ext: cp-ext-test: for f in $(wildcard $(TEST_EXT_DIR)/js/*) ; do \ echo "y" | cp -i -r "$$f" $(PUBLIC_DIR)/js/ ; \ - sed -i "/JS_EXTENSIONS/a \<xsl:element name=\"script\"><xsl:attribute name=\"src\"><xsl:value-of select=\"concat\(\$$basepath, 'webinterface/${BUILD_NUMBER}$${f#$(SRC_EXT_DIR)}'\)\" /></xsl:attribute></xsl:element>" $(PUBLIC_DIR)/xsl/main.xsl ; \ + sed -i "/JS_EXTENSIONS/a \<xsl:element name=\"script\"><xsl:attribute name=\"src\"><xsl:value-of select=\"concat\(\$$basepath, 'webinterface/${BUILD_NUMBER}$${f#$(TEST_EXT_DIR)}'\)\" /></xsl:attribute></xsl:element>" $(PUBLIC_DIR)/xsl/main.xsl ; \ done mkdir -p $(PUBLIC_DIR)/html for f in $(wildcard $(TEST_EXT_DIR)/html/*) ; do \ diff --git a/src/core/css/webcaosdb.css b/src/core/css/webcaosdb.css index 21a73e3acd96337b6dd6adec7bf1a024cac91e6c..d24e33fb36b96e29fb2f30039e74a06082bdb1ac 100644 --- a/src/core/css/webcaosdb.css +++ b/src/core/css/webcaosdb.css @@ -27,6 +27,26 @@ body { flex-direction: column; } +.caosdb-v-navbar-toolbox li a:hover, +.caosdb-v-navbar-toolbox li input:hover, +.caosdb-v-navbar-toolbox li button:hover { + background-color: #f5f5f5; +} + +.caosdb-v-navbar-toolbox li a, +.caosdb-v-navbar-toolbox li input, +.caosdb-v-navbar-toolbox li button { + display: block; + padding: 3px 20px; + border: none; + width: 100%; + text-align: left; + margin-top: 0px; + margin-bottom: 0px; + text-decoration: none; + background-color: transparent; +} + .caosdb-v-bottom-line-image-preview > img { max-width: 400px; max-height: 280px; diff --git a/src/core/js/webcaosdb.js b/src/core/js/webcaosdb.js index b496c3b7a176828cf99a3efd017d591dde2e937b..df93703d83fafde15908efb40c3ae578824980d9 100644 --- a/src/core/js/webcaosdb.js +++ b/src/core/js/webcaosdb.js @@ -26,7 +26,7 @@ window.addEventListener('error', (e) => globalError(e.error)); -var globalError = function(error) { +var globalError = function (error) { var stack = error.stack; var message = "Error! Please help to make CaosDB better! Copy this message and send it via email to info@indiscale.com.\n\n"; message += error.toString(); @@ -40,7 +40,7 @@ var globalError = function(error) { throw error; } -var globalClassNames = new function() { +var globalClassNames = new function () { this.NotificationArea = "caosdb-preview-notification-area"; this.WaitingNotification = "caosdb-preview-waiting-notification"; this.ErrorNotification = "caosdb-preview-error-notification"; @@ -49,62 +49,192 @@ var globalClassNames = new function() { /** * navbar module contains convenience functions for the navbar. */ -this.navbar = new function() { +this.navbar = new function () { var logger = log.getLogger("navbar"); this.logger = logger; /** - * Add a button to the navbar (left navbar, append right), wrapped in a LI - * element. + * Add a button to the navbar. * * If the param `button` is a string then a suitable BUTTON element will be - * created. If the button is otherwise an HTMLElement it will just be - * wrapped and appended to the navbar. + * created. If the button is otherwise an HTMLElement or an array of + * HTMLElements it will just be wrapped, styled and appended to the navbar. * - * The optional `click_callback` function is bound to the click event. + * The optional `options` object knows the following optional keys: + * @property {function} callback - a function which will be bound to the + * click event of the button. + * @property {string} title - the title attribute (a kind of tooltip) + * of the button. + * @property {HTMLElement} menu - where to append the button. Defaults + * to the navbar itself. * - * @param {String|HTMLElement} button - add this button to navbar - * @param {function} [click_callback] - callback function for click + * If the `options["menu"] parameter is not set, the button is appended + * to the navbar directly and appears left of all previously appended + * children. + * + * + * @param {String|HTMLElement|HTMLElement[]} button - the button + * @param {object} [options] - further options * @return {HTMLElement} wrapper of the new button */ - this.add_button = function(button, click_callback) { + this.add_button = function (button, options) { + logger.trace("enter add_button", button, options); + + // assure that button parameter is {String|HTMLElement|HTMLElement[]} + if (typeof button === "undefined") { + throw new TypeError("button is expected to be a string, a single HTMLElement or HTMLElement[], was " + typeof button); + } else if (Array.isArray(button)) { + for (const element of button) { + if (!(element instanceof HTMLElement)) { + throw new TypeError("button is expected to be a string, a single HTMLElement or HTMLElement[], element was " + typeof element); + } + } + } else if (!(typeof button === "string" || button instanceof String || button instanceof HTMLElement)) { + throw new TypeError("button is expected to be a string, a single HTMLElement or HTMLElement[], was " + typeof button); + } + + // default: empty options + const _options = options || {}; var button_elem = button; if (typeof button === "string" || button instanceof String) { // create button element from string button_elem = $('<button>' + button + '</button>'); } - $(button_elem).toggleClass("navbar-btn", true); - $(button_elem).toggleClass("btn", true); - $(button_elem).toggleClass("btn-link", true); + + // set title + const title = _options["title"]; + if (typeof _options !== "undefined") { + $(button_elem).attr("title", title); + } + // bind click - if(typeof click_callback === "function") { + const click_callback = _options["callback"] + if (typeof click_callback === "function") { $(button_elem).click(click_callback); } // wrapp button let wrapper = $("<li></li>").append(button_elem); - $('#top-navbar').find("ul.caosdb-navbar").first().append(wrapper); + + // menu defaults to the navbar + const menu = _options["menu"] || this.get_navbar(); + + if ($(menu).is("ul.caosdb-navbar")) { + // special styling for buttons which are added directly to the + // navbar + $(button_elem) + .toggleClass("navbar-btn", true) + .toggleClass("btn", true) + .toggleClass("btn-link", true); + } + + logger.debug("add", wrapper, "to", menu); + $(menu).append(wrapper); + + logger.trace("leave add_button", wrapper[0]); return wrapper[0]; } - this.init = function() { + this.init = function () { $("nav.navbar-fixed-top") - .on("shown.bs.collapse", function(e) { + .on("shown.bs.collapse", function (e) { logger.trace("navbar expands", e); }) - .on("hidden.bs.collapse", function(e) { + .on("hidden.bs.collapse", function (e) { logger.trace("navbar shrinks", e); }); } + + /** + * Create initially empty tool box dropdown and append to navbar. + * + * The returned element is the drop-down menu which will eventually contain + * the tool buttons. That means, the buttons can be added directly to the + * returned element to appear in the drop-down menu. + * + * @return {HTMLElement} the dropdown-menu. + */ + this.init_toolbox = function (name) { + var button = $(`<a class="dropdown-toggle" + data-toggle="dropdown" href="#">${name} + <span class="caret"></span></a>`)[0]; + + var menu = $(`<ul + class="caosdb-v-navbar-toolbox + caosdb-f-navbar-toolbox + dropdown-menu" + data-toolbox-name="${name}"/>`)[0]; + + const wrapper = this.add_button([button, menu]); + $(wrapper).toggleClass("dropdown", true) + .children() + .toggleClass("btn", false) + .toggleClass("btn-link", false) + .toggleClass("navbar-btn", false); + + return menu; + } + + /** + * Add a tool to a toolbox. + * + * If the button is a string a new button element is created showing the + * string as its label. Otherwise the button should be an HTMLElement which + * is directly added to the toolbox. + * toolbox. + * + * The `callback` is a function which will be bound to the click event of + * the button. + * + * The passed or created button is wrapped in a LI element and added to the + * toolbox. The wrapper of the button is returned + * + * The optional `options` object knows the following optional keys: + * @property {function} callback - a function which will be bound to the + * click event of the button. + * @property {string} title - the title attribute (a kind of tooltip) + * of the button. + * + * @param {string|HTMLElement} button + * @param {string} [toolbox] - the name of the toolbox + * @param {object} [options] further options. + * @return {HTMLElement} the button wrapper. + */ + this.add_tool = function (button, toolbox, options) { + const toolbox_element = this.get_toolbox(toolbox); + + // put toolbox_element as menu into the options for `add_button` + const _options = $.extend({ + menu: toolbox_element + }, options); + + const wrapper = this.add_button(button, _options); + return wrapper; + } + + this.get_navbar = function () { + return $('#top-navbar') + .find("ul.caosdb-navbar")[0]; + } + + this.get_toolbox = function (name) { + var toolbox = $(this.get_navbar()).find(".caosdb-f-navbar-toolbox[data-toolbox-name='" + name + "']"); + if (toolbox.length > 0) { + return toolbox[0]; + } else { + return this.init_toolbox(name); + } + } + } -this.caosdb_utils = new function() { - this.assert_string = function(obj, name, optional=false) { +this.caosdb_utils = new function () { + this.assert_string = function (obj, name, optional = false) { if (typeof obj === "undefined" && optional) { return obj; } @@ -114,7 +244,7 @@ this.caosdb_utils = new function() { throw new TypeError(name + " is expected to be a string, was " + typeof obj); } - this.assert_type = function(obj, type, name, optional=false) { + this.assert_type = function (obj, type, name, optional = false) { if (typeof obj === "undefined" && optional) { return obj; } @@ -124,14 +254,14 @@ this.caosdb_utils = new function() { return obj; } - this.assert_html_element = function(obj, name) { + this.assert_html_element = function (obj, name) { if (typeof obj === "undefined" || !(obj instanceof HTMLElement)) { throw new TypeError(name + " is expected to be an HTMLElement, was " + typeof obj); } return obj; } - this.assert_array = function(obj, name, wrap_if_not_array) { + this.assert_array = function (obj, name, wrap_if_not_array) { if (Array.isArray(obj)) { return obj; } else if (wrap_if_not_array) { @@ -144,8 +274,8 @@ this.caosdb_utils = new function() { /** * connection module contains all ajax calls. */ -this.connection = new function() { - this._init = function() { +this.connection = new function () { + this._init = function () { /** * Send a get request. */ @@ -224,8 +354,8 @@ this.connection = new function() { console.log(error); } else if (error.status != null) { throw new Error( - "POST scripting returned with HTTP status " + error.status - + " - " + error.statusText); + "POST scripting returned with HTTP status " + error.status + + " - " + error.statusText); } else { throw error; } @@ -284,7 +414,7 @@ this.connection = new function() { /** * Return the base path of the server. */ - this.getBasePath = function() { + this.getBasePath = function () { var base = window.location.origin + "/"; if (typeof window.sessionStorage.caosdbBasePath !== "undefined") { base = window.sessionStorage.caosdbBasePath; @@ -305,7 +435,7 @@ this.connection = new function() { * @param {string[]|number[]} ids * @return {string} the URI. */ - this.getEntityUri = function(ids) { + this.getEntityUri = function (ids) { return this.getBasePath() + transaction.generateEntitiesUri(ids); } } @@ -316,14 +446,14 @@ this.connection = new function() { * transformation module contains all code for tranforming xml into html via * xslt. */ -this.transformation = new function() { +this.transformation = new function () { /** * remove all permission information from a server's response. * * @param {XMLDocument} xml * @return {XMLDocument} without <Permissions> tags. */ - this.removePermissions = function(xml) { + this.removePermissions = function (xml) { $(xml).find('Permissions').remove(); return xml } @@ -424,10 +554,10 @@ this.transformation = new function() { * be included. * @return {XMLDocument} a new style sheets with all template rules; */ - this.mergeXsltScripts = function(xslMain, xslIncludes) { + this.mergeXsltScripts = function (xslMain, xslIncludes) { let ret = getXSLScriptClone(xslMain); for (var i = 0; i < xslIncludes.length; i++) { - $(xslIncludes[i].firstElementChild).find('xsl\\:template').each(function(index) { + $(xslIncludes[i].firstElementChild).find('xsl\\:template').each(function (index) { $(ret.firstElementChild).append(this); }); } @@ -439,7 +569,7 @@ this.transformation = new function() { * transaction module contains all code for insertion, update and deletion of * entities. Currently, only updates are implemented. */ -this.transaction = new function() { +this.transaction = new function () { this.classNameUpdateForm = "caosdb-update-entity-form"; /** @@ -499,14 +629,14 @@ this.transaction = new function() { * @param {String[]} entityIds - An array of entity ids.. * @return {String} The uri. */ - this.generateEntitiesUri = function(entityIds) { + this.generateEntitiesUri = function (entityIds) { return "Entity/" + entityIds.join("&"); } /** * Submodule for update transactions. */ - this.update = new function() { + this.update = new function () { /** * Create a form for updating entities. It has only a textarea and a * submit button. @@ -520,7 +650,7 @@ this.transaction = new function() { * @param {String} entityXmlStr, the old entity * @param {function} putCallback, the function which sends a put request. */ - this.createUpdateForm = function(entityXmlStr, putCallback) { + this.createUpdateForm = function (entityXmlStr, putCallback) { // check the parameters if (putCallback == null) { throw new Error("putCallback function must not be null."); @@ -544,13 +674,13 @@ this.transaction = new function() { form.append(resetButton); // reset restores the original xmlStr - form.on('reset', function(e) { + form.on('reset', function (e) { textarea.find('textarea').val(entityXmlStr); return false; }); // submit calls the putCallback - form.submit(function(e) { + form.submit(function (e) { putCallback(e.target.updateXml.value); return false; }); @@ -568,7 +698,7 @@ this.transaction = new function() { * @param {HTMLElement} entity, the div which represent the entity. * @return {Object} a state machine. */ - this.updateSingleEntity = function(entity) { + this.updateSingleEntity = function (entity) { let updatePanel = transaction.update.createUpdateEntityPanel(transaction.update.createUpdateEntityHeading($(entity).find('.caosdb-entity-panel-heading')[0])); var app = new StateMachine({ transitions: [{ @@ -593,14 +723,14 @@ this.transaction = new function() { to: 'final' }, ], }); - app.errorHandler = function(fn) { + app.errorHandler = function (fn) { try { fn(); } catch (e) { setTimeout(() => app.resetApp(e), 1000); } } - app.onInit = function(e, entity) { + app.onInit = function (e, entity) { // remove entity $(entity).hide(); app.errorHandler(() => { @@ -617,7 +747,7 @@ this.transaction = new function() { }); // retrieve old xml, trigger state change when response is ready }; - app.onOpenForm = function(e, entityXmlStr) { + app.onOpenForm = function (e, entityXmlStr) { app.errorHandler(() => { // create and show Form let form = transaction.update.createUpdateForm(entityXmlStr, (xmlstr) => { @@ -626,14 +756,14 @@ this.transaction = new function() { updatePanel.append(form); }); }; - app.onResetApp = function(e, error) { + app.onResetApp = function (e, error) { $(entity).show(); $(updatePanel).remove(); if (error != null) { globalError(error); } }; - app.onShowUpdatedEntity = function(e, newentity) { + app.onShowUpdatedEntity = function (e, newentity) { // remove updatePanel updatePanel.remove(); // show new version of entity @@ -641,7 +771,7 @@ this.transaction = new function() { // remove old version $(entity).remove(); }; - app.onSubmitForm = function(e, xmlstr) { + app.onSubmitForm = function (e, xmlstr) { // remove form $(updatePanel).find('form').remove(); @@ -673,7 +803,7 @@ this.transaction = new function() { ); }); }; - app.onLeaveWaitPutEntity = function() { + app.onLeaveWaitPutEntity = function () { // remove waiting notifications removeAllWaitingNotifications(updatePanel); }; @@ -700,19 +830,19 @@ this.transaction = new function() { return xml2str(transformation.removePermissions(xml)); } - this.createWaitRetrieveNotification = function() { + this.createWaitRetrieveNotification = function () { return createWaitingNotification("Retrieving xml and loading form. Please wait."); } - this.createWaitUpdateNotification = function() { + this.createWaitUpdateNotification = function () { return createWaitingNotification("Sending update to the server. Please wait."); } - this.createErrorInUpdatedEntityNotification = function() { + this.createErrorInUpdatedEntityNotification = function () { return createErrorNotification("The update was not successful."); } - this.addErrorNotification = function(elem, err) { + this.addErrorNotification = function (elem, err) { $(elem).append(err); return elem; } @@ -724,7 +854,7 @@ this.transaction = new function() { * @param {HTMLElement} heading, the heading of the panel. * @return {HTMLElement} A div. */ - this.createUpdateEntityPanel = function(heading) { + this.createUpdateEntityPanel = function (heading) { let panel = $('<div class="panel panel-default" style="border-color: blue;"/>'); panel.append(heading); return panel[0]; @@ -739,7 +869,7 @@ this.transaction = new function() { * @param {HTMLElement} entityHeading, the heading of the entity. * @return {HTMLElement} the heading for the update panel. */ - this.createUpdateEntityHeading = function(entityHeading) { + this.createUpdateEntityHeading = function (entityHeading) { let heading = entityHeading.cloneNode(true); let update = $('<span><h3>Update</h3></span>')[0]; $(heading).children().slice(1).remove(); @@ -754,19 +884,19 @@ this.transaction = new function() { * @param {HTMLElement} entityPanel, the entity panel. * @return {HTMLElement} the heading. */ - this.getEntityHeading = function(entityPanel) { + this.getEntityHeading = function (entityPanel) { return $(entityPanel).find('.caosdb-entity-panel-heading')[0]; } - this.initUpdate = function(button) { + this.initUpdate = function (button) { transaction.update.updateSingleEntity( $(button).closest('.caosdb-entity-panel')[0] ); } - this.createCloseButton = function(close, callback) { + this.createCloseButton = function (close, callback) { let button = $('<button title="Cancel update" class="btn btn-link close" aria-label="Cancel update">×</button>'); - button.bind('click', function() { + button.bind('click', function () { $(this).closest(close).hide(); callback(); }); @@ -776,7 +906,7 @@ this.transaction = new function() { } -var paging = new function() { +var paging = new function () { this.defaultPageLen = 10; /** @@ -791,7 +921,7 @@ var paging = new function() { * the number of entities which are currently shown on this * page. */ - this.initPaging = function(href, totalEntities) { + this.initPaging = function (href, totalEntities) { if (totalEntities == null) { return false; @@ -841,7 +971,7 @@ var paging = new function() { * the page the new uri shall point to * @return a string uri which points to the page denotes by the parameter */ - this.getPageHref = function(uri_old, page) { + this.getPageHref = function (uri_old, page) { if (uri_old == null) { throw new Error("uri was null."); } @@ -869,7 +999,7 @@ var paging = new function() { * @param uri An arbitrary URI, usually the current URI of the window. * @return The first part of the query segment which begins with 'P=' */ - this.getPSegmentFromUri = function(uri) { + this.getPSegmentFromUri = function (uri) { if (uri == null) { throw new Error("uri was null."); } @@ -886,7 +1016,7 @@ var paging = new function() { * a paging string * @return a String */ - this.getPrevPage = function(P) { + this.getPrevPage = function (P) { if (P == null) { throw new Error("P was null"); } @@ -920,7 +1050,7 @@ var paging = new function() { * total numbers of entities. * @return a String */ - this.getNextPage = function(P, n) { + this.getNextPage = function (P, n) { // check n and P for null values and correct formatting if (n == null) { throw new Error("n was null"); @@ -950,8 +1080,8 @@ var paging = new function() { } }; -var queryForm = new function() { - this.init = function(form) { +var queryForm = new function () { + this.init = function (form) { this.restoreLastQuery(form, () => window.sessionStorage.lastQuery); this.bindOnClick(form, (set) => { window.sessionStorage.lastQuery = set; @@ -959,7 +1089,7 @@ var queryForm = new function() { }); }; - this.restoreLastQuery = function(form, getter) { + this.restoreLastQuery = function (form, getter) { if (form == null) { throw new Error("form was null"); } @@ -972,16 +1102,16 @@ var queryForm = new function() { * @value {string} query - the query string. * @param {string} paging - the paging string, e.g. 0L10. */ - this.redirect = function(query, paging) { + this.redirect = function (query, paging) { var pagingparam = "" - if(paging && paging.length > 0) { + if (paging && paging.length > 0) { pagingparam = "P=" + paging + "&"; } location.href = connection.getBasePath() + "Entity/?" + pagingparam + "query=" + query; } - this.bindOnClick = function(form, setter) { - if (setter == null || typeof(setter) !== 'function' || setter.length !== 1) { + this.bindOnClick = function (form, setter) { + if (setter == null || typeof (setter) !== 'function' || setter.length !== 1) { throw new Error("setter must be a function with one param"); } @@ -990,7 +1120,7 @@ var queryForm = new function() { 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() { + var submithandler = function () { // store current query var queryField = form.query; @@ -1004,7 +1134,7 @@ var queryForm = new function() { setter(queryField.value); var paging = ""; - if(form.P && !queryForm.isSelectQuery(queryField.value)) { + if (form.P && !queryForm.isSelectQuery(queryField.value)) { paging = form.P.value } @@ -1013,15 +1143,15 @@ var queryForm = new function() { // handler for the form - form.onsubmit = function(e) { - e.preventDefault(); - submithandler(); + form.onsubmit = function (e) { + e.preventDefault(); + submithandler(); return false; - }; + }; // same handler for the button - form.getElementsByClassName("caosdb-search-btn")[0].onclick = function() { - submithandler(); + form.getElementsByClassName("caosdb-search-btn")[0].onclick = function () { + submithandler(); }; }; @@ -1031,7 +1161,7 @@ var queryForm = new function() { * @param {HTMLElement} query, the query to be tested. * @return {Boolean} */ - this.isSelectQuery = function(query) { + this.isSelectQuery = function (query) { return query.toUpperCase().startsWith("SELECT"); } @@ -1042,7 +1172,7 @@ var queryForm = new function() { * @param {HTMLElement} form, the query form. * @return {HTMLElement} the form without the paging input. */ - this.removePagingField = function(form) { + this.removePagingField = function (form) { $(form.P).remove(); return form; } @@ -1052,9 +1182,9 @@ var queryForm = new function() { /** * Small module containing only a converter from markdown to html. */ -this.markdown = new function() { +this.markdown = new function () { this.dependencies = ["showdown", "caosdb_utils"]; - this.init = function() { + this.init = function () { this.converter = new showdown.Converter(); }; @@ -1065,7 +1195,7 @@ this.markdown = new function() { * @param {HTMLElement} textElement - an element with text which is to be * converted to html. */ - this.toHtml = function(textElement) { + this.toHtml = function (textElement) { let text = $(textElement).html(); let html = this.textToHtml(text); $(textElement).html(html); @@ -1074,28 +1204,28 @@ this.markdown = new function() { /** * Convert a markdown text to HTML */ - this.textToHtml = function(text) { + this.textToHtml = function (text) { caosdb_utils.assert_string(text, "param `text`"); return this.converter.makeHtml(text.trim()); }; - $(document).ready(function() { + $(document).ready(function () { caosdb_modules.register(markdown); }); } -var hintMessages = new function() { - this.init = function() { +var hintMessages = new function () { + this.init = function () { for (var entity of $('.caosdb-entity-panel')) { this.hintMessages(entity); } } - this.removeMessages = function(entity) { + this.removeMessages = function (entity) { $(entity).find(".alert").remove(); } - this.unhintMessages = function(entity) { + this.unhintMessages = function (entity) { $(entity).find(".caosdb-f-message-badge").remove(); $(entity).find(".alert").show(); } @@ -1115,7 +1245,7 @@ var hintMessages = new function() { * @param {HTMLElement} entity - the element where to replace the * messages. */ - this.hintMessages = function(entity) { + this.hintMessages = function (entity) { // TODO refactor such that the function can detect whether a message is // replaced yet instead of "unhintMessage"ing all of them first and do @@ -1131,7 +1261,7 @@ var hintMessages = new function() { for (let alrt in messageType) { // find all message divs - $(entity).find(".alert.alert-" + alrt).each(function(index) { + $(entity).find(".alert.alert-" + alrt).each(function (index) { var messageElem = $(this); // this way only one badge is shown, even if there are more @@ -1140,7 +1270,7 @@ var hintMessages = new function() { // TODO why is the message badge added to the .caosdb-v-property-row here? shouldn't .caosdb-messages suffice? messageElem.parent('.caosdb-messages, .caosdb-v-property-row').prepend('<button title="Click here to show the ' + messageType[alrt] + ' messages of the last transaction." class="btn caosdb-v-message-badge caosdb-f-message-badge badge alert-' + alrt + '">' + messageType[alrt] + '</button>'); - messageElem.parent().find(".caosdb-f-message-badge.alert-" + alrt).on("click", function(e) { + messageElem.parent().find(".caosdb-f-message-badge.alert-" + alrt).on("click", function (e) { // TODO use remove here instead of hide? $(this).hide(); @@ -1235,7 +1365,7 @@ function postXml(xml, basepath, querySegment, timeout) { dataType: 'xml', timeout: timeout, statusCode: { - 401: function() { + 401: function () { throw new Error("unauthorized"); }, }, @@ -1273,7 +1403,7 @@ async function load_config(filename) { } else if (error.status == 404) { return []; } else { - throw new Error("loading '"+ uri + "' failed.", error); + throw new Error("loading '" + uri + "' failed.", error); } } return data; @@ -1391,13 +1521,13 @@ function initOnDocumentReady() { } // show image 100% width - $(".entity-image-preview").click(function() { + $(".entity-image-preview").click(function () { $(this).css('width', '100%'); $(this).css('max-width', ""); $(this).css('max-height', ""); }); - if(typeof caosdb_modules.auto_init === "undefined") { + if (typeof caosdb_modules.auto_init === "undefined") { // the test index.html sets this to false, // unset -> no tests caosdb_modules.auto_init = true; @@ -1429,12 +1559,12 @@ class _CaosDBModules { * @throws TypeError - if module has no `init` method. */ register(module) { - if(!(typeof module.init === "function")) { + if (!(typeof module.init === "function")) { throw new TypeError("modules must define an init function"); } this.modules.push(module); - if(this.auto_init) { + if (this.auto_init) { this._init_module(module); } } diff --git a/test/core/js/modules/webcaosdb.js.js b/test/core/js/modules/webcaosdb.js.js index 1f8db45c07fef49fbd1e90be803d48fc8c96c7d2..239758161b8419a8a5e15799c67c909977f1fbf0 100644 --- a/test/core/js/modules/webcaosdb.js.js +++ b/test/core/js/modules/webcaosdb.js.js @@ -1874,6 +1874,25 @@ QUnit.test("annotation module", function(assert) { /* MODULE navbar */ QUnit.module("webcaosdb.js - navbar", { + before: () => { + $(document.body).append('<div id="top-navbar"><ul class="caosdb-navbar"/></div>'); + }, + beforeEach: () => { + $(".caosdb-f-navbar-toolbox").remove(); + }, + after: () => { + $("#top-navbar").remove(); + }, +}); + +QUnit.test("get_navbar", function(assert) { + assert.equal(navbar.get_navbar().className, "caosdb-navbar"); +}); + +QUnit.test("add_button wrong parameters", function(assert) { + assert.throws(()=>{navbar.add_button(undefined)}, /button is expected/, "undefined throws"); + assert.throws(()=>{navbar.add_button({"test": "an object"})}, "object throws"); + assert.throws(()=>{navbar.add_button(["array of strings"])}, "array of string throws"); }); QUnit.test("test button classes", function(assert) { @@ -1883,3 +1902,40 @@ QUnit.test("test button classes", function(assert) { assert.ok(result.hasClass("btn-link"), "has class btn-link"); assert.equal(result.text(), "TestButton", "text is correct"); }); + +QUnit.test("add_tool", function(assert) { + assert.equal($(".caosdb-f-navbar-toolbox").length, 0, "no toolbox"); + navbar.add_tool("TestButton", "TestMenu"); + + var toolbox = $("ul.caosdb-f-navbar-toolbox"); + assert.equal(toolbox.length, 1, "new toolbox"); + assert.equal(toolbox.find("button").length, 1, "new button"); + assert.equal(toolbox.find("button").text(), "TestButton", "Name correct") + + assert.notOk(toolbox.hasClass("btn")); + assert.notOk(toolbox.hasClass("btn-link")); + assert.notOk(toolbox.hasClass("navbar-btn")); + + assert.notOk(toolbox.siblings("a.dropdown-toggle").hasClass("btn")); + assert.notOk(toolbox.siblings("a.dropdown-toggle").hasClass("btn-link")); + assert.notOk(toolbox.siblings("a.dropdown-toggle").hasClass("navbar-btn")); +}); + +QUnit.test("toolbox example", function(assert) { + // this is a kind of integration test and it uses the toolbox_example + // module from toolbox_example.js. That example is also usefull for manual + // testing. + assert.equal($(".caosdb-f-navbar-toolbox").length, 0, "no toolbox"); + toolbox_example.init(); + assert.equal($(".caosdb-f-navbar-toolbox").length, 3, "three toolboxes"); + + assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Useful Links"]').length, 1, "one 'Useful Links' toolbox"); + assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Useful Links"] a[href="https://indiscale.com"]').length, 1, "one external link"); + + assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Server-side Scripts"]').length, 1, "one 'Server-side Scripts' toolbox"); + assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Server-side Scripts"] form input[type="submit"]').attr("value"), "Trigger Crawler", "one crawler trigger button"); + assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Server-side Scripts"] form').attr("title"), "Trigger the crawler.", "form has tooltip"); + + assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Tools"]').length, 1, "one 'Tools' toolbox"); + assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Tools"] button').length, 3, "three 'Tools' buttons"); +}); diff --git a/test/ext/js/toolbox_example.js b/test/ext/js/toolbox_example.js new file mode 100644 index 0000000000000000000000000000000000000000..9b9ef6548353c248376c3c3183835d03698366da --- /dev/null +++ b/test/ext/js/toolbox_example.js @@ -0,0 +1,49 @@ +var toolbox_example = function() { + + var init = function() { + navbar.add_button("Single Button", {callback: ()=>{alert("Single Button");}, title: "Click me!"}); + navbar.add_tool("Tool 1", "Tools", {callback: ()=>{alert("Tool 1");}, title: "Tooltip 1"}); + navbar.add_tool("Tool 2", "Tools", {callback: ()=>{alert("Tool 2");}, title: "Tooltip 2"}); + navbar.add_tool("Tool 3", "Tools", {callback: ()=>{alert("Tool 3");}, title: "Tooltip 3"}); + + navbar.add_tool($('<a href="https://indiscale.com">Link1</a>')[0], "Useful Links", {title: "Browse to indiscale.com"}); + + + const script = "crawler.py" + const args = { + "-p0": "positional argument 1", + "-p1": "positional argument 2", + "-Ooption1": "option value 1", + "-Ooption2": "option value 2", + }; + const button_name = "Trigger Crawler"; + const title = "Trigger the crawler."; + + const crawler_form = make_scripting_caller_form( + script, args, button_name); + + navbar.add_tool(crawler_form, "Server-side Scripts", {title: title}); + } + + var make_scripting_caller_form = function (script, args, button_name) { + const scripting_caller = $(` + <form method="POST" action="/scripting"> + <input type="hidden" name="call" value="${script}"/> + <input type="submit" + class="btn btn-link" value="${button_name}"/> + </form>`); + + // add arguements + for (const arg in args) { + scripting_caller.append(`<input type="hidden" name="${arg}" + value="${args[arg]}"/>`); + } + + return scripting_caller[0]; + } + + return { + init: init + }; + +}();