diff --git a/src/core/js/ext_entity_action_panel.js b/src/core/js/ext_entity_action_panel.js new file mode 100644 index 0000000000000000000000000000000000000000..0573be0f2448ab1decc4156b48f584874d64c0c1 --- /dev/null +++ b/src/core/js/ext_entity_action_panel.js @@ -0,0 +1,384 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ + +'use strict'; + + +/** + * Add a special section to each entity one the current page where a thumbnail, + * a video preview or any other kind of summary for the entity is shown. + * + * Apart from some defaults, the content for the bottom line has to be + * configured in the file `conf/ext/json/ext_bottom_line.json` which must + * contain a {@link BottomLineConfig}. An example is located at + * `conf/core/json/ext_bottom_line.json`. + * + * @module ext_bottom_line + * @version 0.1.1 + * + * @requires jQuery (library) + * @requires log (singleton from loglevel library) + * @requires resolve_references.is_in_viewport_vertically (funtion from ext_references.js) + * @requires load_config (function from caosdb.js) + * @requires getEntityPath (function from caosdb.js) + * @requires connection (module from webcaosdb.js) + */ +var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntityPath, connection, ext_applicable) { + + /** + * @property {string|function} create - a function with one parameter + * (entity) Note: This property can as well be a + * 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. + * + * Note: the array of extensions must contain only lower-case strings. + * + * @param {HTMLElement} entity + * @param {string[]} extensions - an array of file extesions, e.g. `jpg`. + * @return {boolean} true iff the entity has a path with one of the + * extensionss. + */ + const _path_has_file_extension = function(entity, extensions) { + const path = getEntityPath(entity); + if (path) { + for (var ext of extensions) { + if (path.toLowerCase().endsWith(ext)) { + return true; + } + } + } + return false; + } + + /** + * Create a preview for videos. + * + * @param {HTMLElement} entity + * @return {HTMLElement} a VIDEO element. + */ + const _create_video_preview = function(entity) { + var path = connection.getFileSystemPath() + getEntityPath(entity); + return $(`<div class="caosdb-v-bottom-line-video-preview"><video controls="controls"><source src="${path}"/></video></div>`)[0]; + } + + /** + * Create a preview for pictures. + * + * @param {HTMLElement} entity + * @return {HTMLElement} an IMG element. + */ + const _create_picture_preview = function(entity) { + var path = connection.getFileSystemPath() + getEntityPath(entity); + return $(`<div class="caosdb-v-bottom-line-image-preview"><img src="${path}"/></div>`)[0]; + } + + var fallback_preview = undefined; + + /** + * Default creators. + * + * @member {Creator[]} + */ + const _default_creators = [{ // pictures + id: "_default_creators.pictures", + is_applicable: (entity) => _path_has_file_extension( + entity, ["jpg", "png", "gif", "svg"]), + create: _create_picture_preview + }, { // videos + id: "_default_creators.videos", + is_applicable: (entity) => _path_has_file_extension( + entity, ["mp4", "mov", "webm"]), + create: _create_video_preview, + }, { // fallback + id: "_default_creators.fallback", + is_applicable: (entity) => true, + create: (entity) => fallback_preview, + }, + + ]; + + + const previewShownEvent = new Event("ext_bottom_line.preview.shown"); + + + /** + * Store the list of creators. + * + * @member {Creator[]} + */ + const _creators = []; + + /** + * Return the preview container of the entity. + * + * @param {HTMLElement} entity - An entity in HTML representation. + * @returns {HTMLElement} the preview container or `undefined` if the entity + * doesn't have any. + */ + const get_preview_container = function(entity) { + return $(entity).children(`.${ext_applicable. + _css_class_preview_container}`)[0]; + } + + /** + * Add a preview container to the entity. + * + * The preview container is a HTMLElement with class {@link + * ext_applicable._css_class_preview_container}. It is intended to contain + * the preview after it has been created. + * + * The presence of a preview container is also the marker that an entity + * has been visited by the root_preview_handler yet and that a preview is + * loaded and added to the entity or on its way. + * + * Depending on the implementation the actual preview container might be + * wrapped into other elements which can be used for styling or other + * purposes. + * + * @param {HTMLElement} entity - An entity in HTML representation. + * @return {HTMLElement} the newly created container. + */ + var add_preview_container = function(entity) { + const button_show = $('<button class="btn btn-xs"><span class="glyphicon glyphicon-menu-down"/> Show Preview</button>') + .css({ + width: "100%" + }) + .addClass(ext_applicable._css_class_preview_container_button); + const button_hide = $('<button class="btn btn-xs"><span class="glyphicon glyphicon-menu-up"/> Hide Preview</button>') + .css({ + width: "100%" + }) + .addClass(ext_applicable._css_class_preview_container_button) + .hide(); + const style = { + padding: "0px 10px" + }; + const container = $(`<div class="collapse"/>`) + .addClass(ext_applicable._css_class_preview_container) + .addClass(ext_applicable._css_class_preview_container_resolvable) + .css(style); + const show = function() { + button_show.hide(); + button_hide.show(); + container.collapse("show"); + } + const hide = function() { + button_hide.hide(); + button_show.show(); + container.collapse("hide"); + } + button_show.click(show); + button_hide.click(hide); + container.on("shown.bs.collapse", () => { + container[0].dispatchEvent(previewShownEvent); + }); + $(entity).append(container); + $(entity).append(button_show); + $(entity).append(button_hide); + + return container[0]; + } + + /** + * Create a preview for the entity and append it to the entity. + * + * The root_preview_handler calls the root_preview_creator which returns a + * preview which is to be added to the entity. + * + * @async + * @param {HTMLElement} entity - the entity for which the preview is to + * created. + */ + var root_preview_handler = async function(entity) { + const container = get_preview_container(entity) || add_preview_container(entity); + if (container) { + await ext_applicable.root_handler(entity, container, _creators); + } else { + logger.error(new Error("Could not create the preview container.")); + } + } + + /** + * Trigger the root_preview_handler for all entities within the view port + * when the view port. + */ + var root_preview_handler_trigger = function() { + ext_applicable.root_handler_trigger(root_preview_handler); + } + + + + /** + * Configure the creators. + * + * @param {BottomLineConfig} config + */ + var configure = async function(config) { + logger.debug("configure", config); + if (config.version != "0.1") { + throw new Error("Wrong version in config."); + } + + // append/load custom creators + _creators.splice(0, _creators.length); + for (let c of ext_applicable.make_creators(config.creators)) { + _creators.push(c); + } + + // append default creators + for (let c of _default_creators) { + _creators.push(c); + } + fallback_preview = config.fallback || fallback_preview; + }; + + + /** + * Initialize this module. + * + * I.e. configure the list of creators and setup the scroll listener which + * triggers the root_preview_handler. + * + * @property {BottomLineConfig} [config] - an optional config. Per default, the + * configuration is fetched from the server. + */ + const init = async function(config) { + logger.info("ext_bottom_line initialized"); + + try { + let _config = config || await load_config("json/ext_bottom_line.json"); + await configure(_config); + + ext_applicable.init_watcher(_config.delay || 500, + root_preview_handler_trigger); + + + } catch (err) { + logger.error(err); + } + + } + + return { + previewShownEvent: previewShownEvent, + init: init, + init_watcher: ext_applicable.init_watcher, + configure: configure, + add_preview_container: add_preview_container, + root_preview_handler: root_preview_handler, + _creators: _creators, + } +}($, log.getLogger("ext_bottom_line"), resolve_references.is_in_viewport_vertically, load_config, getEntityPath, connection, ext_applicable); + + +/** + * Helper for plotly + */ +var plotly_preview = function(logger, ext_bottom_line, plotly) { + + /** + * Create a plot with plotly. + * + * The layout and any other plotly options are set by this function. The + * only parameters are `data` and `layout` which are the respective + * parameters of the `Plotly.newPlot` factory. + * + * Hence the full documentation of the `data` parameter is available at + * https://plotly.com/javascript/plotlyjs-function-reference/#plotlynewplot + * + * + * @param {object[]} data - array of objects containing the data which is + * to be plotted. + * @param {object[]} layout - dictionary of settings defining the layout of + * the plot. + * @returns {HTMLElement} the element which contains the plot. + */ + const create_plot = function(data, + layout = { + margin: { + t: 0 + }, + height: 400, + widht: 400 + }) { + var div = $('<div/>')[0]; + plotly.newPlot(div, data, layout, { + responsive: true + }); + return div; + } + + const resize_plots_event_handler = function(e) { + var plots = $(e.target).find(".js-plotly-plot"); + for (let plot of plots) { + plotly.Plots.resize(plot); + } + } + + const init = function() { + window.addEventListener(ext_applicable.previewReadyEvent.type, + resize_plots_event_handler, true); + window.addEventListener(ext_bottom_line.previewShownEvent.type, + resize_plots_event_handler, true); + } + + return { + create_plot: create_plot, + init: init, + }; + +}(log.getLogger("plotly_preview"), ext_bottom_line, Plotly); + + +// this will be replaced by require.js in the future. +$(document).ready(function() { + if ("${BUILD_MODULE_EXT_BOTTOM_LINE}" === "ENABLED") { + caosdb_modules.register(plotly_preview); + caosdb_modules.register(ext_bottom_line); + } +}); diff --git a/src/core/xsl/main.xsl b/src/core/xsl/main.xsl index a8a5aa1e7961ba0f52e68036c398957df35e8703..35bc2d53a366530c63f1ff24fe620a4f37b5fce1 100644 --- a/src/core/xsl/main.xsl +++ b/src/core/xsl/main.xsl @@ -240,6 +240,11 @@ <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_bottom_line.js')"/> </xsl:attribute> </xsl:element> + <xsl:element name="script"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_entity_action_panel.js')"/> + </xsl:attribute> + </xsl:element> <xsl:element name="script"> <xsl:attribute name="src"> <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_revisions.js')"/> diff --git a/test/core/index.html b/test/core/index.html index e4d6d81634aa73c2d71385545fb917392ba5df1f..8d8a583cce9f2c429681dc99a8010c8b7b72d7c4 100644 --- a/test/core/index.html +++ b/test/core/index.html @@ -66,6 +66,7 @@ <script src="js/ext_map.js"></script> <script src="js/ext_applicable.js"></script> <script src="js/ext_bottom_line.js"></script> + <script src="js/ext_entity_action_panel.js"></script> <script src="js/ext_revisions.js"></script> <script src="js/autocomplete.js"></script> <script src="js/ext_sss_markdown.js"></script> @@ -87,6 +88,7 @@ <script src="js/modules/ext_map.js.js"></script> <script src="js/modules/ext_applicable.js.js"></script> <script src="js/modules/ext_bottom_line.js.js"></script> + <script src="js/modules/ext_entity_action_panel.js.js"></script> <script src="js/modules/ext_revisions.js.js"></script> <script src="js/modules/autocomplete.js.js"></script> <script src="js/modules/ext_sss_markdown.js.js"></script> diff --git a/test/core/js/modules/ext_entity_action_panel.js.js b/test/core/js/modules/ext_entity_action_panel.js.js new file mode 100644 index 0000000000000000000000000000000000000000..f58fbe8514d861b6d57ed81e81116bf1ec99c90c --- /dev/null +++ b/test/core/js/modules/ext_entity_action_panel.js.js @@ -0,0 +1,28 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ + +'use strict'; + +var ext_entity_action_panel_test_suite = function ($, ext_entity_action_panel, QUnit) { + +}($, ext_entity_action_panel, QUnit);