Skip to content
Snippets Groups Projects
Commit 24c8c17b authored by Henrik tom Wörden's avatar Henrik tom Wörden
Browse files

Add documentation on the forms module

parent fc773a90
No related branches found
No related tags found
No related merge requests found
...@@ -15,6 +15,9 @@ ...@@ -15,6 +15,9 @@
/build /build
__pycache__ __pycache__
# auto-generated sources
/src/doc/api
# screen logs # screen logs
screenlog.* screenlog.*
xerr.log xerr.log
......
...@@ -81,5 +81,6 @@ Build documentation in `build/` with `make doc`. ...@@ -81,5 +81,6 @@ Build documentation in `build/` with `make doc`.
- sphinx - sphinx
- sphinx-autoapi - sphinx-autoapi
- jsdoc (`npm install jsdoc`) - jsdoc (`npm install jsdoc`)
- jsdoc-sphinx (`npm install jsdoc-sphinx`)
- sphinx-js - sphinx-js
- recommonmark - recommonmark
...@@ -23,6 +23,28 @@ ...@@ -23,6 +23,28 @@
'use strict'; 'use strict';
/**
* @typedef {BottomLineConfig}
* @property {string|HTMLElement} fallback - Fallback content if none of
* the creators are applicable.
* @property {string} version - the version of the configuration which must
* match this module's version.
* @property {CreatorConfig[]} creators - an array of creators.
*/
/**
* @typedef {CreatorConfig}
* @property {string} [id] - a unique id for the creator. optional, for
* debuggin purposes.
* @property {function|string} is_applicable - If this is a string this has
* to be valid javascript! An asynchronous function which accepts one
* parameter, an entity in html representation, and which returns true
* iff this creator is applicable for the given entity.
* @property {string} create - This has to be valid javascript! An
* asynchronous function which accepts one parameter, an entity in html
* representation. It returns a HTMLElement or text node which will be
* shown in the bottom line container iff the creator is applicable.
*/
/** /**
* Add a special section to each entity one the current page where a thumbnail, * Add a special section to each entity one the current page where a thumbnail,
...@@ -45,6 +67,7 @@ ...@@ -45,6 +67,7 @@
* @requires UTIF (from utif.js library) * @requires UTIF (from utif.js library)
* @requires ext_table_preview (module from ext_table_preview.js) * @requires ext_table_preview (module from ext_table_preview.js)
*/ */
var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntityPath, connection, UTIF, ext_table_preview) { var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntityPath, connection, UTIF, ext_table_preview) {
/** /**
...@@ -52,28 +75,7 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit ...@@ -52,28 +75,7 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit
* (entity) Note: This property can as well be a * (entity) Note: This property can as well be a
* javascript string which evaluates to a function. * javascript string which evaluates to a function.
*/ */
/**
* @type {BottomLineConfig}
* @property {string|HTMLElement} fallback - Fallback content if none of
* the creators are applicable.
* @property {string} version - the version of the configuration which must
* match this module's version.
* @property {CreatorConfig[]} creators - an array of creators.
*/
/**
* @type {CreatorConfig}
* @property {string} [id] - a unique id for the creator. optional, for
* debuggin purposes.
* @property {function|string} is_applicable - If this is a string this has
* to be valid javascript! An asynchronous function which accepts one
* parameter, an entity in html representation, and which returns true
* iff this creator is applicable for the given entity.
* @property {string} create - This has to be valid javascript! An
* asynchronous function which accepts one parameter, an entity in html
* representation. It returns a HTMLElement or text node which will be
* shown in the bottom line container iff the creator is applicable.
*/
/** /**
* Check if an entity has a path attribute and one of a set of extensions. * Check if an entity has a path attribute and one of a set of extensions.
...@@ -547,6 +549,9 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit ...@@ -547,6 +549,9 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit
} }
/**
* @exports ext_bottom_line
*/
return { return {
previewShownEvent: previewShownEvent, previewShownEvent: previewShownEvent,
previewReadyEvent: previewReadyEvent, previewReadyEvent: previewReadyEvent,
......
...@@ -25,8 +25,6 @@ ...@@ -25,8 +25,6 @@
* form_elements module for reusable form elemenst which already have a basic * form_elements module for reusable form elemenst which already have a basic
* css styling. * css styling.
* *
* @version 0.2
*
* IMPORTANCE CONCEPTS * IMPORTANCE CONCEPTS
* *
* FIELD - an HTMLElement which wraps a LABEL element (the fields name) and the * FIELD - an HTMLElement which wraps a LABEL element (the fields name) and the
...@@ -48,24 +46,53 @@ ...@@ -48,24 +46,53 @@
* SUBFORM - an HTMLElement which contains FIELDS and other SUBFORMS. SUBFORMS * SUBFORM - an HTMLElement which contains FIELDS and other SUBFORMS. SUBFORMS
* can be used to nest FIELDS, which is not supported by HTML5 but allows only * can be used to nest FIELDS, which is not supported by HTML5 but allows only
* for flat key-value pairs. * for flat key-value pairs.
*/
/**
* The configuration for double, integer, date input elements.
*
* @typedef {object} input_config
* @property {string} name
* @property {string} type
* @property {string} label
*/
/**
* The configuration for reference_select input fields
*
* TODO
* *
* @version 0.2
* @exports form_elements
*/ */
var form_elements = new function () { var form_elements = new function () {
/**
* Config for an alert
*
* @typedef {object} AlertConfig
* @property {string} [title] - an optional title for the alert.
* @property {string} [severity="danger"] - a bootstrap class suffix. Other
* examples: warning, info
* @property {string} message - informs the user what they are about to do.
* @property {function} proceed_callback - the function which is called
* then the user hits the "Proceed" button.
* @property {function} [cancel_callback] - a callback which is called then
* the cancel button is clicked. By default, only the alert is being
* closed an nothing happens.
* @property {string} [proceed_text="Proceed"] - the text on the proceed button.
* @property {string} [cancel_text="Cancel"] - the text on the cancel button.
* @property {string} [remember_my_decision_id] - if this parameter is
* present, a checkbox is appended to the alert ("Don't ask me
* again."). If the checkbox is checked the next time the make_alert
* function is called with the same remember_my_decision_id is created,
* the alert won't show up and the proceed_callback is called without
* any user interaction.
* @property {string} [remember_my_decision_text="Don't ask me again."] -
* label text for the checkbox.
* @property {HTMLElement} [proceed_button] - an optional custom proceed
* button.
* @property {HTMLElement} [cancel_button] - an optional custom cancel
* button.
*/
/**
* The configuration for double, integer, date input elements.
*
* There are specializations of this configuration object. See
* {@link ReferenceDropDownConfig}
*
* @typedef {object} FieldConfig
* @property {string} name
* @property {string} type
* @property {string} label
* @see {@link ReferenceDropDownConfig}
*/
this.version = "0.1"; this.version = "0.1";
this.dependencies = ["log", "caosdb_utils", "markdown"]; this.dependencies = ["log", "caosdb_utils", "markdown"];
...@@ -171,33 +198,6 @@ var form_elements = new function () { ...@@ -171,33 +198,6 @@ var form_elements = new function () {
localStorage["form_elements.alert_decision." + key] = val; localStorage["form_elements.alert_decision." + key] = val;
} }
/**
* @type {AlertConfig}
* @property {string} [title] - an optional title for the alert.
* @property {string} [severity="danger"] - a bootstrap class suffix. Other
* examples: warning, info
* @property {string} message - informs the user what they are about to do.
* @property {function} proceed_callback - the function which is called
* then the user hits the "Proceed" button.
* @property {function} [cancel_callback] - a callback which is called then
* the cancel button is clicked. By default, only the alert is being
* closed an nothing happens.
* @property {string} [proceed_text="Proceed"] - the text on the proceed button.
* @property {string} [cancel_text="Cancel"] - the text on the cancel button.
* @property {string} [remember_my_decision_id] - if this parameter is
* present, a checkbox is appended to the alert ("Don't ask me
* again."). If the checkbox is checked the next time the make_alert
* function is called with the same remember_my_decision_id is created,
* the alert won't show up and the proceed_callback is called without
* any user interaction.
* @property {string} [remember_my_decision_text="Don't ask me again."] -
* label text for the checkbox.
* @property {HTMLElement} [proceed_button] - an optional custom proceed
* button.
* @property {HTMLElement] [cancel_button] - an optional custom cancel
* button.
*/
/** /**
* Make an alert, that is a dialog which can intercept a function call and * Make an alert, that is a dialog which can intercept a function call and
* asks the user to proceed or cancel. * asks the user to proceed or cancel.
...@@ -270,37 +270,39 @@ var form_elements = new function () { ...@@ -270,37 +270,39 @@ var form_elements = new function () {
return _alert[0]; return _alert[0];
} }
this.init = function () {
this.logger.trace("enter init");
}
/** /**
* (Re-)set this module's functions to standard implementation. * Return an OPTION element with entity reference.
*
* The OPTION element for a SELECT form input shows a short
* summary/description of an entity and has the entity's id as value.
*
* If the `desc` parameter is undefined, the entity_id is shown
* instead.
*
* @param {string} entity_id - the entity's id.
* @param {string} [desc] - the description for the entity.
* @returns {HTMLElement} OPTION element.
*/ */
this._init_functions = function () { this.make_reference_option = function (entity_id, desc) {
caosdb_utils.assert_string(entity_id, "param `entity_id`");
this.init = function () { if (typeof desc == "undefined") {
this.logger.trace("enter init"); desc = entity_id;
} }
var opt_str = '<option value="' + entity_id + '">' + desc +
"</option>";
return $(opt_str)[0];
}
/**
* Return an OPTION element with entity reference. /**
* * (Re-)set this module's functions to standard implementation.
* The OPTION element for a SELECT form input shows a short */
* summary/description of an entity and has the entity's id as value. this._init_functions = function () {
*
* If the `desc` parameter is undefined, the entity_id is shown
* instead.
*
* @param {string} entity_id - the entity's id.
* @param {string} [desc] - the description for the entity.
* @returns {HTMLElement} OPTION element.
*/
this.make_reference_option = function (entity_id, desc) {
caosdb_utils.assert_string(entity_id, "param `entity_id`");
if (typeof desc == "undefined") {
desc = entity_id;
}
var opt_str = '<option value="' + entity_id + '">' + desc +
"</option>";
return $(opt_str)[0];
}
/** /**
* Return SELECT form element with entity references. * Return SELECT form element with entity references.
...@@ -350,8 +352,6 @@ var form_elements = new function () { ...@@ -350,8 +352,6 @@ var form_elements = new function () {
} }
/** /**
* @typedef {option} ReferenceDropDownConfig
*
* Configuration object for a drop down menu for selecting references. * Configuration object for a drop down menu for selecting references.
* `make_reference_drop_down` generates such a drop down menu using a * `make_reference_drop_down` generates such a drop down menu using a
* SELECT input with the references as its OPTION elements. * SELECT input with the references as its OPTION elements.
...@@ -369,6 +369,10 @@ var form_elements = new function () { ...@@ -369,6 +369,10 @@ var form_elements = new function () {
* defined by `label`. If the `label` property is undefined, the `name` * defined by `label`. If the `label` property is undefined, the `name`
* is shown instead. * is shown instead.
* *
* The ReferenceDropDownConfig is a specialisation of a
* {@link FieldConfig}.
*
* @typedef {option} ReferenceDropDownConfig
* @property {string} name - The name of the select input. * @property {string} name - The name of the select input.
* @property {string} query - Query for entities. * @property {string} query - Query for entities.
* @property {function} [make_value] - Call-back for the generation of * @property {function} [make_value] - Call-back for the generation of
...@@ -383,129 +387,9 @@ var form_elements = new function () { ...@@ -383,129 +387,9 @@ var form_elements = new function () {
* undefined. This property is used by `make_form_field` to decide * undefined. This property is used by `make_form_field` to decide
* which type of field is to be generated. * which type of field is to be generated.
* *
* @see {@link FieldConfig}
*/ */
/**
* Search and retrieve entities and create a SELECT from element.
*
* @param {ReferenceDropDownConfig} config - all necessary parameters
* for the configuration.
* @returns {HTMLElement} SELECT element.
*/
this.make_reference_drop_down = 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) {
let select = $(await form_elements.make_reference_select(
entities, config.make_desc, config.make_value, config.multiple,
config.value));
select.attr("name", config.name);
loading.remove();
input_col.append(select);
form_elements.init_select_picker(ret[0], config.value);
ret[0].dispatchEvent(form_elements.field_ready_event);
select.change(function () {
ret[0].dispatchEvent(form_elements.field_changed_event);
});
}).catch(err => {
form_elements.logger.error(err);
loading.remove();
input_col.append(err);
ret[0].dispatchEvent(form_elements.field_error_event);
});
return ret.append(label, input_col)[0];
}
this.init_select_picker = function (field, value) {
caosdb_utils.assert_html_element(field, "parameter `field`");
const select = $(field).find("select")[0];
const select_picker_options = {};
if ($(select).prop("multiple")) {
select_picker_options["actionsBox"] = true;
}
if ($(select).find("option").length > 8) {
select_picker_options["liveSearch"] = true;
select_picker_options["liveSearchNormalize"] = true;
select_picker_options["liveSearchPlaceholder"] = "search...";
}
$(select).selectpicker(select_picker_options);
$(select).selectpicker("val", value);
this.init_actions_box(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) {
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>
</div>
</div>`)
.hide();
select
.siblings(".dropdown-menu")
.prepend(actions_box);
field.addEventListener(
form_elements.field_changed_event.type,
(e) => {
if (form_elements.is_set(field)) {
actions_box.show();
} else {
actions_box.hide();
}
}, true);
actions_box
.find(".bs-deselect-all")
.click((e) => {
select.val(null)
.selectpicker("render")
.parent().toggleClass("open", false);
select[0].dispatchEvent(form_elements.field_changed_event);
});
}
}
/**
* Return a promise which resolves with the field when the field is ready.
*
* This function is especially useful if the caller can not be sure if
* the field_ready_event has been dispatched already and the field is
* ready or if the fields creation is still pending.
*
* @param {HTMLElement} field
* @return {Promise} the field-ready promise
*/
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) {
try {
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,
(e) => resolve(e.target), true);
}
} catch (err) {
reject(err);
}
});
}
this._query = async function (q) { this._query = async function (q) {
const result = await query(q); const result = await query(q);
this.logger.debug("query returned", result); this.logger.debug("query returned", result);
...@@ -527,802 +411,978 @@ var form_elements = new function () { ...@@ -527,802 +411,978 @@ var form_elements = new function () {
return this.parse_script_result(result); return this.parse_script_result(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;
const call = result.evaluate("call", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; /**
const stderr = result.evaluate("stderr", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; * @typedef {object} ScriptingResult
const stdout = result.evaluate("stdout", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; * @property {string} code
* @property {string} call
* @property {string} stdout
* @property {string} stderr
*/
return { /**
"code": code, * Bla, TODO
"call": call, *
"stdout": stdout, * @param {XMLDocument} result
"stderr": stderr * @return {ScriptingResult}
}; */
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;
const call = result.evaluate("call", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue;
const stderr = result.evaluate("stderr", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue;
const stdout = result.evaluate("stdout", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue;
const ret = {
"code": code,
"call": call,
"stdout": stdout,
"stderr": stderr
};
return ret;
}
/**
* Search and retrieve entities and create a SELECT from element.
*
* @param {ReferenceDropDownConfig} config - all necessary parameters
* for the configuration.
* @returns {HTMLElement} SELECT element.
*/
this.make_reference_drop_down = 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) {
let select = $(await form_elements.make_reference_select(
entities, config.make_desc, config.make_value, config.multiple,
config.value));
select.attr("name", config.name);
loading.remove();
input_col.append(select);
form_elements.init_select_picker(ret[0], config.value);
ret[0].dispatchEvent(form_elements.field_ready_event);
select.change(function () {
ret[0].dispatchEvent(form_elements.field_changed_event);
});
}).catch(err => {
form_elements.logger.error(err);
loading.remove();
input_col.append(err);
ret[0].dispatchEvent(form_elements.field_error_event);
});
return ret.append(label, input_col)[0];
}
/**
* Test 16
*/
this.init_select_picker = function (field, value) {
caosdb_utils.assert_html_element(field, "parameter `field`");
const select = $(field).find("select")[0];
const select_picker_options = {};
if ($(select).prop("multiple")) {
select_picker_options["actionsBox"] = true;
} }
if ($(select).find("option").length > 8) {
select_picker_options["liveSearch"] = true;
select_picker_options["liveSearchNormalize"] = true;
select_picker_options["liveSearchPlaceholder"] = "search...";
}
$(select).selectpicker(select_picker_options);
$(select).selectpicker("val", value);
this.init_actions_box(field);
}
/**
* generate a java script object representation of a form
*/
this.form_to_object = function (form) {
this.logger.trace("entity form_to_json", form);
caosdb_utils.assert_html_element(form, "parameter `form`");
const _to_json = (element, data) => { /**
this.logger.trace("enter element_to_json", element, data); * Test 17
*/
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) {
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>
</div>
</div>`)
.hide();
select
.siblings(".dropdown-menu")
.prepend(actions_box);
field.addEventListener(
form_elements.field_changed_event.type,
(e) => {
if (form_elements.is_set(field)) {
actions_box.show();
} else {
actions_box.hide();
}
}, true);
for (const child of element.children) { actions_box
// ignore disabled fields and subforms .find(".bs-deselect-all")
if ($(child).hasClass("caosdb-f-field-disabled")) { .click((e) => {
continue; select.val(null)
.selectpicker("render")
.parent().toggleClass("open", false);
select[0].dispatchEvent(form_elements.field_changed_event);
});
}
}
/**
* Return a promise which resolves with the field when the field is ready.
*
* This function is especially useful if the caller can not be sure if
* the field_ready_event has been dispatched already and the field is
* ready or if the fields creation is still pending.
*
* @param {HTMLElement} field
* @return {Promise} the field-ready promise
*/
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) {
try {
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,
(e) => resolve(e.target), true);
}
} catch (err) {
reject(err);
}
});
}
/**
* generate a java script object representation of a form
*
* @function
*/
this.form_to_object = function (form) {
this.logger.trace("entity form_to_json", form);
caosdb_utils.assert_html_element(form, "parameter `form`");
const _to_json = (element, data) => {
this.logger.trace("enter element_to_json", element, data);
for (const child of element.children) {
// ignore disabled fields and subforms
if ($(child).hasClass("caosdb-f-field-disabled")) {
continue;
}
const name = $(child).attr("name");
const is_subform = $(child).hasClass("caosdb-f-form-elements-subform");
if (is_subform) {
const subform = $(child).data("subform-name");
// recursive
var subform_obj = _to_json(child, {});
if (typeof data[subform] === "undefined") {
data[subform] = subform_obj;
} else if (Array.isArray(data[subform])) {
data[subform].push(subform_obj);
} else {
data[subform] = [data[subform], subform_obj]
} }
const name = $(child).attr("name"); } else if (name && name !== "") {
const is_subform = $(child).hasClass("caosdb-f-form-elements-subform"); // input elements
if (is_subform) { const not_checkbox = !$(child).is(":checkbox");
const subform = $(child).data("subform-name"); if (not_checkbox || $(child).is(":checked")) {
// recursive // checked or not a checkbox
var subform_obj = _to_json(child, {}); var value = $(child).val();
if (typeof data[subform] === "undefined") { if (typeof data[name] === "undefined") {
data[subform] = subform_obj; data[name] = value;
} else if (Array.isArray(data[subform])) { } else if (Array.isArray(data[name])) {
data[subform].push(subform_obj); data[name].push(value);
} else {
data[subform] = [data[subform], subform_obj]
}
} else if (name && name !== "") {
// input elements
const not_checkbox = !$(child).is(":checkbox");
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])) {
data[name].push(value);
} else {
data[name] = [data[name], value]
}
} else { } else {
// TODO checkbox data[name] = [data[name], value]
} }
} else if (child.children.length > 0) { } else {
// recursive // TODO checkbox
_to_json(child, data);
} }
} else if (child.children.length > 0) {
// recursive
_to_json(child, data);
} }
}
this.logger.trace("leave element_to_json", element, data); this.logger.trace("leave element_to_json", element, data);
return data; return data;
}; };
const ret = _to_json(form, {});
this.logger.trace("leave form_to_json", ret);
return ret;
}
this.make_submit_button = function () { const ret = _to_json(form, {});
var ret = $('<button class="caosdb-f-form-elements-submit-button btn btn-primary" type="submit">Submit</button>'); this.logger.trace("leave form_to_json", ret);
return ret[0]; return ret;
} }
this.make_cancel_button = function (form) { this.make_submit_button = function () {
var ret = $('<button class="caosdb-f-form-elements-cancel-button btn btn-primary" type="button">Cancel</button>'); var ret = $('<button class="caosdb-f-form-elements-submit-button btn btn-primary" type="submit">Submit</button>');
ret.on("click", e => { return ret[0];
this.logger.debug("cancel form", e, form); }
form.dispatchEvent(this.cancel_form_event);
});
return ret[0];
}
/** this.make_cancel_button = function (form) {
* TODO make syncronous var ret = $('<button class="caosdb-f-form-elements-cancel-button btn btn-primary" type="button">Cancel</button>');
*/ ret.on("click", e => {
this.make_form_field = async function (config) { this.logger.debug("cancel form", e, form);
caosdb_utils.assert_type(config, "object", "param `config`"); form.dispatchEvent(this.cancel_form_event);
caosdb_utils.assert_string(config.type, "`config.type` of param `config`"); });
return ret[0];
var field = undefined; }
const type = config.type;
if (type === "date") {
field = this.make_date_input(config);
} else if (type === "checkbox") {
field = this.make_checkbox_input(config);
} else if (type === "text") {
field = this.make_text_input(config);
} else if (type === "double") {
field = this.make_double_input(config);
} else if (type === "integer") {
field = this.make_integer_input(config);
} else if (type === "range") {
field = await this.make_range_input(config);
} else if (type === "reference_drop_down") {
field = this.make_reference_drop_down(config);
} else if (type === "subform") {
// TODO handle cache and required for subforms
return await this.make_subform(config);
} else {
throw new TypeError("undefined field type `" + type + "`");
}
if (config.required) { /**
this.set_required(field); * TODO make syncronous
} *
if (config.cached) { * @return {HTMLElement}
this.set_cached(field); */
} this.make_form_field = async function (config) {
if (config.help) { caosdb_utils.assert_type(config, "object", "param `config`");
this.add_help(field, config.help); caosdb_utils.assert_string(config.type, "`config.type` of param `config`");
}
var field = undefined;
const type = config.type;
if (type === "date") {
field = this.make_date_input(config);
} else if (type === "checkbox") {
field = this.make_checkbox_input(config);
} else if (type === "text") {
field = this.make_text_input(config);
} else if (type === "double") {
field = this.make_double_input(config);
} else if (type === "integer") {
field = this.make_integer_input(config);
} else if (type === "range") {
field = await this.make_range_input(config);
} else if (type === "reference_drop_down") {
field = this.make_reference_drop_down(config);
} else if (type === "subform") {
// TODO handle cache and required for subforms
return await this.make_subform(config);
} else {
throw new TypeError("undefined field type `" + type + "`");
}
return field; if (config.required) {
this.set_required(field);
}
if (config.cached) {
this.set_cached(field);
}
if (config.help) {
this.add_help(field, config.help);
} }
return field;
}
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"
});
if (typeof config === "string" || config instanceof String) { this.add_help = function (field, config) {
help_button.attr("data-content", config); var help_button = $('<span data-trigger="click focus" data-toggle="popover" class="caosdb-f-form-help pull-right glyphicon glyphicon-info-sign"/>')
help_button.popover(); .css({
} else { "cursor": "pointer"
help_button.popover(config); });
}
if (typeof config === "string" || config instanceof String) {
help_button.attr("data-content", config);
help_button.popover();
} else {
help_button.popover(config);
}
var label = $(field).children("label");
if (label.length > 0) { var label = $(field).children("label");
help_button.css({ if (label.length > 0) {
"margin-left": "4px" help_button.css({
}); "margin-left": "4px"
label.first().append(help_button); });
} else { label.first().append(help_button);
$(field).append(help_button); } else {
} $(field).append(help_button);
} }
}
this.make_heading = function (config) { this.make_heading = function (config) {
if (typeof config.header === "undefined") { if (typeof config.header === "undefined") {
return; return;
} else if (typeof config.header === "string" || config.header instanceof String) { } else if (typeof config.header === "string" || config.header instanceof String) {
return $('<div class="caosdb-f-form-header h3">' + config.header + '</div>')[0]; return $('<div class="caosdb-f-form-header h3">' + config.header + '</div>')[0];
}
caosdb_utils.assert_html_element(config.header, "member `header` of parameter `config`");
return config.header;
} }
caosdb_utils.assert_html_element(config.header, "member `header` of parameter `config`");
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 wrapper = $('<div class="caosdb-f-form-wrapper"/>');
var header = this.make_heading(config); var header = this.make_heading(config);
wrapper.append(header); wrapper.append(header);
var loading = $('<div>loading...</div>'); var loading = $('<div>loading...</div>');
var logger = this.logger; var logger = this.logger;
var cancel = (e) => { var cancel = (e) => {
logger.trace("cancel form", e); logger.trace("cancel form", e);
wrapper.remove(); wrapper.remove();
}; };
wrapper.append(loading); wrapper.append(loading);
Promise.resolve(form).then(form => { Promise.resolve(form).then(form => {
// form ready // form ready
loading.remove(); loading.remove();
wrapper.append(form); wrapper.append(form);
wrapper[0].dispatchEvent(this.form_ready_event); wrapper[0].dispatchEvent(this.form_ready_event);
}).catch(err => { }).catch(err => {
logger.error("form loading error", err); logger.error("form loading error", err);
loading.remove(); loading.remove();
wrapper.append(err); wrapper.append(err);
}); });
wrapper[0].addEventListener(this.cancel_form_event.type, cancel, true); wrapper[0].addEventListener(this.cancel_form_event.type, cancel, true);
return wrapper[0]; return wrapper[0];
} }
this.make_form = function (config) { /**
var form = undefined; * Configuration objects which are passed to {@link make_form}.
*
* Note: either the `script` or the `name` property must be defined. If the former is defined, the latter will be overriden.
*
* @typedef {object} FormConfig
*
* @property {FieldConfig[]} fields - array of fields. The order is the
* order in which they appear in the resulting form.
* @property {string} [script] - if present the form will call a
* server-side script on submission.
* @property {string} [name] - The name of the form. This is being
* overridden by the `script` parameter if present.
* @property {function} [submit] - a callback which handles the submission
* of the form. This parameter is being overridden if the `script`
* parameter is present.
*/
if (config.script) { /**
form = this.make_script_form(config, config.script); * Create a form.
} else { *
form = this.make_generic_form(config); * The returned element is a container which will eventually contain a HTML
} * form element. The container emits a {@link form_ready_event} when the
var wrapper = this.make_form_wrapper(form, config); * form is ready.
return wrapper; *
} * @param {FormConfig} config
* @return {HTMLElement}
*/
this.make_form = function (config) {
var form = undefined;
/** if (config.script) {
* TODO make syncronous form = this.make_script_form(config, config.script);
*/ } else {
this.make_subform = async function (config) { form = this.make_generic_form(config);
this.logger.trace("enter make_subform"); }
caosdb_utils.assert_type(config, "object", "param `config`"); var wrapper = this.make_form_wrapper(form, config);
caosdb_utils.assert_string(config.name, "`config.name` of param `config`"); return wrapper;
caosdb_utils.assert_array(config.fields, "`config.fields` of param `config`"); }
const name = config.name; /**
var form = $('<div data-subform-name="' + name + '" class="caosdb-f-form-elements-subform"/>'); * 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`");
caosdb_utils.assert_array(config.fields, "`config.fields` of param `config`");
const name = config.name;
var form = $('<div data-subform-name="' + name + '" class="caosdb-f-form-elements-subform"/>');
for (let field of config.fields) {
this.logger.trace("add subform field", field);
let elem = await this.make_form_field(field);
form.append(elem);
}
for (let field of config.fields) { this.logger.trace("leave make_subform", form[0]);
this.logger.trace("add subform field", field); return form[0];
let elem = await this.make_form_field(field); }
form.append(elem);
}
this.logger.trace("leave make_subform", form[0]); this.dismiss_form = function (form) {
return form[0]; if (form.tagName === "FORM") {
form.dispatchEvent(this.cancel_form_event);
} }
var _form = $(form).find("form");
this.dismiss_form = function (form) { if (_form.length > 0) {
if (form.tagName === "FORM") { _form[0].dispatchEvent(this.cancel_form_event);
form.dispatchEvent(this.cancel_form_event);
}
var _form = $(form).find("form");
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.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.disable_fields(this.get_group_fields(form, group));
} }
this.get_group_fields = function (form, group) { this.get_group_fields = function (form, group) {
return $(form).find(".caosdb-f-field[data-groups*='(" + group + ")']").toArray(); return $(form).find(".caosdb-f-field[data-groups*='(" + group + ")']").toArray();
} }
/** /**
* Return an array of field with name * Return an array of field with name
* *
* @param {string} name - the field name * @param {string} name - the field name
* @return {HTMLElement[]} array of fields * @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_html_element(form, "parameter `form`");
caosdb_utils.assert_string(name, "parameter `name`"); caosdb_utils.assert_string(name, "parameter `name`");
return $(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray(); 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); 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); $(field).attr("data-groups", groups);
} }
this.disable_fields = function (fields) { this.disable_fields = function (fields) {
$(fields).toggleClass("caosdb-f-field-disabled", true).hide(); $(fields).toggleClass("caosdb-f-field-disabled", true).hide();
for (const field of $(fields)) { for (const field of $(fields)) {
field.dispatchEvent(this.field_disabled_event); field.dispatchEvent(this.field_disabled_event);
}
} }
}
this.enable_fields = function (fields) { this.enable_fields = function (fields) {
$(fields).toggleClass("caosdb-f-field-disabled", false).show(); $(fields).toggleClass("caosdb-f-field-disabled", false).show();
for (const field of $(fields)) { for (const field of $(fields)) {
field.dispatchEvent(this.field_enabled_event); 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.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.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"); this.logger.trace("enter make_script_form");
const submit_callback = async function (form) { const submit_callback = async function (form) {
form = $(form); form = $(form);
// actually submit the form // actually submit the form
var response = await form_elements._run_script(script, form); var response = await form_elements._run_script(script, form);
var result = []; var result = [];
if (response.code === "0") { if (response.code === "0") {
// handle success // handle success
result.push(form_elements.make_success_message(response.stdout)); result.push(form_elements.make_success_message(response.stdout));
return result; return result;
} else { } else {
// handle scripting error // handle scripting error
result.push(form_elements.make_error_message(response.call)); result.push(form_elements.make_error_message(response.call));
result.push(form_elements.make_error_message(response.stderr)); result.push(form_elements.make_error_message(response.stderr));
throw result; throw result;
} }
}; };
this.logger.trace("leave make_script_form");
const new_config = $.extend({}, {
name: script,
submit: submit_callback
}, config);
return await this.make_generic_form(new_config);
}
this.logger.trace("leave make_script_form"); /**
const new_config = $.extend({}, { * Return a generic form, bind the config.submit to the submit event
name: script, * of the form.
submit: submit_callback *
}, config); * The `config.fields` array may contain `form_elements.field_config`
return await this.make_generic_form(new_config); * objects or HTMLElements.
} *
* TODO
*/
this.make_generic_form = async function (config) {
this.logger.trace("enter make_generic_form");
/** caosdb_utils.assert_type(config, "object", "param `config`");
* Return a generic form, bind the config.submit to the submit event caosdb_utils.assert_string(config.name, "`config.name` of param `config`", true);
* of the form. caosdb_utils.assert_array(config.fields, "`config.fields` of param `config`");
*
* The `config.fields` array may contain `form_elements.field_config`
* objects or HTMLElements.
*
* TODO
*/
this.make_generic_form = async function (config) {
this.logger.trace("enter make_generic_form");
caosdb_utils.assert_type(config, "object", "param `config`"); const form = $('<form class="form-horizontal" action="#" method="post" />');
caosdb_utils.assert_string(config.name, "`config.name` of param `config`", true);
caosdb_utils.assert_array(config.fields, "`config.fields` of param `config`");
const form = $('<form class="form-horizontal" action="#" method="post" />'); // set name
if (config.name) {
form.attr("name", config.name);
}
// set name // add fields
if (config.name) { for (let field of config.fields) {
form.attr("name", config.name); this.logger.trace("add field", field);
if (field instanceof HTMLElement) {
form.append(field);
} else {
let elem = await this.make_form_field(field);
form.append(elem);
} }
}
// set groups
if (config.groups) {
for (let group of config.groups) {
this.logger.trace("add group", group);
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)
// add fields }
for (let field of config.fields) { // disable if necessary
this.logger.trace("add field", field); if (typeof group.enabled === "undefined" || group.enabled) {
if (field instanceof HTMLElement) { this.enable_group(form, group.name);
form.append(field);
} else { } else {
let elem = await this.make_form_field(field); this.disable_group(form, group.name);
form.append(elem);
} }
} }
}
// set groups const footer = this.make_footer();
if (config.groups) { form.append(footer);
for (let group of config.groups) {
this.logger.trace("add group", group);
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)
} if (!(typeof config.submit === 'boolean' && config.submit === false)) {
// disable if necessary // add submit button unless config.submit is false
if (typeof group.enabled === "undefined" || group.enabled) { footer.append(this.make_submit_button());
this.enable_group(form, group.name); }
} else { form[0].addEventListener("submit", (e) => {
this.disable_group(form, group.name); e.preventDefault();
} e.stopPropagation();
} if (form.find(".caosdb-f-form-submitting").length > 0) {
// do not submit twice
return;
} }
const footer = this.make_footer(); this.logger.debug("submit form", e);
form.append(footer);
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) {
// do not submit twice
return;
}
this.logger.debug("submit form", e); form[0].dispatchEvent(this.submit_form_event);
form[0].dispatchEvent(this.submit_form_event); form.find(":input").prop("disabled", true);
var submitting = form_elements.make_submitting_info();
form.find(".caosdb-f-form-elements-footer").before(submitting);
form.find(":input").prop("disabled", true);
var submitting = form_elements.make_submitting_info();
form.find(".caosdb-f-form-elements-footer").before(submitting);
form[0].addEventListener(this.form_success_event.type, (e) => {
submitting.remove();
}, true);
form[0].addEventListener(this.form_error_event.type, (e) => {
submitting.remove();
}, true);
form[0].addEventListener(this.form_success_event.type, (e) => {
submitting.remove();
}, true);
form[0].addEventListener(this.form_error_event.type, (e) => {
submitting.remove();
}, true);
// remove old messages
const error_handler = config.error;
const success_handler = config.success;
const submit_callback = config.submit;
form.find(".caosdb-f-form-elements-message").remove();
if (typeof config.submit === "function") {
// wrap callback in async function
const _wrap_callback = async function () {
try {
var results = await submit_callback(form[0]);
// remove old messages // success_handler
const error_handler = config.error; if (typeof success_handler === "function") {
const success_handler = config.success; var processed = await success_handler(form[0], results);
const submit_callback = config.submit; if (typeof processed !== "undefined") {
form.find(".caosdb-f-form-elements-message").remove(); form_elements.show_results(form[0], processed);
if (typeof config.submit === "function") {
// wrap callback in async function
const _wrap_callback = async function () {
try {
var results = await submit_callback(form[0]);
// success_handler
if (typeof success_handler === "function") {
var processed = await success_handler(form[0], results);
if (typeof processed !== "undefined") {
form_elements.show_results(form[0], processed);
}
} else {
form_elements.show_results(form[0], results);
} }
} else {
form_elements.show_results(form[0], results);
}
form[0].dispatchEvent(form_elements.form_success_event); form[0].dispatchEvent(form_elements.form_success_event);
} catch (err) { } catch (err) {
// error_handler
if (typeof error_handler === "function") {
var processed = await error_handler(form[0], err);
if (typeof processed !== "undefined") {
form_elements.show_results(form[0], processed);
}
} else {
form_elements.show_errors(form[0], err);
}
form[0].dispatchEvent(form_elements.form_error_event); // error_handler
if (typeof error_handler === "function") {
var processed = await error_handler(form[0], err);
if (typeof processed !== "undefined") {
form_elements.show_results(form[0], processed);
}
} else {
form_elements.show_errors(form[0], err);
} }
}(); form[0].dispatchEvent(form_elements.form_error_event);
} }
return false;
}();
}
return false;
}, true);
form[0].addEventListener(this.form_success_event.type, function (e) { }, true);
// 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) {
// reenable inputs
form.find(":input").prop("disabled", false);
}, true);
// add cancel button form[0].addEventListener(this.form_success_event.type, function (e) {
$(footer).append(this.make_cancel_button(form[0])); // 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) {
// reenable inputs
form.find(":input").prop("disabled", false);
}, true);
// init caching for this form // add cancel button
form_elements.init_form_caching(config, form[0]); $(footer).append(this.make_cancel_button(form[0]));
// init validation // init caching for this form
form_elements.init_validator(form[0]); form_elements.init_form_caching(config, form[0]);
this.logger.trace("leave make_generic_form"); // init validation
return form[0]; form_elements.init_validator(form[0]);
}
this.init_form_caching = function (config, form) { this.logger.trace("leave make_generic_form");
var default_config = { return form[0];
"cache_event": form_elements.submit_form_event.type, }
"cache_storage": localStorage
};
var lconfig = $.extend({}, default_config, config);
this.logger.trace("init_form_caching", lconfig, form); this.init_form_caching = function (config, form) {
var default_config = {
"cache_event": form_elements.submit_form_event.type,
"cache_storage": localStorage
};
var lconfig = $.extend({}, default_config, config);
form.addEventListener(lconfig.cache_event, (e) => { this.logger.trace("init_form_caching", lconfig, form);
form_elements.cache_form(lconfig.cache_storage, form);
}, true);
form_elements.load_cached(lconfig.cache_storage, form);
}
this.show_results = function (form, results) { form.addEventListener(lconfig.cache_event, (e) => {
$(form).append(results); form_elements.cache_form(lconfig.cache_storage, form);
} }, true);
form_elements.load_cached(lconfig.cache_storage, form);
}
this.show_errors = function (form, errors) { this.show_results = function (form, results) {
$(form).append(errors); $(form).append(results);
} }
this.make_footer = function () { this.show_errors = function (form, errors) {
return $('<div class="text-right caosdb-f-form-elements-footer"/>') $(form).append(errors);
.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_footer = function () {
return this.make_message(message, "error"); 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_success_message = function (message) { this.make_error_message = function (message) {
return this.make_message(message, "success"); return this.make_message(message, "error");
} }
this.make_submitting_info = function () { this.make_success_message = function (message) {
// TODO styling return this.make_message(message, "success");
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];
}
this.make_message = function (message, type) { this.make_submitting_info = function () {
var ret = $('<div class="caosdb-f-form-elements-message"/>'); // TODO styling
if (type) { return $(this.make_message("Submitting... please wait. This might take some time.", "info"))
ret.addClass("caosdb-f-form-elements-message-" + type); .toggleClass("h3", true)
} .toggleClass("caosdb-f-form-submitting", true)
return ret.append(markdown.textToHtml(message))[0]; .toggleClass("text-right", true)[0];
}
this.make_message = function (message, type) {
var ret = $('<div class="caosdb-f-form-elements-message"/>');
if (type) {
ret.addClass("caosdb-f-form-elements-message-" + type);
} }
return ret.append(markdown.textToHtml(message))[0];
}
/** /**
* TODO make syncronous * TODO make syncronous
*/ */
this.make_range_input = async function (config) { this.make_range_input = async function (config) {
// TODO // TODO
// 1. wrapp both inputs to separate it from the label into a container // 1. wrapp both inputs to separate it from the label into a container
// 2. make two rows for each input // 2. make two rows for each input
// 3. make inline-block for all included elements // 3. make inline-block for all included elements
const from_config = $.extend({}, { const from_config = $.extend({}, {
cached: config.cached, cached: config.cached,
required: config.required, required: config.required,
type: "double" type: "double"
}, config.from); }, config.from);
const to_config = $.extend({}, { const to_config = $.extend({}, {
cached: config.cached, cached: config.cached,
required: config.required, required: config.required,
type: "double" type: "double"
}, config.to); }, config.to);
const from_input = await this.make_form_field(from_config); const from_input = await this.make_form_field(from_config);
const to_input = await this.make_form_field(to_config); const to_input = await this.make_form_field(to_config);
const ret = $(this._make_field_wrapper(config.name)); const ret = $(this._make_field_wrapper(config.name));
if (config.label) { if (config.label) {
ret.append(this._make_input_label_str(config)); ret.append(this._make_input_label_str(config));
} }
ret.append(from_input); ret.append(from_input);
ret.append(to_input); ret.append(to_input);
// styling // styling
$(from_input).toggleClass("form-group", false); $(from_input).toggleClass("form-group", false);
$(from_input).find(".col-sm-3").toggleClass("col-sm-3", false).toggleClass("col-sm-1"); $(from_input).find(".col-sm-3").toggleClass("col-sm-3", false).toggleClass("col-sm-1");
$(from_input).find(".col-sm-9").toggleClass("col-sm-9", false).toggleClass("col-sm-3"); $(from_input).find(".col-sm-9").toggleClass("col-sm-9", false).toggleClass("col-sm-3");
$(to_input).toggleClass("form-group", false); $(to_input).toggleClass("form-group", false);
$(to_input).find(".col-sm-3").toggleClass("col-sm-3", false).toggleClass("col-sm-1").toggleClass("col-sm-offset-1"); $(to_input).find(".col-sm-3").toggleClass("col-sm-3", false).toggleClass("col-sm-1").toggleClass("col-sm-offset-1");
$(to_input).find(".col-sm-9").toggleClass("col-sm-9", false).toggleClass("col-sm-3"); $(to_input).find(".col-sm-9").toggleClass("col-sm-9", false).toggleClass("col-sm-3");
return ret[0]; return ret[0];
} }
/** /**
* Return a DIV with class `caosdb-f-field` and a data attribute * Return a DIV with class `caosdb-f-field` and a data attribute
* `data-field-name` which contains the name. * `data-field-name` which contains the name.
* *
* The DIV is used to wrap LABEL and INPUT elements of a form together. * The DIV is used to wrap LABEL and INPUT elements of a form together.
* *
* @param {string} name - the name of the field. * @param {string} name - the name of the field.
* @returns {HTMLElement} a DIV. * @returns {HTMLElement} a DIV.
*/ */
this._make_field_wrapper = function (name) { this._make_field_wrapper = function (name) {
caosdb_utils.assert_string(name, "param `name`"); caosdb_utils.assert_string(name, "param `name`");
return $('<div class="form-group caosdb-f-field" data-field-name="' + name + '" />') return $('<div class="form-group caosdb-f-field" data-field-name="' + name + '" />')
.css({"padding": "0"})[0]; .css({"padding": "0"})[0];
} }
this.make_date_input = function (config) { this.make_date_input = function (config) {
return this._make_input(config); return this._make_input(config);
} }
this.make_text_input = function (config) { this.make_text_input = function (config) {
return this._make_input(config); return this._make_input(config);
} }
/** /**
* Return an input field which accepts double values. * Return an input field which accepts double values.
* *
* `config.type` is set to "number" and overrides any other type. * `config.type` is set to "number" and overrides any other type.
* *
* @param {form_elements.input_config} config. * @param {form_elements.input_config} config.
* @returns {HTMLElement} a double form field. * @returns {HTMLElement} a double form field.
*/ */
this.make_double_input = function (config) { this.make_double_input = function (config) {
var clone = $.extend({}, config, { var clone = $.extend({}, config, {
type: "number" type: "number"
}); });
var ret = $(this._make_input(clone)) var ret = $(this._make_input(clone))
ret.find("input").attr("step", "any"); ret.find("input").attr("step", "any");
return ret[0]; return ret[0];
} }
/** /**
* Return an input field which accepts integers. * Return an input field which accepts integers.
* *
* `config.type` is set to "number" and overrides any other type. * `config.type` is set to "number" and overrides any other type.
* *
* @param {form_elements.input_config} config. * @param {form_elements.input_config} config.
* @returns {HTMLElement} an integer form field. * @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)); var ret = $(this.make_double_input(config));
ret.find("input").attr("step", "1"); ret.find("input").attr("step", "1");
return ret[0]; return ret[0];
} }
/** /**
* Return a checkbox input field. * Return a checkbox input field.
* *
* @param {form_elements.checkbox_config} config. * @param {form_elements.checkbox_config} config.
* @returns {HTMLElement} a checkbox form field. * @returns {HTMLElement} a checkbox form field.
*/ */
this.make_checkbox_input = function (config) { this.make_checkbox_input = function (config) {
var clone = $.extend({}, config, { var clone = $.extend({}, config, {
type: "checkbox" type: "checkbox"
}); });
var ret = $(this._make_input(clone)); var ret = $(this._make_input(clone));
ret.find("input:checkbox").prop("checked", false); ret.find("input:checkbox").prop("checked", false);
ret.find("input:checkbox").toggleClass("form-control", 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").prop("checked", true);
ret.find("input:checkbox").attr("checked", "checked"); ret.find("input:checkbox").attr("checked", "checked");
} }
if (config.value) { if (config.value) {
ret.find("input:checkbox").attr("value", config.value); ret.find("input:checkbox").attr("value", config.value);
}
return ret[0];
} }
return ret[0];
}
/** /**
* Add `caosdb-f-form-field-required` class to form field. * Add `caosdb-f-form-field-required` class to form field.
* *
* @param {HTMLElement} field - the required form field. * @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).toggleClass("caosdb-f-form-field-required", true);
$(field).find(":input").prop("required", true); $(field).find(":input").prop("required", true);
$(field).find("label").prepend(this.make_required_marker()); $(field).find("label").prepend(this.make_required_marker());
} }
/** /**
* Return a span which is to be inserted before a field's label text * Return a span which is to be inserted before a field's label text
* and which marks that field as required. * and which marks that field as required.
* *
* @returns {HTMLElement} span element. * @returns {HTMLElement} span element.
*/ */
this.make_required_marker = function () { this.make_required_marker = function () {
// TODO create class and move to css file // TODO create class and move to css file
return $('<span>*</span>') return $('<span>*</span>')
.css({ .css({
"font-size": "10px", "font-size": "10px",
"color": "red", "color": "red",
"margin-right": "4px", "margin-right": "4px",
"font-weight": "100", "font-weight": "100",
})[0]; })[0];
} }
this.get_enabled_required_fields = function (form) { this.get_enabled_required_fields = function (form) {
return $(this.get_enabled_fields(form)) return $(this.get_enabled_fields(form))
.filter(".caosdb-f-form-field-required") .filter(".caosdb-f-form-field-required")
.toArray(); .toArray();
} }
this.get_enabled_fields = function (form) { this.get_enabled_fields = function (form) {
return $(form) return $(form)
.find(".caosdb-f-field") .find(".caosdb-f-field")
.filter(function (idx) { .filter(function (idx) {
// remove disabled fields from results // remove disabled fields from results
return !$(this).hasClass("caosdb-f-field-disabled"); return !$(this).hasClass("caosdb-f-field-disabled");
}) })
.toArray(); .toArray();
} }
this.all_required_fields_set = function (form) { this.all_required_fields_set = function (form) {
const req = form_elements.get_enabled_required_fields(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)) { if (!form_elements.is_set(field)) {
return false; return false;
}
} }
return true;
} }
return true;
}
/** /**
* @param {HTMLElement} form - the form be validated. * @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); 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 // TODO do not change the submit button directly. change the
// `submittable` state of the form and handle the case where a form // `submittable` state of the form and handle the case where a form
// is submitting when this function is called. // is submitting when this function is called.
if (form_elements.is_valid(form)) { if (form_elements.is_valid(form)) {
$(submit).prop("disabled", false); $(submit).prop("disabled", false);
} else { } else {
$(submit).prop("disabled", true); $(submit).prop("disabled", true);
}
} }
}
this.init_validator = function (form) { this.init_validator = function (form) {
const submit = $(form).find(":input[type='submit']")[0]; 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.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("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); form.addEventListener("input", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true);
}
} }
}
/** /**
* Return an input and a label, wrapped in a div with class * Return an input and a label, wrapped in a div with class
* `caosdb-f-field`. * `caosdb-f-field`.
* *
* @param {object} config - config object with `name`, `type` and * @param {object} config - config object with `name`, `type` and
* optional `label` * optional `label`
* @returns {HTMLElement} a form field. * @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"); caosdb_utils.assert_string(config.name, "the name of a form field");
let ret = $(this._make_field_wrapper(config.name)); let ret = $(this._make_field_wrapper(config.name));
let name = config.name; let name = config.name;
let label = this._make_input_label_str(config); let label = this._make_input_label_str(config);
let type = config.type || "text"; let type = config.type || "text";
let value = config.value; let value = config.value;
let input = $('<input class="form-control caosdb-f-property-single-raw-value" type="' + type + let input = $('<input class="form-control caosdb-f-property-single-raw-value" type="' + type +
'" name="' + name + '" name="' + name +
'" />'); '" />');
input.change(function () { input.change(function () {
ret[0].dispatchEvent(form_elements.field_changed_event); ret[0].dispatchEvent(form_elements.field_changed_event);
}); });
let input_col = $('<div class="caosdb-f-property-value col-sm-9"/>'); let input_col = $('<div class="caosdb-f-property-value col-sm-9"/>');
input_col.append(input); input_col.append(input);
if (value) { if (value) {
input.val(value); input.val(value);
}
return ret.append(label, input_col)[0];
}
/**
* Return a string representation of a LABEL element, ready for parsing.
*
* This function is used by other functions to generate a LABEL element.
*
* The config's `name` goes to the `for` attribute, the `label` is the
* text node of the resulting LABEL element.
*
* @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) {
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 ret.append(label, input_col)[0];
}
/**
* Return a string representation of a LABEL element, ready for parsing.
*
* This function is used by other functions to generate a LABEL element.
*
* The config's `name` goes to the `for` attribute, the `label` is the
* text node of the resulting LABEL element.
*
* @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) {
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>' : "";
} }
this._init_functions(); this._init_functions();
} }
......
...@@ -33,8 +33,9 @@ BUILDDIR = ../../build/doc ...@@ -33,8 +33,9 @@ BUILDDIR = ../../build/doc
# npm is not always in the global PATH # npm is not always in the global PATH
NPM_PATH = $(shell npm bin) NPM_PATH = $(shell npm bin)
NPM_PREFIX = $(shell npm prefix)
.PHONY: doc-help Makefile apidoc .PHONY: doc-help Makefile api
# Put it first so that "make" without argument is like "make help". # Put it first so that "make" without argument is like "make help".
doc-help: doc-help:
...@@ -42,10 +43,9 @@ doc-help: ...@@ -42,10 +43,9 @@ doc-help:
# Catch-all target: route all unknown targets to Sphinx using the new # Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile %: Makefile api
PATH=$(NPM_PATH):$$PATH $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) PATH=$(NPM_PATH):$$PATH $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
# sphinx-build -M html . ../../build/doc # sphinx-build -M html . ../../build/doc
# Not necessary in this repository, apidoc is doone with the sphinx-autoapi extension api:
# apidoc: PATH=$(NPM_PATH):$$PATH jsdoc -t $(NPM_PREFIX)/node_modules/jsdoc-sphinx/template -d $@ -r "../../src/core"
# @$(SPHINXAPIDOC) -o _apidoc --update --title="CaosDB Server" ../main/
...@@ -41,13 +41,15 @@ release = '0.x.y-beta-rc2' ...@@ -41,13 +41,15 @@ release = '0.x.y-beta-rc2'
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [ extensions = [
'sphinx_js', # 'sphinx_js',
'sphinx.ext.todo', 'sphinx.ext.todo',
"sphinx.ext.autodoc", "sphinx.ext.autodoc",
'autoapi.extension', # 'autoapi.extension',
"recommonmark", # For markdown files. "recommonmark", # For markdown files.
"sphinx_rtd_theme", "sphinx_rtd_theme",
# 'sphinx.ext.intersphinx', 'sphinx.ext.intersphinx',
'sphinx.ext.mathjax',
'sphinx.ext.ifconfig',
# 'sphinx.ext.napoleon', # For Google style docstrings # 'sphinx.ext.napoleon', # For Google style docstrings
] ]
...@@ -207,3 +209,4 @@ autodoc_default_options = { ...@@ -207,3 +209,4 @@ autodoc_default_options = {
autoapi_type = 'javascript' autoapi_type = 'javascript'
autoapi_dirs = ['../core/js/'] autoapi_dirs = ['../core/js/']
autoapi_add_toctree_entry = False
Extending the CaosDB Web Interface
==================================
Here we collect information on how to extend the web interface as a developer.
.. toctree::
:maxdepth: 1
:glob:
extension/*
Creating forms for the CaosDB Web Interface
===========================================
The ``form_elements`` module provides a library for generating forms from simple config objects. The forms are styled for the seamless integration into the CaosDB web interface and are especially useful for calling server side scripts.
See also the :doc:`API documentation <../api/module-form_elements>`
Examples
--------
Generating a generic form
^^^^^^^^^^^^^^^^^^^^^^^^^
The following code snippet adds a form to the body of the HTML document.
.. code-block:: javascript
function my_special_submit_handler (form) {
// handle form submision
};
const config = {
name: "my_form",
fields: [
{ type: "reference_drop_down", name: "experiment_id", label: "Experiment", query: "FIND Record Experiment", required: true },
{ type: "integer", name: "number", label: "A Number", required: true },
{ type: "date", name: "date", label: "A Date", required: false },
{ type: "text", name: "comment", label: "A Comment", required: false },
],
submit: my_special_submit_handler
};
const form = form_elements.make_form(config);
$("body").append(form);
The form has four fields:
1. A drop-down menu which contains all Records of type "Experiment" as options,
2. an integer field, labeled "A Number",
3. a date field, labeled "A Date", and
4. a text field, labeled "A Comment".
The first two fields are required and the form cannot be submitted without it. The latter are optional.
On submission, the function ``my_special_submit_handler`` is being called with the form element as only parameter.
As the generated form is a plain HTML form, the javascript form API can be used. However, there are special methods in the ``form_elements`` module e.g. :doc:`get_fields <../api/module-form_elements>` which are especially designed to interact with the forms generated by the ``make_form`` factory.
Calling a server-side script
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If you intend to call a server-side script, the config has to be changed a litte bit and the script calling is done by the ``form_elements`` module. There is no need to define the submit_hander anymore. Instead, just name the script which is to be called.
.. code-block:: javascript
const config = {
script: "process.py",
fields: [
{ type: "reference_drop_down", name: "experiment_id", label: "Experiment", query: "FIND Record Experiment", required: true },
{ type: "integer", name: "number", label: "A Number", required: true },
{ type: "date", name: "date", label: "A Date", required: false },
{ type: "text", name: "comment", label: "A Comment", required: false },
],
};
const form = form_elements.make_form(config);
$("body").append(form);
On submission, the form data will be send as a json file to the script and passed as the first parameter. The call would look like ``./process.py form.json`` and the file would contain, for example,
.. code-block:: json
{
"experiment_id": "234234",
"number": "400",
"date": "2020-12-24",
"comment": "This is a comment",
}
For more and advanced options for the form see the :doc:`API documentation <../api/module-form_elements>`
...@@ -10,7 +10,8 @@ Welcome to the documentation of CaosDB's web UI! ...@@ -10,7 +10,8 @@ Welcome to the documentation of CaosDB's web UI!
Getting started <getting_started> Getting started <getting_started>
Tutorials <tutorials/index> Tutorials <tutorials/index>
Concepts <concepts> Concepts <concepts>
API Index<genindex> Extending the UI <extension>
API <api/index>
This documentation helps you to :doc:`get started<getting_started>`, explains the most important This documentation helps you to :doc:`get started<getting_started>`, explains the most important
......
...@@ -13,4 +13,5 @@ RUN pip3 install pandas xlrd==1.2.0 ...@@ -13,4 +13,5 @@ RUN pip3 install pandas xlrd==1.2.0
RUN pip3 install git+https://gitlab.com/caosdb/caosdb-advanced-user-tools.git@dev RUN pip3 install git+https://gitlab.com/caosdb/caosdb-advanced-user-tools.git@dev
# For automatic documentation # For automatic documentation
RUN npm install -g jsdoc RUN npm install -g jsdoc
RUN npm install -g jsdoc-sphinx
RUN pip3 install sphinx-js sphinx-autoapi recommonmark sphinx-rtd-theme RUN pip3 install sphinx-js sphinx-autoapi recommonmark sphinx-rtd-theme
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment