diff --git a/misc/unit_test_http_server.py b/misc/unit_test_http_server.py index 0667572361b35b47c65475b0af35955b650fa39f..a83d762ae69940d0eb43083ad9eff892e54f182b 100755 --- a/misc/unit_test_http_server.py +++ b/misc/unit_test_http_server.py @@ -30,6 +30,7 @@ import sys import os from http.server import SimpleHTTPRequestHandler, HTTPServer +counter = 0 class UnitTestsHandler(SimpleHTTPRequestHandler): """UnitTestsHandler @@ -88,10 +89,12 @@ class UnitTestsHandler(SimpleHTTPRequestHandler): Print the body of a request. """ + global counter + counter += 1 content_length = int(self.headers['Content-Length']) post_data = self.rfile.read(content_length).decode("utf-8") with open("qunit.log", "a") as logfile: - logfile.write("[LOG] " + post_data + "\n") + logfile.write("[LOG {}] ".format(counter) + post_data + "\n") return post_data diff --git a/src/core/js/ext_map.js b/src/core/js/ext_map.js index 44e50df23936debadbc1c03265d23ecc61d8bb55..5ef5c5dc2a2a8f336764f6f083defb26f444cad9 100644 --- a/src/core/js/ext_map.js +++ b/src/core/js/ext_map.js @@ -52,7 +52,7 @@ var caosdb_map = new function () { * the configuration of the graticule(s) and the data model for the * integrated query generator. * - * @typedef {Object} MapConfig + * @typedef {object} MapConfig * @property {string} version - the version of the map * @property {string} default_view - the view which is shown when the user * opens the map and has no stored view from prior visits to the map. @@ -73,7 +73,7 @@ var caosdb_map = new function () { * decimal format. The latitude should not exceed [-90°,90°] ranges and the * longitude should not exceed [-180°, 180°] ranges. * - * @typedef {Object} DataModelConfig + * @typedef {object} DataModelConfig * @property {string} [lat=latitude] - the name of the latitude property. * @property {string} [lng=longitude] - the name of the longitude property. */ @@ -272,6 +272,7 @@ var caosdb_map = new function () { * } * } * + * @typedef {object} GraticuleConfig * @property {string} type - either "simple" or "latlngGraticule". * @property {object} options - the options for the graticule * implementation. @@ -305,6 +306,7 @@ var caosdb_map = new function () { * } * } * + * @typedef {object} CRSConfig * @property {string} code - the standardized CRS Code. * @property {string} proj4def - the proj4 definition of the CRS. * @property {object} options - options according to the @@ -330,6 +332,140 @@ var caosdb_map = new function () { }, } + /** + * A function which returns entities which are to be displayed on the map. + * + * The parameters may be used to filter the entities. The caller should not + * expect anything from the entities. + * + * @async + * @callback {mapEntityGetter} + * @param {DataModelConfig} datamodel - datamodel of the entities to be returned. + * @param {number} north - northern bounding latitude + * @param {number} south - southern bounding latitude + * @param {number} west - western bounding longitude + * @param {number} east - eastern bounding longitude + * @returns {HTMLElement[]} array of entities in HTML representation. + */ + + /** + * A function which generates a condensed HTML representation of an entity + * which can be shown as a popup in a map. + * + * @callback {mapEntityPopupGenerator} + * @param {HTMLElement} entity - in HTML representation + * @param {DataModelConfig} datamodel + * @return {HTMLElement} a popup element. + */ + + /** + * @typedef {object} EntityLayerConfig + * @property {string} id + * @property {string} name + * @property {string} description + * @property {DivIcon_options} icon_options + * @property {number} zIndexOffset + * @property {mapEntityGetter} get_entities + * @property {mapEntityPopupGenerator} make_popup + */ + + /** + * Implements {@link mapEntityGetter}. + */ + this._get_current_page_entities = function ( + datamodel, north, south, west, east) { + const container = $(".caosdb-f-main-entities")[0]; + return caosdb_map.get_map_entities(container, datamodel); + } + + /** + * Implements {@link mapEntityGetter}. + */ + this._query_all_entities = async function ( + datamodel, north, south, west, east) { + const results = await caosdb_map.query(`FIND ENTITY WITH ${datamodel.lat} AND ${datamodel.lng}`); + const container = $('<div>').append(results)[0]; + return caosdb_map.get_map_entities(container, datamodel); + } + + /** + * Create a HTML representation of an entity which is suitable for being + * displayed in the map as a marker's popup. + * + * Implements {@link mapEntityPopupGenerator} + * @param {HTMLElement} entity - an entity in HTML representation. + * @param {DataModelConfig} datamodel - configuration of the properties + * used for the coordinates. + * @returns {HTMLElement} a popup element. + */ + this._make_map_popup = function (entity, datamodel) { + const lat = getProperty(entity, datamodel.lat); + const lng = getProperty(entity, datamodel.lng); + const role_label = $(entity).find( + ".label.caosdb-f-entity-role").first().clone(); + const parent_list = caosdb_map.make_parent_labels(entity); + const name = caosdb_map.make_entity_name_label(entity); + const dms_lat = L.NumberFormatter.toDMS(lat); + const dms_lng = L.NumberFormatter.toDMS(lng); + const loc = $(`<div class="small text-muted"> + Lat: ${dms_lat} Lng: ${dms_lng} + </div>`); + const ret = $('<div/>') + .append(role_label) + .append(parent_list) + .append(name) + .append(loc); + return ret[0]; + } + + + /** + * Default entities layers configuration with two layers: + * "current_page_entities" which shows the entities on the current page and + * "all_map_entities" which shows all entities in the database with + * coordinates. + * + * @type {EntityLayerConfig[]} + */ + this._default_entity_layer_config = [{ + "id": "current_page_entities", + "name": "Entities on the current page.", + "description": "Show all entities on the current page.", + "icon": { + html: '<span style="color: #00F; font-size: 20px" class="glyphicon glyphicon-map-marker"></span>', + iconAnchor: [10, 19], + className: "", + }, + "zIndexOffset": 1000, + "datamodel": { + "lat": "latitude", + "lng": "longitude", + "role": "ENTITY", + "entity": "", + }, + "get_entities": this._get_current_page_entities, + "make_popup": this._make_map_popup, + }, { + "id": "all_map_entities", + "name": "All entities", + "description": "Show all entities with coordinates.", + "icon": { + html: '<span style="color: #F00; font-size: 20px" class="glyphicon glyphicon-map-marker"></span>', + iconAnchor: [10, 19], + className: "", + }, + "zIndexOffset": 0, + "datamodel": { + "lat": "latitude", + "lng": "longitude", + "role": "ENTITY", + "entity": "", + }, + "get_entities": this._query_all_entities, + "make_popup": this._make_map_popup, + }, ]; + + /** * If no views are configured this dummy view servers as a placeholder. * @@ -606,7 +742,7 @@ var caosdb_map = new function () { var toggle_button = undefined; try { const config = await this.load_config(); - if(!config.views || config.views.length === 0) { + if (!config.views || config.views.length === 0) { logger.warn("no views in config"); return; } @@ -649,7 +785,7 @@ var caosdb_map = new function () { nextview + ".zoom"]; local_conf["center"] = sessionStorage["caosdb_map.view." + nextview + ".center"]; - if(local_conf["center"]) { + if (local_conf["center"]) { local_conf["center"] = JSON.parse(local_conf["center"]); } @@ -664,13 +800,14 @@ var caosdb_map = new function () { this._map = this.create_map_view(panel, view_config); - // initialize layer groups - this.init_layer_groups(this._map); - var current_page_entities_layer = - this.get_layer_group("current_page_entities"); - this.add_current_page_entities( - config.datamodel, - current_page_entities_layer); + // init entity layers + var layers = this.init_entity_layers(this._default_entity_layer_config); + var layerControl = L.control.layers(); + for (const layer of layers) { + layerControl.addOverlay(layer.layer_group, layer.chooser_html.outerHTML); + layer.layer_group.addTo(this._map); + } + layerControl.addTo(this._map); // initialize handlers this.add_select_handler(this._map); @@ -697,6 +834,79 @@ var caosdb_map = new function () { logger.trace("leave init"); } + + /** + * The _EntityLayer object is used to pass around map overlay layers + * between functions. It is not part of the public API. + * + * @typedef {object} _EntityLayer + * @property {string} id + * @property {HTMLElement} chooser_html + * @property {L.LayerGroup} layer_group + * @property {boolean} active + */ + + /** + * @param {EntityLayerConfig[]} configs + * @returns {_EntityLayer[]} + */ + this.init_entity_layers = function (configs) { + var ret = [] + for (const conf of configs) { + ret.push(this.init_entity_layer(conf)); + } + return ret; + } + + + /** + * Initialize an entity layer. + * + * @param {EntityLayerConfig} config + * @return {_EntityLayer} + */ + this.init_entity_layer = function (config) { + logger.trace("enter init_entity_layer", config); + + var layer_group = L.layerGroup(); + + // load all entities into layer group + var _load = async function (layer_group, config) { + var entities = await config.get_entities(config.datamodel); + var markers = caosdb_map.create_entitiy_markers( + entities, config.datamodel, config.make_popup, + config.zIndexOffset, config.icon); + + for (const marker of markers) { + layer_group.addLayer(marker); + } + }; + _load(layer_group, config); + + var ret = { + "id": config.id, + "active": typeof config.active === "undefined" || config.active, + "chooser_html": this.make_layer_chooser_html(config), + "layer_group": layer_group + }; + + + return ret; + } + + + /** + * @param {EntityLayerConfig} config + * @return {HTMLElement} + */ + this.make_layer_chooser_html = function (config) { + return $('<span/>') + .attr("title", config.description) + .append(config.icon.html) + .append(config.name)[0]; + } + + /** * Add the graticule to the current map view. * @@ -824,8 +1034,8 @@ var caosdb_map = new function () { * area to be searched in. * * @param {number} north - * @param {number} west * @param {number} south + * @param {number} west * @param {number} east * @return {string} a query string. */ @@ -870,7 +1080,7 @@ var caosdb_map = new function () { var result = $.extend(true, {}, this ._default_config, conf); - if(!result.views || result.views.length === 0){ + if (!result.views || result.views.length === 0) { logger.debug( "Could not find any view config. using a dummy tiling server." ); @@ -932,14 +1142,14 @@ var caosdb_map = new function () { * @param {DataModelConfig} datamodel * @returns {HTMLElement[]} entities with coordinate properties. */ - this.get_map_objects = function (container, datamodel) { - var map_objects = $(container) + this.get_map_entities = function (container, datamodel) { + var map_entities = $(container) .find(".caosdb-entity-panel").has( - ".caosdb-property-name:contains('" + datamodel - .lng + "')").has( - ".caosdb-property-name:contains('" + datamodel - .lat + "')"); - return map_objects.toArray(); + ".caosdb-property-name:contains('" + datamodel + .lng + "')").has( + ".caosdb-property-name:contains('" + datamodel + .lat + "')"); + return map_entities.toArray(); } @@ -980,74 +1190,35 @@ var caosdb_map = new function () { * @returns {HTMLElement} a label with the entities name. */ this.make_entity_name_label = function (entity) { - var name = getEntityName(entity); - var id = getEntityId(entity); - var name_label = $('<div/>') + const name = getEntityName(entity); + const id = getEntityId(entity); + + const entity_on_page = $(`#${id}`).length > 0; + const href = entity_on_page ? `#${id}` : connection.getBasePath() + `Entity/${id}` + const link_title = entity_on_page ? "Jump to this entitiy." : "Browse to this entity."; + const link = $(`<a title="${link_title}" href="${href}"/>`) + .addClass("pull-right") + .append(`<span class="glyphicon glyphicon-share-alt"/></a>`); + + const name_label = $('<div/>') // TODO move to global css .css({ "margin-top": "4px", "margin-bottom": "4px" }) .text(name) - .append( - '<a title="Jump to this entity" class="pull-right" href="#' + - id + - '"><span class="glyphicon glyphicon-share-alt"/></a>' - ); + .append(link); return name_label[0]; } /** - * Create a div element which shows the most relevant information of an - * entity and is shown as a pop-up when the user clicks on a marker. + * Retrieve entities from the server and return a container. * - * @param {HTMLElement} entity - the entity in HTML representation. - * @returns {HTMLElement} the div element. + * @param {string} query_str - a CQL query. + * @returns {HTMLElement[]} an array of entities in HTML representation. */ - this.make_map_popup_representation = function (entity) { - var role_label = $(entity).find( - ".label.caosdb-f-entity-role").first().clone(); - var parent_list = this.make_parent_labels(entity); - var name = this.make_entity_name_label(entity); - var ret = $('<div/>') - .append(role_label) - .append(parent_list) - .append(name); - return ret[0]; - } - - /** - * Collect all entities with coordinates on the current page and create - * markers ({@link L.Marker}). - * - * The markers are not added to any map by this method. The are only - * added to the {@link L.LayerGroup}. - * - * The entities must have the properties for latitude and longitude as - * configured by the {@link DataModelConfig}. - * - * @param {DataModelConfig} datamodel - the datamodel which specifies - * the properties for coordinates. - * @param {L.LayerGroup} layergroup - where to add the newly created - * markers. - */ - this.add_current_page_entities = function (datamodel, - layergroup) { - logger.trace( - "enter add_current_page_entities", - datamodel, layergroup); - var container = $(".caosdb-f-main-entities")[0]; - var map_objects = this.get_map_objects(container, datamodel); - - var markers = this.create_entitiy_markers(map_objects, datamodel); - for (const marker of markers) { - layergroup.addLayer(marker) - }; - - logger.trace( - "leave add_current_page_entities"); - } + this.query = query; /** @@ -1057,99 +1228,41 @@ var caosdb_map = new function () { * representation. * @param {DataModelConfig} datamodel - specifies the properties for * coordinates. + * @param {mapEntityPopupGenerator} [make_popup] - creates popup content. + * @param {number} zIndexOffset - zIndexOffset of the marker. + * @param {DivIcon_options} icon_options * @returns {L.Marker[]} an array of markers for the map. */ - this.create_entitiy_markers = function (entities, datamodel) { - logger.trace("enter create_entitiy_markers", entities, datamodel); + this.create_entitiy_markers = function (entities, datamodel, make_popup, zIndexOffset, icon_options) { + logger.trace("enter create_entitiy_markers", entities, datamodel, zIndexOffset, icon_options); var ret = [] - for (const map_object of entities) { - var lat = getProperty(map_object, datamodel.lat); - var lng = getProperty(map_object, datamodel.lng); + for (const map_entity of entities) { + var lat = getProperty(map_entity, datamodel.lat); + var lng = getProperty(map_entity, datamodel.lng); if (lat && lng) { - logger.debug("found map-object", map_object, - lat, lng); - var marker = L.marker([lat, lng]); - marker.bindPopup(this - .make_map_popup_representation( - map_object)); + logger.debug(`create entity marker at [${lat}, ${lng}] for`, + map_entity); + var marker = L.marker([lat, lng], { + icon: L.divIcon(icon_options) + }); + + if (zIndexOffset) { + marker.setZIndexOffset(zIndexOffset); + } + if (make_popup) { + marker.bindPopup(make_popup(map_entity, datamodel)); + } ret.push(marker); } else { - logger.debug("no valid latitude or longitude", - map_object, lat, lng); + logger.debug("undefined latitude or longitude", + map_entity, lat, lng); } } return ret; } - /** - * Return the standard layer groups. - * - * Currently only one standard layer group is known: - * "current_page_layer", which contains markers of the position of all - * entities which are on the current page. - * - * @returns {object} a mapping from string name to a L.LayerGroup. - */ - this.get_standard_layer_groups = function () { - logger.trace("enter get_standard_layer_groups"); - var new_groups = {}; - - // current page entities - var cpe_group = L.layerGroup(); - new_groups["current_page_entities"] = cpe_group; - - logger.trace("leave get_standard_layer_groups"); - return new_groups; - } - - /** - * Initialize the layer groups - * - * - * Layer groups are sets of Layers identified by a string name, - * i.e. a mapping from a string name to a {@link L.LayerGroup}. - * - * Layer groups are used to organize different types of layers. - * - * This method adds standard layer groups the map (without creating any - * layers tho). - * - * Especially the markers that are used to show the location of those - * entities which are shown on the current page are grouped together in - * the "current_page_entities" group. - * - * @param {L.Map} map - the map of the layers. - * @returns {object} a mapping from names to L.LayerGroups. - */ - this.init_layer_groups = function (map) { - logger.trace("enter init_layer_groups"); - if (typeof this._layer_groups == "undefined") { - this._layer_groups = this - .get_standard_layer_groups(); - } - - for (var name of Object.keys(this._layer_groups)) { - let lg = this.get_layer_group(name); - logger.debug("add layer group to map:", name, lg); - lg.addTo(map); - } - logger.trace("leave init_layer_groups"); - return this._layer_groups; - } - - /** - * Get a layer group identified by a name. - * - * The layer group must already be in the _layer_groups list. - * - * @param {string} name - name of the layer group. - * @returns {L.LayerGroup} - */ - this.get_layer_group = function (name) { - return this._layer_groups[name]; - } /** * Plug-in for leaflet which adds a menu to the map for changing the view. diff --git a/src/core/js/ext_references.js b/src/core/js/ext_references.js index 943eadad4dd1bc50cd1df96deb93caa153678887..68a52a023e49d8e9f092883817ad82b1da1c87cd 100644 --- a/src/core/js/ext_references.js +++ b/src/core/js/ext_references.js @@ -12,7 +12,7 @@ * @return {Object} A set of booleans for each side of the element */ // Source address: https://gomakethings.com/how-to-check-if-any-part-of-an-element-is-out-of-the-viewport-with-vanilla-js/ -var isOutOfViewport = function(elem) { +var isOutOfViewport = function (elem) { // Get element's bounding var bounding = elem.getBoundingClientRect(); @@ -38,7 +38,7 @@ var isOutOfViewport = function(elem) { * @return {boolean} * */ -var is_in_viewport_vertically = function(elem) { +var is_in_viewport_vertically = function (elem) { var out = isOutOfViewport(elem); return !(out.top || out.bottom); } @@ -54,7 +54,7 @@ var is_in_viewport_vertically = function(elem) { * @return {boolean} * */ -var is_in_viewport_horizontally = function(elem) { +var is_in_viewport_horizontally = function (elem) { var scrollbox = elem.parentElement.parentElement; // Check this condition only if the grand parent is a list and return true otherwise. if (scrollbox.classList.contains("caosdb-value-list") == true) { @@ -72,13 +72,14 @@ var is_in_viewport_horizontally = function(elem) { /** * The resolve_references module. */ -var resolve_references = new function() { - this.init = function() { - // this.references(); +var resolve_references = new function () { + + this.init = function () { this.update_visible_references(); } - this.get_person_str = function(el) { + + this.get_person_str = function (el) { var valpr = getProperties(el); if (valpr == undefined) { return; @@ -87,6 +88,60 @@ var resolve_references = new function() { " " + valpr.filter(valprel => valprel.name.toLowerCase() == "lastname")[0].value; } + + /** + * Return true iff the entity has at least one parent named `rt`. + * + * @param {HTMLElement} el - entity in HTML representation. + * @param {string} rt - parent name. + * @returns {boolean} + */ + this.isChild = function (el, rt) { + var pars = getParents(el); + for (const par of pars) { + if (par.name === rt) { + return true; + } + } + return false; + } + + + this.find_bag_of_sample = async function (el) { + return await this._find_ice_sample_back_ref(getEntityID(el), "Bag"); + } + + + this.find_ice_core_of_sample = async function (el) { + return await this._find_ice_sample_back_ref(getEntityID(el), "IceCore"); + } + + + this._find_ice_sample_back_ref = async function (id, rt, oldcounter) { + var counter = oldcounter + 1 || 1 + if (counter > 5) { + return null; + } + var referencing_samples = await query("FIND IceSample WHICH REFERENCES " + id); + for (const sample of referencing_samples) { + if (this.isChild(sample, rt)) { + return sample; + } else { + var ret = await this._find_ice_sample_back_ref(getEntityID(sample), rt, counter); + if (ret) { + return ret; + } + } + } + return undefined; + } + + const _stripe_re = /Stripe$/i; + this.isStripe = function(el) { + return _stripe_re.test(el.name) + } + + /* * Function that retrieves meaningful information for a single element. * @@ -94,42 +149,73 @@ var resolve_references = new function() { * * rs: Element having the class caosdb-resolvable-reference and including a caosdb-resolve-reference target. */ - this.update_single_resolvable_reference = async function(rs) { + this.update_single_resolvable_reference = async function (rs) { + // remove caosdb-resolvable-reference class because this reference is + // being resolved right now. + $(rs).toggleClass("caosdb-resolvable-reference", false); + var rseditable = rs.getElementsByClassName("caosdb-resolve-reference-target")[0]; + var id = getIDfromHREF(rs); + rseditable.textContent = id; var el = await retrieve(getIDfromHREF(rs)); var pr = getParents(el[0]); if (getEntityHeadingAttribute(el[0], "path") !== undefined || pr[0].name == "Image") { var pths = getEntityHeadingAttribute(el[0], "path").split("/"); rseditable.textContent = pths[pths.length - 1]; - } else if (pr[0].name == "Person") { + } else if (pr[0].name === "Person") { rseditable.textContent = this.get_person_str(el[0]); - } else if (pr[0].name == "ExperimentSeries") { + } else if (pr[0].name === "ExperimentSeries") { rseditable.textContent = getEntityName(el[0]); - } else if (pr[0].name == "BoxType") { + } else if (pr[0].name === "BoxType") { rseditable.textContent = getEntityName(el[0]); - } else if (pr[0].name == "Loan") { + } else if (pr[0].name === "Loan") { var persel = await retrieve(getProperty(el[0], "Borrower")); var loan_state = awi_demo.get_loan_state_string(getProperties(el[0])); rseditable.textContent = "Borrowed by " + this.get_person_str(persel[0]) + " (" + loan_state.replace("_", " ") + ")"; - } else if (pr[0].name == "Box") { - rseditable.textContent = getProperty(el[0], "Number"); - } else if (pr[0].name == "Bag") { + } else if (pr[0].name === "SubSample" || this.isStripe(pr[0])) { + var bag = await this.find_bag_of_sample(el[0]); + if (!bag) { + var icecore = await this.find_ice_core_of_sample(el[0]); + if (!icecore) { + rseditable.textContent = `${id} (Sample w/o Bag or Ice Core)`; + } else { + rseditable.textContent = `${id} (Ice Core ${getEntityName(icecore)}, no Bag)`; + } + } else { + var icecore = (await query("SELECT name FROM IceCore WHICH REFERENCES " + getEntityID(bag)))[0]; + if (!icecore) { + rseditable.textContent = `${id} (Bag ${getProperty(bag, "Number", false)}, no Ice Core)`; + } else { + rseditable.textContent = `${id} (Ice Core ${getEntityName(icecore)}, Bag ${getProperty(bag, "Number", false)})`; + } + } + } else if (pr[0].name === "Bag") { + var bag = el[0]; + var icecore = (await query("SELECT name FROM IceCore WHICH REFERENCES " + getEntityID(bag)))[0]; + if (!icecore) { + rseditable.textContent = `${id} (Number ${getProperty(bag, "Number", false)}, no Ice Core)`; + } else { + rseditable.textContent = `${id} (Ice Core ${getEntityName(icecore)}, Number ${getProperty(bag, "Number", false)})`; + } + } else if (pr[0].name === "Box") { rseditable.textContent = getProperty(el[0], "Number"); - } else if (pr[0].name == "Palette") { + } else if (pr[0].name === "Palette") { rseditable.textContent = getProperty(el[0], "Number"); - } else if (pr[0].name.includes("Model")) { - rseditable.textContent = pr[0].name; } else { - rseditable.textContent = el[0].id; + if (typeof el[0].name !== "undefined" && el[0].length > 0) { + rseditable.textContent = el[0].name; + } } + } + /* * This function updates all references that are inside of the current viewport. * */ - this.update_visible_references = async function() { - var rs = document.getElementsByClassName("caosdb-resolvable-reference"); + this.update_visible_references = async function () { + var rs = $(".caosdb-resolvable-reference"); for (var i = 0; i < rs.length; i++) { if (is_in_viewport_vertically(rs[i]) && @@ -138,25 +224,17 @@ var resolve_references = new function() { } } } - - this.references = async function() { - var rs = document.getElementsByClassName("caosdb-resolvable-reference"); - - for (var i = 0; i < rs.length; i++) { - this.update_single_resolvable_reference(rs[i]); - } - } } -$(document).ready(function() { +$(document).ready(function () { resolve_references.init(); var scrollTimeout = undefined; var updatefunc = () => { if (scrollTimeout) { clearTimeout(scrollTimeout); } - scrollTimeout = setTimeout(function() { + scrollTimeout = setTimeout(function () { resolve_references.update_visible_references(); }, 500); }; diff --git a/src/core/js/form_elements.js b/src/core/js/form_elements.js index cb08ac7083b352883fa4bafb806711eccb850b27..8e0f6c82deba0ed55122eb0451bde6080d5e7280 100644 --- a/src/core/js/form_elements.js +++ b/src/core/js/form_elements.js @@ -63,7 +63,7 @@ * TODO * */ -var form_elements = new function() { +var form_elements = new function () { this.version = "0.1"; this.dependencies = ["log", "caosdb_utils", "markdown"]; @@ -80,13 +80,13 @@ var form_elements = new function() { this.form_error_event = new Event("caosdb.form.error"); - this.get_cache_key = function(form, field) { + this.get_cache_key = function (form, field) { var form_key = $(form).prop("name"); var field_key = $(field).attr("data-field-name"); return "form_elements." + form_key + "." + field_key; } - this.get_cache_value = function(field) { + this.get_cache_value = function (field) { var ret = $(field) .find(":input") .val(); @@ -97,11 +97,11 @@ var form_elements = new function() { } } - this.cache_form = function(cache, form) { + this.cache_form = function (cache, form) { this.logger.trace("enter cache_form", cache, form); $(form) .find(".caosdb-f-field.caosdb-f-form-field-cached") - .each(function(index, field) { + .each(function (index, field) { var value = form_elements.get_cache_value(field); const key = form_elements.get_cache_key(form, field); if (value !== null) { @@ -114,7 +114,7 @@ var form_elements = new function() { } - this.set_cached = function(field) { + this.set_cached = function (field) { $(field).toggleClass("caosdb-f-form-field-cached", true); } @@ -126,9 +126,9 @@ var form_elements = new function() { await form_elements.field_ready(field); const old_value = form_elements.get_cache_value(field); - if(old_value !== value) { + if (old_value !== value) { form_elements.logger.trace("loaded from cache", field, value); - if(typeof $().selectpicker === "function" && + if (typeof $().selectpicker === "function" && $(field).find(".selectpicker").length > 0) { $(field).find(".selectpicker").selectpicker("val", value); } else { @@ -138,16 +138,16 @@ var form_elements = new function() { } } - this.is_set = function(field) { + this.is_set = function (field) { var value = $(field).find(":input").val(); return value && value.length > 0; } - this.load_cached = function(cache, form) { + this.load_cached = function (cache, form) { this.logger.trace("enter load_cached", cache, form); $(form) .find(".caosdb-f-field.caosdb-f-form-field-cached") - .each(function(index, field) { + .each(function (index, field) { var key = form_elements.get_cache_key(form, field); var value = cache[key] || null; if (value !== null) { @@ -164,9 +164,9 @@ var form_elements = new function() { /** * (Re-)set this module's functions to standard implementation. */ - this._init_functions = function() { + this._init_functions = function () { - this.init = function() { + this.init = function () { this.logger.trace("enter init"); } @@ -183,13 +183,13 @@ var form_elements = new function() { * @param {string} [desc] - the description for the entity. * @returns {HTMLElement} OPTION element. */ - this.make_reference_option = function(entity_id, desc) { + this.make_reference_option = function (entity_id, desc) { caosdb_utils.assert_string(entity_id, "param `entity_id`"); - if(typeof desc == "undefined") { + if (typeof desc == "undefined") { desc = entity_id; } - var opt_str = '<option value="' + entity_id + '">' + desc - + "</option>"; + var opt_str = '<option value="' + entity_id + '">' + desc + + "</option>"; return $(opt_str)[0]; } @@ -205,9 +205,9 @@ var form_elements = new function() { * parameter. * @returns {HTMLElement} SELECT element with entity options. */ - this.make_reference_select = async function(entities, make_desc, multiple=false) { + this.make_reference_select = async function (entities, make_desc, multiple = false) { caosdb_utils.assert_array(entities, "param `entities`", false); - if ( typeof make_desc !== "undefined" ) { + if (typeof make_desc !== "undefined") { caosdb_utils.assert_type(make_desc, "function", "param `make_desc`"); } @@ -217,7 +217,7 @@ var form_elements = new function() { } else { ret.append('<option style="display: none" selected="selected" value="" disabled="disabled"></option>'); } - for ( let entity of entities ) { + for (let entity of entities) { this.logger.trace("add option", entity); let entity_id = getEntityID(entity); let desc = typeof make_desc == "function" ? await make_desc(entity) : @@ -238,15 +238,17 @@ var form_elements = new function() { * * @param {object} config * @returns {HTMLElement} SELECT element. + * + * TODO make syncronous */ - this.make_reference_drop_down = async function(config) { + this.make_reference_drop_down = async function (config) { let ret = $(this._make_field_wrapper(config.name)); let label = this._make_input_label_str(config); let loading = $('<div class="caosdb-f-field-not-ready">loading...</div>'); let input_col = $('<div class="col-sm-9"/>'); input_col.append(loading); - this._query(config.query).then(async function(entities){ + this._query(config.query).then(async function (entities) { let select = $(await form_elements.make_reference_select( entities, config.make_desc, config.multiple)); select.attr("name", config.name); @@ -254,7 +256,7 @@ var form_elements = new function() { input_col.append(select); form_elements.init_select_picker(ret[0]); ret[0].dispatchEvent(form_elements.field_ready_event); - select.change(function() { + select.change(function () { ret[0].dispatchEvent(form_elements.field_changed_event); }); }).catch(err => { @@ -268,7 +270,7 @@ var form_elements = new function() { } - this.init_select_picker = function(field) { + this.init_select_picker = function (field) { caosdb_utils.assert_html_element(field, "parameter `field`"); const select = $(field).find("select")[0]; const select_picker_options = {}; @@ -285,12 +287,12 @@ var form_elements = new function() { } - this.init_actions_box = function(field) { + this.init_actions_box = function (field) { this.logger.trace("enter init_actions_box", field); caosdb_utils.assert_html_element(field, "parameter `field`"); const select = $(field).find("select"); var actions_box = select.siblings().find(".bs-actionsbox"); - if(actions_box.length === 0) { + if (actions_box.length === 0) { actions_box = $(`<div class="bs-actionsbox"> <div class="btn-group btn-group-sm btn-block"> <button type="button" class="actions-btn btn-default bs-deselect-all btn btn-light">None</button> @@ -305,7 +307,7 @@ var form_elements = new function() { field.addEventListener( form_elements.field_changed_event.type, (e) => { - if(form_elements.is_set(field)) { + if (form_elements.is_set(field)) { actions_box.show(); } else { actions_box.hide(); @@ -314,7 +316,7 @@ var form_elements = new function() { actions_box .find(".bs-deselect-all") - .click((e)=>{ + .click((e) => { select.val(null) .selectpicker("render") .parent().toggleClass("open", false); @@ -333,13 +335,13 @@ var form_elements = new function() { * @param {HTMLElement} field * @return {Promise} the field-ready promise */ - this.field_ready = function(field) { + this.field_ready = function (field) { // TODO add support for field name (string) as field parameter // TODO check type of param field (not an array!) caosdb_utils.assert_html_element(field, "parameter `field`"); - return new Promise(function(resolve, reject) { + return new Promise(function (resolve, reject) { try { - if(!$(field).hasClass("caosdb-f-field-not-ready") && $(field).find(".caosdb-f-field-not-ready").length === 0) { + if (!$(field).hasClass("caosdb-f-field-not-ready") && $(field).find(".caosdb-f-field-not-ready").length === 0) { resolve(field); } else { field.addEventListener(form_elements.field_ready_event.type, @@ -359,13 +361,20 @@ var form_elements = new function() { this._run_script = async function (script, form) { const json_str = JSON.stringify(form_elements.form_to_object(form[0])); - const params = {"-p0": {"filename": "form.json", "blob": new Blob([json_str], {type: "application/json"})}}; + const params = { + "-p0": { + "filename": "form.json", + "blob": new Blob([json_str], { + type: "application/json" + }) + } + }; const result = await connection.runScript(script, params); this.logger.debug("server-side script returned", result); return this.parse_script_result(result); } - this.parse_script_result = function(result) { + this.parse_script_result = function (result) { console.log(result); const scriptNode = result.evaluate("/Response/script", result, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; const code = result.evaluate("@code", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; @@ -374,13 +383,18 @@ var form_elements = new function() { const stderr = result.evaluate("stderr", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; const stdout = result.evaluate("stdout", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; - return { "code": code, "call": call, "stdout": stdout, "stderr": stderr }; + return { + "code": code, + "call": call, + "stdout": stdout, + "stderr": stderr + }; } /** * generate a java script object representation of a form */ - this.form_to_object = function(form) { + this.form_to_object = function (form) { this.logger.trace("entity form_to_json", form); caosdb_utils.assert_html_element(form, "parameter `form`"); @@ -389,7 +403,7 @@ var form_elements = new function() { for (const child of element.children) { // ignore disabled fields and subforms - if ($(child).hasClass("caosdb-f-field-disabled")){ + if ($(child).hasClass("caosdb-f-field-disabled")) { continue; } const name = $(child).attr("name"); @@ -400,7 +414,7 @@ var form_elements = new function() { var subform_obj = _to_json(child, {}); if (typeof data[subform] === "undefined") { data[subform] = subform_obj; - } else if (Array.isArray(data[subform])){ + } else if (Array.isArray(data[subform])) { data[subform].push(subform_obj); } else { data[subform] = [data[subform], subform_obj] @@ -408,12 +422,12 @@ var form_elements = new function() { } else if (name && name !== "") { // input elements const not_checkbox = !$(child).is(":checkbox"); - if ( not_checkbox || $(child).is(":checked")) { + if (not_checkbox || $(child).is(":checked")) { // checked or not a checkbox var value = $(child).val(); if (typeof data[name] === "undefined") { data[name] = value; - } else if (Array.isArray(data[name])){ + } else if (Array.isArray(data[name])) { data[name].push(value); } else { data[name] = [data[name], value] @@ -436,12 +450,12 @@ var form_elements = new function() { return ret; } - this.make_submit_button = function() { + this.make_submit_button = function () { var ret = $('<button class="caosdb-f-form-elements-submit-button btn btn-primary" type="submit">Submit</button>'); return ret[0]; } - this.make_cancel_button = function(form) { + this.make_cancel_button = function (form) { var ret = $('<button class="caosdb-f-form-elements-cancel-button btn btn-primary" type="button">Cancel</button>'); ret.on("click", e => { this.logger.debug("cancel form", e, form); @@ -450,22 +464,25 @@ var form_elements = new function() { return ret[0]; } - this.make_form_field = async function(config) { + /** + * TODO make syncronous + */ + this.make_form_field = async function (config) { caosdb_utils.assert_type(config, "object", "param `config`"); caosdb_utils.assert_string(config.type, "`config.type` of param `config`"); var field = undefined; const type = config.type; if (type === "date") { - field = await this.make_date_input(config); + field = this.make_date_input(config); } else if (type === "checkbox") { - field = await this.make_checkbox_input(config); + field = this.make_checkbox_input(config); } else if (type === "text") { - field = await this.make_text_input(config); + field = this.make_text_input(config); } else if (type === "double") { - field = await this.make_double_input(config); + field = this.make_double_input(config); } else if (type === "integer") { - field = await this.make_integer_input(config); + field = this.make_integer_input(config); } else if (type === "range") { field = await this.make_range_input(config); } else if (type === "reference_drop_down") { @@ -491,9 +508,11 @@ var form_elements = new function() { } - this.add_help = function(field, config) { + this.add_help = function (field, config) { var help_button = $('<span data-trigger="click focus" data-toggle="popover" class="caosdb-f-form-help pull-right glyphicon glyphicon-info-sign"/>') - .css({"cursor": "pointer"}); + .css({ + "cursor": "pointer" + }); if (typeof config === "string" || config instanceof String) { help_button.attr("data-content", config); @@ -504,16 +523,18 @@ var form_elements = new function() { var label = $(field).children("label"); - if(label.length > 0) { - help_button.css({"margin-left": "4px"}); + if (label.length > 0) { + help_button.css({ + "margin-left": "4px" + }); label.first().append(help_button); } else { $(field).append(help_button); } } - this.make_heading = function(config) { - if(typeof config.header === "undefined") { + this.make_heading = function (config) { + if (typeof config.header === "undefined") { return; } else if (typeof config.header === "string" || config.header instanceof String) { return $('<div class="caosdb-f-form-header h3">' + config.header + '</div>')[0]; @@ -522,7 +543,7 @@ var form_elements = new function() { return config.header; } - this.make_form_wrapper = function(form, config) { + this.make_form_wrapper = function (form, config) { var wrapper = $('<div class="caosdb-f-form-wrapper"/>'); var header = this.make_heading(config); @@ -554,10 +575,10 @@ var form_elements = new function() { return wrapper[0]; } - this.make_form = function(config) { + this.make_form = function (config) { var form = undefined; - if(config.script) { + if (config.script) { form = this.make_script_form(config, config.script); } else { form = this.make_generic_form(config); @@ -566,7 +587,10 @@ var form_elements = new function() { return wrapper; } - this.make_subform = async function(config) { + /** + * TODO make syncronous + */ + this.make_subform = async function (config) { this.logger.trace("enter make_subform"); caosdb_utils.assert_type(config, "object", "param `config`"); caosdb_utils.assert_string(config.name, "`config.name` of param `config`"); @@ -575,7 +599,7 @@ var form_elements = new function() { const name = config.name; var form = $('<div data-subform-name="' + name + '" class="caosdb-f-form-elements-subform"/>'); - for ( let field of config.fields ) { + for (let field of config.fields) { this.logger.trace("add subform field", field); let elem = await this.make_form_field(field); form.append(elem); @@ -585,26 +609,26 @@ var form_elements = new function() { return form[0]; } - this.dismiss_form = function(form) { - if(form.tagName === "FORM") { + this.dismiss_form = function (form) { + if (form.tagName === "FORM") { form.dispatchEvent(this.cancel_form_event); } var _form = $(form).find("form"); - if (_form.length > 0){ + if (_form.length > 0) { _form[0].dispatchEvent(this.cancel_form_event); } } - this.enable_group = function(form, group) { + this.enable_group = function (form, group) { this.enable_fields(this.get_group_fields(form, group)); } - this.disable_group = function(form, group) { + this.disable_group = function (form, group) { this.disable_fields(this.get_group_fields(form, group)); } - this.get_group_fields = function(form, group) { - return $(form).find(".caosdb-f-field[data-groups*='("+group+")']").toArray(); + this.get_group_fields = function (form, group) { + return $(form).find(".caosdb-f-field[data-groups*='(" + group + ")']").toArray(); } /** @@ -613,44 +637,44 @@ var form_elements = new function() { * @param {string} name - the field name * @return {HTMLElement[]} array of fields */ - this.get_fields = function(form, name) { + this.get_fields = function (form, name) { caosdb_utils.assert_html_element(form, "parameter `form`"); caosdb_utils.assert_string(name, "parameter `name`"); return $(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray(); } - this.add_field_to_group = function(field, group) { + this.add_field_to_group = function (field, group) { this.logger.trace("enter add_field_to_group", field, group); - var groups = ($(field).attr("data-groups")?$(field).attr("data-groups"):"") + "(" + group + ")"; + var groups = ($(field).attr("data-groups") ? $(field).attr("data-groups") : "") + "(" + group + ")"; $(field).attr("data-groups", groups); } - this.disable_fields = function(fields) { + this.disable_fields = function (fields) { $(fields).toggleClass("caosdb-f-field-disabled", true).hide(); for (const field of $(fields)) { field.dispatchEvent(this.field_disabled_event); } } - this.enable_fields = function(fields) { + this.enable_fields = function (fields) { $(fields).toggleClass("caosdb-f-field-disabled", false).show(); for (const field of $(fields)) { field.dispatchEvent(this.field_enabled_event); } } - this.enable_name = function(form, name) { + this.enable_name = function (form, name) { this.enable_fields(form.find(".caosdb-f-field[data-field-name='" + name + "']").toArray()); } - this.disable_name = function(form, name) { + this.disable_name = function (form, name) { this.disable_fields(form.find(".caosdb-f-field[data-field-name='" + name + "']").toArray()); } - this.make_script_form = async function(config, script) { + this.make_script_form = async function (config, script) { this.logger.trace("enter make_script_form"); - const submit_callback = async function(form) { + const submit_callback = async function (form) { form = $(form); @@ -672,7 +696,10 @@ var form_elements = new function() { }; this.logger.trace("leave make_script_form"); - const new_config = $.extend({}, {name: script, submit:submit_callback}, config); + const new_config = $.extend({}, { + name: script, + submit: submit_callback + }, config); return await this.make_generic_form(new_config); } @@ -685,7 +712,7 @@ var form_elements = new function() { * * TODO */ - this.make_generic_form = async function(config) { + this.make_generic_form = async function (config) { this.logger.trace("enter make_generic_form"); caosdb_utils.assert_type(config, "object", "param `config`"); @@ -695,14 +722,14 @@ var form_elements = new function() { const form = $('<form class="form-horizontal" action="#" method="post" />'); // set name - if(config.name) { + if (config.name) { form.attr("name", config.name); } // add fields - for ( let field of config.fields ) { + for (let field of config.fields) { this.logger.trace("add field", field); - if ( field instanceof HTMLElement ) { + if (field instanceof HTMLElement) { form.append(field); } else { let elem = await this.make_form_field(field); @@ -712,16 +739,16 @@ var form_elements = new function() { // set groups if (config.groups) { - for ( let group of config.groups ) { + for (let group of config.groups) { this.logger.trace("add group", group); - for ( let fieldname of group.fields ) { + for (let fieldname of group.fields) { let field = form.find(".caosdb-f-field[data-field-name='" + fieldname + "']"); this.logger.trace("set group", field, group); this.add_field_to_group(field, group.name) } // disable if necessary - if (typeof group.enabled === "undefined" || group.enabled ) { + if (typeof group.enabled === "undefined" || group.enabled) { this.enable_group(form, group.name); } else { this.disable_group(form, group.name); @@ -732,14 +759,14 @@ var form_elements = new function() { const footer = this.make_footer(); form.append(footer); - if(!(typeof config.submit === 'boolean' && config.submit === false)) { + if (!(typeof config.submit === 'boolean' && config.submit === false)) { // add submit button unless config.submit is false footer.append(this.make_submit_button()); } form[0].addEventListener("submit", (e) => { e.preventDefault(); e.stopPropagation(); - if(form.find(".caosdb-f-form-submitting").length > 0) { + if (form.find(".caosdb-f-form-submitting").length > 0) { // do not submit twice return; } @@ -766,9 +793,9 @@ var form_elements = new function() { const success_handler = config.success; const submit_callback = config.submit; form.find(".caosdb-f-form-elements-message").remove(); - if(typeof config.submit === "function") { + if (typeof config.submit === "function") { // wrap callback in async function - const _wrap_callback = async function() { + const _wrap_callback = async function () { try { var results = await submit_callback(form[0]); @@ -805,12 +832,12 @@ var form_elements = new function() { }, true); - form[0].addEventListener(this.form_success_event.type, function(e) { + form[0].addEventListener(this.form_success_event.type, function (e) { // remove submit button, show ok button form.find("button[type='submit']").remove(); form.find("button:contains('Cancel')").text("Ok").prop("disabled", false); }, true); - form[0].addEventListener(this.form_error_event.type, function(e) { + form[0].addEventListener(this.form_error_event.type, function (e) { // reenable inputs form.find(":input").prop("disabled", false); }, true); @@ -828,7 +855,7 @@ var form_elements = new function() { return form[0]; } - this.init_form_caching = function(config, form) { + this.init_form_caching = function (config, form) { var default_config = { "cache_event": form_elements.submit_form_event.type, "cache_storage": localStorage @@ -843,50 +870,65 @@ var form_elements = new function() { form_elements.load_cached(lconfig.cache_storage, form); } - this.show_results = function(form, results) { + this.show_results = function (form, results) { $(form).append(results); } - this.show_errors = function(form, errors) { + this.show_errors = function (form, errors) { $(form).append(errors); } - this.make_footer = function() { - return $('<div class="text-right caosdb-f-form-elements-footer"/>')[0]; + this.make_footer = function () { + return $('<div class="text-right caosdb-f-form-elements-footer"/>') + .css({ + "margin": "20px", + }).append(this.make_required_marker()) + .append('<span style="margin-right: 4px; font-size: 11px">required field</span>')[0]; } - this.make_error_message = function(message) { + this.make_error_message = function (message) { return this.make_message(message, "error"); } - this.make_success_message = function(message) { + this.make_success_message = function (message) { return this.make_message(message, "success"); } - this.make_submitting_info = function() { + this.make_submitting_info = function () { // TODO styling return $(this.make_message("Submitting... please wait. This might take some time.", "info")) - .toggleClass("h3",true) - .toggleClass("caosdb-f-form-submitting",true) - .toggleClass("text-right",true)[0]; + .toggleClass("h3", true) + .toggleClass("caosdb-f-form-submitting", true) + .toggleClass("text-right", true)[0]; } - this.make_message = function(message, type) { + this.make_message = function (message, type) { var ret = $('<div class="caosdb-f-form-elements-message"/>'); - if(type) { + if (type) { ret.addClass("caosdb-f-form-elements-message-" + type); } return ret.append(markdown.textToHtml(message))[0]; } - this.make_range_input = async function(config) { + /** + * TODO make syncronous + */ + this.make_range_input = async function (config) { // TODO // 1. wrapp both inputs to separate it from the label into a container // 2. make two rows for each input // 3. make inline-block for all included elements - const from_config = $.extend({}, {cached: config.cached, required: config.required, type: "double"}, config.from); - const to_config = $.extend({}, {cached: config.cached, required: config.required, type: "double"}, config.to); + const from_config = $.extend({}, { + cached: config.cached, + required: config.required, + type: "double" + }, config.from); + const to_config = $.extend({}, { + cached: config.cached, + required: config.required, + type: "double" + }, config.to); const from_input = await this.make_form_field(from_config); const to_input = await this.make_form_field(to_config); @@ -919,16 +961,17 @@ var form_elements = new function() { * @param {string} name - the name of the field. * @returns {HTMLElement} a DIV. */ - this._make_field_wrapper = function(name) { + this._make_field_wrapper = function (name) { caosdb_utils.assert_string(name, "param `name`"); - return $('<div class="form-group caosdb-f-field caosdb-property-row" data-field-name="'+name+'" />')[0]; + return $('<div class="form-group caosdb-f-field caosdb-property-row" data-field-name="' + name + '" />') + .css({"padding": "0"})[0]; } - this.make_date_input = function(config) { + this.make_date_input = function (config) { return this._make_input(config); } - this.make_text_input = function(config) { + this.make_text_input = function (config) { return this._make_input(config); } @@ -941,8 +984,10 @@ var form_elements = new function() { * @param {form_elements.input_config} config. * @returns {HTMLElement} a double form field. */ - this.make_double_input = function(config) { - var clone = $.extend({}, config, {type: "number"}); + this.make_double_input = function (config) { + var clone = $.extend({}, config, { + type: "number" + }); var ret = $(this._make_input(clone)) ret.find("input").attr("step", "any"); return ret[0]; @@ -957,7 +1002,7 @@ var form_elements = new function() { * @param {form_elements.input_config} config. * @returns {HTMLElement} an integer form field. */ - this.make_integer_input = function(config) { + this.make_integer_input = function (config) { var ret = $(this.make_double_input(config)); ret.find("input").attr("step", "1"); return ret[0]; @@ -970,16 +1015,18 @@ var form_elements = new function() { * @param {form_elements.checkbox_config} config. * @returns {HTMLElement} a checkbox form field. */ - this.make_checkbox_input = function(config) { - var clone = $.extend({}, config, {type: "checkbox"}); + this.make_checkbox_input = function (config) { + var clone = $.extend({}, config, { + type: "checkbox" + }); var ret = $(this._make_input(clone)); ret.find("input:checkbox").prop("checked", false); ret.find("input:checkbox").toggleClass("form-control", false); - if(config.checked ) { + if (config.checked) { ret.find("input:checkbox").prop("checked", true); ret.find("input:checkbox").attr("checked", "checked"); } - if(config.value) { + if (config.value) { ret.find("input:checkbox").attr("value", config.value); } return ret[0]; @@ -991,22 +1038,41 @@ var form_elements = new function() { * * @param {HTMLElement} field - the required form field. */ - this.set_required = function(field) { + this.set_required = function (field) { $(field).toggleClass("caosdb-f-form-field-required", true); + $(field).find(":input").prop("required", true); + $(field).find("label").prepend(this.make_required_marker()); + } + + /** + * Return a span which is to be inserted before a field's label text + * and which marks that field as required. + * + * @returns {HTMLElement} span element. + */ + this.make_required_marker = function () { + // TODO create class and move to css file + return $('<span>*</span>') + .css({ + "font-size": "10px", + "color": "red", + "margin-right": "4px", + "font-weight": "100", + })[0]; } - this.get_enabled_required_fields = function(form) { + this.get_enabled_required_fields = function (form) { return $(this.get_enabled_fields(form)) .filter(".caosdb-f-form-field-required") .toArray(); } - this.get_enabled_fields = function(form) { + this.get_enabled_fields = function (form) { return $(form) .find(".caosdb-f-field") - .filter(function(idx){ + .filter(function (idx) { // remove disabled fields from results return !$(this).hasClass("caosdb-f-field-disabled"); }) @@ -1014,9 +1080,9 @@ var form_elements = new function() { } - this.all_required_fields_set = function(form) { + this.all_required_fields_set = function (form) { const req = form_elements.get_enabled_required_fields(form); - for ( const field of req ) { + for (const field of req) { if (!form_elements.is_set(field)) { return false; } @@ -1027,16 +1093,16 @@ var form_elements = new function() { /** * @param {HTMLElement} form - the form be validated. */ - this.is_valid = function(form) { + this.is_valid = function (form) { return form_elements.all_required_fields_set(form); } - this.toggle_submit_button_form_valid = function(form, submit) { + this.toggle_submit_button_form_valid = function (form, submit) { // TODO do not change the submit button directly. change the // `submittable` state of the form and handle the case where a form // is submitting when this function is called. - if(form_elements.is_valid(form)){ + if (form_elements.is_valid(form)) { $(submit).prop("disabled", false); } else { $(submit).prop("disabled", true); @@ -1044,9 +1110,9 @@ var form_elements = new function() { } - this.init_validator = function(form) { + this.init_validator = function (form) { const submit = $(form).find(":input[type='submit']")[0]; - if(submit) { + if (submit) { form.addEventListener("caosdb.field.changed", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true); form.addEventListener("caosdb.field.enabled", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true); form.addEventListener("input", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true); @@ -1062,17 +1128,17 @@ var form_elements = new function() { * optional `label` * @returns {HTMLElement} a form field. */ - this._make_input = function(config) { + this._make_input = function (config) { caosdb_utils.assert_string(config.name, "the name of a form field"); let ret = $(this._make_field_wrapper(config.name)); let name = config.name; let label = this._make_input_label_str(config); let type = config.type; let value = config.value; - let input = $('<input class="form-control caosdb-property-text-value" type="' + type - + '" name="' + name - + '" />'); - input.change(function() { + let input = $('<input class="form-control caosdb-property-text-value" type="' + type + + '" name="' + name + + '" />'); + input.change(function () { ret[0].dispatchEvent(form_elements.field_changed_event); }); let input_col = $('<div class="caosdb-property-value col-sm-9"/>'); @@ -1094,19 +1160,19 @@ var form_elements = new function() { * @param {object} config - a config object with `name` and `label`. * @returns {string} a html string for a LABEL element. */ - this._make_input_label_str = function(config) { + this._make_input_label_str = function (config) { let name = config.name; let label = config.label; - return label ? '<label for="' + name - + '" data-property-name="' + name - + '" class="control-label col-sm-3">' + label - + '</label>' : ""; + return label ? '<label for="' + name + + '" data-property-name="' + name + + '" class="control-label col-sm-3">' + label + + '</label>' : ""; } } this._init_functions(); } -$(document).ready(function() { +$(document).ready(function () { caosdb_modules.register(form_elements); }); diff --git a/test/core/js/modules/edit_mode.js.js b/test/core/js/modules/edit_mode.js.js index 24c0a59580a4b0f346bd12b80ff52e049b79feb6..98fd3b2f9aac4f23f52edc6a57d6740d41600914 100644 --- a/test/core/js/modules/edit_mode.js.js +++ b/test/core/js/modules/edit_mode.js.js @@ -63,10 +63,6 @@ QUnit.test("init", function(assert){ assert.ok(edit_mode.init); }); -QUnit.test("scroll_edit_panel", function(assert){ - assert.ok(edit_mode.scroll_edit_panel); -}); - QUnit.test("dragstart", function(assert){ assert.ok(edit_mode.dragstart); }); diff --git a/test/core/js/modules/ext_map.js.js b/test/core/js/modules/ext_map.js.js index 7fb9c69dcd7b9ee2f2e4c7254ee534321cce2c6f..88a66a1c11c6691d7f4db53a502a5734ab4cbb3f 100644 --- a/test/core/js/modules/ext_map.js.js +++ b/test/core/js/modules/ext_map.js.js @@ -163,39 +163,73 @@ QUnit.test("create_map_view", function(assert) { }); - -QUnit.test("get_map_objects", function(assert) { +QUnit.test("get_map_entities", function(assert) { var datamodel = this.datamodel; var container = $('<div/>').append(this.test_map_entity); - var map_objects = caosdb_map.get_map_objects(container[0], datamodel); + var map_objects = caosdb_map.get_map_entities(container[0], datamodel); assert.equal(map_objects.length, 1, "has one map object"); }); + QUnit.test("create_entitiy_markers", function(assert) { var datamodel = this.datamodel; var entities = $(this.test_map_entity).toArray(); + + // w/o popup var markers = caosdb_map.create_entitiy_markers(entities, datamodel); assert.equal(markers.length, 1, "has one marker"); assert.ok(markers[0] instanceof L.Marker, "is marker"); var latlng = markers[0]._latlng; assert.equal(latlng.lat, "1.23", "latitude set"); assert.equal(latlng.lng, "5.23", "longitude set"); - assert.ok(markers[0]._popupHandlersAdded, "has popup"); + assert.notOk(markers[0].getPopup(), "no popup"); + + // with popup + var markers = caosdb_map.create_entitiy_markers(entities, datamodel, ()=>"popup"); + assert.ok(markers[0].getPopup(), "has popup"); }); -QUnit.test("add_current_page_entities", function(assert) { + +QUnit.test("_add_current_page_entities", function(assert) { var datamodel = this.datamodel; var layerGroup = L.layerGroup(); var container = $('<div class="caosdb-f-main-entities"/>').append(this.test_map_entity); $("body").append(container); assert.equal(layerGroup.getLayers().length, 0, "no layer"); - caosdb_map.add_current_page_entities(datamodel, layerGroup); - - assert.equal(layerGroup.getLayers().length, 1, "has one layer"); - assert.ok(layerGroup.getLayers()[0] instanceof L.Marker, - "layergroup has entity marker"); + var cpe = caosdb_map._get_current_page_entities(datamodel, undefined, undefined, undefined, undefined); + assert.equal(cpe.length, 1, "has one entity"); container.remove(); }); + +QUnit.test("make_layer_chooser_html", function(assert) { + var test_conf = { "id": "test_id", + "name": "test name", + "description": "test description", + "icon": { "html": "<span>ICON</span>", + }, + }; + + var layer_chooser = caosdb_map.make_layer_chooser_html(test_conf); + assert.ok(layer_chooser, "available"); + assert.equal($(layer_chooser).attr("title"), "test description", "description set as title"); +}); + +QUnit.test("init_entity_layer", function(assert) { + var done = assert.async(); + var test_conf = { "id": "test_id", + "name": "test name", + "description": "test description", + "get_entities": async function() {done(); return []}, + "icon": { "html": "<span>ICON</span>", + }, + } + + var entityLayer= caosdb_map.init_entity_layer(test_conf); + assert.equal(entityLayer.id, test_conf.id, "id"); + assert.equal(entityLayer.active, true, "is active"); + assert.ok(entityLayer.chooser_html instanceof HTMLElement, "chooser_html is HTMLElement"); + assert.equal(entityLayer.layer_group.getLayers().length, 0 , "empty layergroup"); +});