Skip to content
Snippets Groups Projects
Select Git revision
  • f6de7219bb8a51cda02662619c6dd96153a51ab2
  • main default protected
  • dev protected
  • f-render-html-properties
  • f-vishesh0932-ext-cosmetics
  • f-table-references
  • f-update-legacy-adapter
  • f-refactor-refs
  • f-fix-caosadvancedtools-refs
  • f-linkahead-rename
  • f-citation-cff
  • f-map-resolve-reference
  • dev-bmpg
  • f-form-select
  • f-doc-extention
  • f-geo-position-records
  • f-data-analysis
  • f-area-folder-drop
  • f-fix-get-parents
  • f-fix-110
  • f-entity-state
  • v0.16.0
  • v0.15.2
  • v0.15.1
  • v0.15.0
  • v0.14.0
  • v0.13.3
  • v0.13.2
  • v0.13.1
  • v0.13.0
  • v0.12.0
  • v0.11.1
  • v0.11.0
  • v0.10.1
  • v0.10.0
  • v0.9.0
  • v0.8.0
  • v0.7.0
  • v0.6.0
  • v0.5.0
  • v0.4.2
41 results

ext_references.js

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    ext_references.js 19.85 KiB
    /*
     * ** header v3.0
     * This file is a part of the CaosDB Project.
     *
     * Copyright (C) 2018 Alexander Schlemmer
     * Copyright (C) 2019-2020 IndiScale GmbH (info@indiscale.com)
     * Copyright (C) 2019-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
     */
    
    
    /*
     * Check if an element is out of the viewport
     *
     * (c) 2018 Chris Ferdinandi, MIT License, https://gomakethings.com
     *
     * https://gomakethings.com/how-to-check-if-any-part-of-an-element-is-out-of-the-viewport-with-vanilla-js/
     *
     * @param {Node} elem - The element to check
     * @return {Object} A set of booleans for each side of the element.
     */
    var isOutOfViewport = function (elem) {
    
        // Get element's bounding
        var bounding = elem.getBoundingClientRect();
    
        // Check if it's out of the viewport on each side
        var out = {};
        out.top = bounding.top < 0;
        out.left = bounding.left < 0;
        out.bottom = bounding.bottom > (window.innerHeight ||
            document.documentElement.clientHeight);
        out.right = bounding.right >
            (window.innerWidth || document.documentElement.clientWidth);
        out.any =
            out.top || out.left || out.bottom || out.right;
        out.all = out.top &&
            out.left && out.bottom && out.right;
        return out;
    };
    
    
    /**
     * @module reference_list_summary
     * @version 0.1
     *
     * For generating short summaries of LIST properties. This module is used by
     * the resolve_references module.
     *
     * @author Timm Fitschen
     */
    var reference_list_summary = new function () {
    
        var logger = log.getLogger("reference_list_summary");
    
        /** Return a condensed string representation of an array of integers.
         *
         * @example simplify_integer_numbers([1,2,3,5,8,9,10]) returns "1-3, 5,
         * 8-10"
         *
         * @example simplify_integer_numbers([]) returns ""
         *
         * @example simplify_integer_numbers([1]) returns "1"
         *
         * @example simplify_integer_numbers([1,2]) returns "1, 2" because an array
         * with two elements gets a special treatment.
         *
         * The array may be unsorted.  simplify_integer_numbers([1,2,3]) returns
         * "1-3" simplify_integer_numbers([2,1,3]) returns "1-3" as well
         *
         * The array may contain duplicates.  simplify_integer_numbers([1,2,2,3])
         * returns "1-3"
         *
         * @param {numbers} array - unsorted array of integers, possibly with
         * duplicates.  @return {string} a condensed string representation of the
         * array.
         */
        this.simplify_integer_numbers = function (array) {
            logger.trace("enter simplify_integer_numbers", array);
            var set = Array.from(new Set(array));
    
            if (set.length === 0) {
                return ""
            } else if (set.length === 1) {
                return `${set[0]}`;
            }
    
            // sort numerically
            set.sort((a, b) => a - b);
    
            if (set.length === 2) {
                return `${set[0]}, ${set[1]}`;
            }
    
    
            var ret = `${set[0]}`;
            var last = undefined;
            // set[0];
    
            // e.g. [1,2,3,4,5,8,9,10];
            for (const next of set) {
                // append '-' to summarize consecutive numbers
                if (next - last === 1 && !ret.endsWith("-")) {
                    ret += "-";
                }
    
                if (next - last > 1) {
    
                    if (ret.endsWith("-")) {
                        // close previous interval and start new
                        ret += `${last}, ${next}`;
                    } else {
                        // no previous interval, start interval.
                        ret += `, ${next}`;
                    }
                } else if (next === set[set.length - 1]) {
                    // finish interval if next is last item
                    ret += next;
                    break;
                }
    
    
                last = next;
    
            }
    
            // e.g. "1-5, 8-10"
            return ret;
        }
    
        /**
         * Generate a summary of all reference_infos by calling the callback
         * function of the first reference_info in the array.
         *
         * The summary is appended to the summary_container if available and
         * returned (for testing purposes).
         *
         * Returns undefined, if ref_infos is empty or does not come with a
         * callback function.
         *
         * @param {reference_info[]} ref_infos - array of reference_infos.
         * @param {HTMLElement} summary_container - the summary is appended to this
         *     element.
         * @return {HTMLElement|string} generated summary
         */
        this.generate = function (ref_infos, summary_container) {
            logger.trace("enter generate", ref_infos);
            if (ref_infos.length > 0 &&
                typeof ref_infos[0].callback === "function") {
                const summary =
                    ref_infos[0].callback(ref_infos);
                if (summary && summary_container) {
                    $(summary_container).append(summary);
                }
                logger.trace("leave generate", summary);
                return summary;
            }
            logger.trace("leave generate, return undefined");
            return undefined;
        }
    }
    
    
    /**
     * @module resolve_references
     * @version 0.2
     *
     * Original implementation by Alexander Schlemmer, 11/2018
     *
     * @author Timm Fitschen
     * @author Alexander Schlemmer
     */
    var resolve_references = new function () {
    
        var logger = log.getLogger("resolve_references");
    
        var _scroll_timeout = undefined;
    
        // bind global function to local context for unit testing
        this.retrieve = retrieve;
        this.getParents = getParents;
    
        /**
         * This event is dispatched on the summary container after the summary has
         * been generated and appended to the container.
         */
        this.summary_ready_event = new Event("resolve_references.summary.ready");
    
        /**
         * Scroll listener calls {@link resolve_references.update_visible_references} 500 milliseconds after the
         * last scroll event.
         */
        var scroll_listener = () => {
            if (_scroll_timeout) {
                clearTimeout(_scroll_timeout);
            }
            _scroll_timeout = setTimeout(function () {
                resolve_references.update_visible_references();
            }, 500);
        };
    
    
        /**
         * Initilize the scroll listener which triggers the resolution of the
         * entity ids and trigger it for the first time in order to resolve all
         * visible references.
         */
        this.init = function () {
            if ("${BUILD_MODULE_EXT_RESOLVE_REFERENCES}" === "ENABLED") {
                scroll_listener();
    
                // mainly for vertical scrolling
                $(window).scroll(scroll_listener);
    
                // for horizontal scrolling.
                $(".caosdb-value-list").scroll(scroll_listener);
            }
        }
    
        /**
         * Check if an element is inside of the viewport on the vertical axis.
         *
         * Returns true if any of the element's bounding box's top or bottom edges are
         * in the viewport.
         *
         * @param {HTMLElement} elem - the element to check @return {boolean}
         *
         */
        this.is_in_viewport_vertically = function (elem) {
            var out =
                isOutOfViewport(elem);
            return !(out.top || out.bottom);
        }
    
        /** Check if an element is inside of the viewport on the horizontal axis.
         *
         * This criterion is very much different from the vertical correspondent: It
         * looks for the parent which contains the list scroll bar for caosdb values.
         * Then it is checked whether the element's bounding box is visible inside the
         * scroll box.
         *
         * @param {HTMLElement} elem - the element to check @return {boolean}
         *
         */
        this.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) {
                var boundel = elem.getBoundingClientRect();
                var boundscroll = scrollbox.getBoundingClientRect();
                var leftcrit = boundel.right > boundscroll.left;
                var rightcrit = boundel.left < boundscroll.right;
                return leftcrit && rightcrit;
            } else {
                return true;
            }
        }
    
    
        /**
         * Return true iff the entity has at least one direct parent named `par`.
         *
         * @param {HTMLElement} entity - entity in HTML representation.  @param
         * {string} par - parent name.  @return {boolean}
         */
        this.is_child = function (entity, par) {
            var pars = resolve_references.getParents(entity);
            for (const thispar of pars) {
                if (thispar.name === par) {
                    return true;
                }
            }
            return false;
        }
    
        /**
         * @typedef {reference_info}
         * @property {string} text
         * @property {function} callback - a callback function with one parameter
         *     (`data`) which generates a summary from the array of data objects
         *     which are passed to this function.
         * @property {object} data - an object with properties which can be
         *     understood and used by the `callback` function.
         */
    
        /**
         * Return a reference_info for an entity.
         *
         * You may add your own custom resolver by specifying a JS module
         * via the `BUILD_EXT_REFERENCES_CUSTOM_RESOLVER` build
         * variable. The custom resolver has to be a JS module (typically
         * located at caosdb-webui/src/ext/js), the name of which is given
         * as the value of `BUILD_EXT_REFERENCES_CUSTOM_RESOLVER`. It has
         * to provide a `resolve` function that takes the entity id to be
         * resolved as a string and returns a `reference_info` object with
         * the resolved custom reference as a `text` property. 
         * 
         * See caosdb-webui/src/ext/js/person_reference_resolver.js for an
         * example.
         *
         * TODO refactor to be configurable.  @async @param {string} id - the id of
         * the entity which is to be resolved.  @return {reference_info}
         */
        this.resolve_reference = async function (id) {
            const custom_reference_resolver = window["${BUILD_EXT_REFERENCES_CUSTOM_RESOLVER}"];
            if (custom_reference_resolver && typeof custom_reference_resolver.resolve === "function") {
                // try custom_reference_resolver and fall-back to standard implementation
                var ret = await custom_reference_resolver.resolve(id);
                if (ret) {
                  return ret;
                }
            }
    
            const entity = (await resolve_references.retrieve(id))[0];
    
            // TODO handle multiple parents
            const par = resolve_references.getParents(entity)[0] || {};
    
            var ret = {
                "text": id
            };
            if (getEntityHeadingAttribute(entity, "path") !==
                undefined || par.name == "Image") {
                // show file name
                var pths = getEntityHeadingAttribute(entity, "path")
                    .split("/");
                ret["text"] = pths[pths.length - 1];
            } else if (par.name === "TestReferenced" && typeof resolve_references.test_resolver === "function") {
                // this is a test case, initialized by the test suite.
                ret = resolve_references.test_resolver(entity);
            } else {
                var name = getEntityName(entity);
                if (typeof name !== "undefined" && name.length > 0) {
                    ret["text"] = name;
                }
            }
    
            return ret;
        }
    
    
        this._target_class = "caosdb-resolve-reference-target";
    
        /**
         * Add a target span where the resolved reference information can be shown.
         *
         * If the element has a target yet, the existing one is returned.
         *
         * @param {HTMLElement} element - where to append the target.
         * @return {HTMLElement} the new/existing target element.
         */
        this.add_target = function (element) {
            if(element.getElementsByClassName(this._target_class).length > 0){
                return element.getElementsByClassName(this._target_class);
            } else {
                return $(`<span class="${this._target_class}"/>`)
                    .appendTo(element)[0];
            }
        }
    
        /**
         * Retrieve meaningful information for a single caosdb-f-reference-value
         * element.
         *
         * @param {HTMLElement} rs - resolvable reference link
         * @return {reference_info} the resolved reference information
         */
        this.update_single_resolvable_reference = async function (rs) {
            $(rs).find(".caosdb-id-button").hide();
            const target = resolve_references.add_target(rs);
            const id = getEntityID(rs);
            target.textContent = id;
            const resolved_entity_info = (
                await resolve_references.resolve_reference(id));
            target.textContent = resolved_entity_info.text;
            return resolved_entity_info;
        }
    
    
        /**
         * Add a summary field to the the list_values element.
         *
         * A summary field is a DIV element with class
         * `caosdb-resolve-reference-summary`. The summary field is used to display
         * a condensed string representation of the referenced entities in the
         * list-property.
         *
         * @param {HTMLElement} list_values - where to add the summary field.
         * @return {HTMLElement} a summary field.
         */
        this.add_summary_field = function (list_values) {
            const summary = $(
                `<div class="${resolve_references._summary_class}"/>`);
            $(list_values).prepend(summary);
            return summary[0];
        }
    
        this._summary_class = "caosdb-resolve-reference-summary";
    
        /**
         * All references which have not yet been resolved are contained in an HTML
         * Element with this css class.
         */
        this._unresolved_class_name = "caosdb-resolvable-reference";
    
        this.get_resolvable_properties = function (container) {
            const _unresolved_class_name = this._unresolved_class_name;
            return $(container).find(".caosdb-f-property-value").has(
                `.${_unresolved_class_name}`).toArray();
        }
    
    
        /**
         * This function updates all references in the body which are inside of the
         * current viewport.
         *
         * If the optional container parameter is given, only elements inside the
         * container are being processed.
         *
         * @param {HTMLElement} container
         */
        this.update_visible_references = async function (container) {
            const property_values = resolve_references
                .get_resolvable_properties(container || document.body);
    
            const _unresolved_class_name = resolve_references
                ._unresolved_class_name;
    
            // Loop over all property values in the container element. Note: each property_value can be a single reference or a list of references.
            for (const property_value of property_values) {
                var lists = findElementByConditions(
                    property_value, 
                    x => x.classList.contains("caosdb-value-list"), 
                    x => x.classList.contains("caosdb-preview-container"))
                lists = $(lists).has(`.${_unresolved_class_name}`);
    
                if (lists.length > 0) {
                    logger.debug("processing list of references", lists);
    
                    for (var i = 0; i < lists.length; i++) {
                        const list = lists[i];
                        if (resolve_references
                                .is_in_viewport_vertically(list)) {
                            const rs = $(list).find(
                                    `.${_unresolved_class_name}`)
                                .toggleClass(_unresolved_class_name, false);
    
                            // First resolve only one reference. If the `ref_info`
                            // indicates that a summary is to be generated from the
                            // list of references, retrieve all other other
                            // references. Otherwise retrieve only those which are
                            // visible in the viewport horizontally and trigger the
                            // retrieval of the others when they are scrolled into
                            // the view port.
                            const first_ref_info = await resolve_references
                                .update_single_resolvable_reference(rs[0]);
    
                            first_ref_info["index"] = 0;
    
                            if (typeof first_ref_info.callback === "function") {
                                // there is a callback function, hence we need to
                                // generate a summary.
                                logger.debug("loading all references for summary",
                                    rs);
                                const summary_field = resolve_references
                                    .add_summary_field(property_value);
    
                                // collect ref infos for the summary
                                const ref_infos = [first_ref_info];
                                for (var j = 1; j < rs.length; j++) {
                                    const ref_info = resolve_references
                                        .update_single_resolvable_reference(rs[j]);
                                    ref_info["index"] = j;
                                    ref_infos.push(ref_info);
                                }
    
                                // wait for resolution of references,
                                // then generate the summary,
                                // dispatch event when ready.
                                Promise.all(ref_infos)
                                    .then(_ref_infos => {reference_list_summary
                                        .generate(_ref_infos, summary_field);})
                                    .then(() => {
                                        summary_field.dispatchEvent(
                                            resolve_references
                                                .summary_ready_event
                                        );})
                                    .catch((err) => {
                                        logger.error(err);
                                    })
    
                            } else {
                                // no summary to be generated
    
                                logger.debug("lazy loading references", rs);
                                for (var j = 1; j < rs.length; j++) {
                                    // mark others to be loaded later and only if
                                    // visible
                                    $(rs[j]).toggleClass(_unresolved_class_name, true);
                                }
                            }
                        }
                    }
                }
    
                // Load all remaining references. These are single reference values
                // and those references from lists which are left for lazy loading.
                const rs = findElementByConditions(
                    property_value, 
                    x => x.classList.contains(`${_unresolved_class_name}`), 
                    x => x.classList.contains("caosdb-preview-container"));
                for (var i = 0; i < rs.length; i++) {
                    if (resolve_references.is_in_viewport_vertically(
                            rs[i]) &&
                        resolve_references.is_in_viewport_horizontally(
                            rs[i])) {
                        logger.debug("processing single references", rs);
                        $(rs[i]).toggleClass(_unresolved_class_name, false);
    
                        // discard return value as it is not needed for any summary
                        // generation as above.
                        resolve_references.update_single_resolvable_reference(rs[i]);
                    }
                }
            }
        }
    }
    
    
    $(document).ready(function () {
        if ("${BUILD_MODULE_EXT_RESOLVE_REFERENCES}" === "ENABLED") {
            caosdb_modules.register(resolve_references);
        }
    });