view/hbs-helpers.js

/*
 * Your installation or use of this SugarCRM file is subject to the applicable
 * terms available at
 * http://support.sugarcrm.com/Resources/Master_Subscription_Agreements/.
 * If you do not agree to all of the applicable terms or do not have the
 * authority to bind the entity as an authorized representative, then do not
 * install or use this SugarCRM file.
 *
 * Copyright (C) SugarCRM Inc. All rights reserved.
 */

const Currency = require('../utils/currency');
const DateUtils = require('../utils/date');
const Field = require('./field');
const Utils = require('../utils/utils');
const Language = require('../core/language');
const Layout = require('./layout');
const Template = require('./template');
const View = require('./view');
const ViewManager = require('./view-manager');

/**
 * Handlebars helpers.
 *
 * These functions are to be used in
 * {@link http://handlebarsjs.com|Handlebars} templates.
 *
 * Example:
 * ```
 * // to register all the helpers at once
 * Handlebars.registerHelper(require('./hbs-helpers'));
 *
 * // to register a single helper
 * const HbsHelpers = require('./hbs-helpers');
 * Handlebars.registerHelper('fieldOfType', HbsHelpers.fieldOfType);
 * ```
 *
 * @module View/HbsHelpers
 */

/**
 * @alias module:View/HbsHelpers
 */
const Helpers = {
    /**
     * Creates a field widget.
     *
     * Example:
     * ```
     * {{field view model=mymodel template=edit parent=fieldset}}
     * ```
     *
     * @param {View/View} view Parent view
     * @param {Object} [options] Optional params to pass to the field.
     * @param {Backbone.Model} [options.model] The model associated with the field.
     * @param {string} [options.template] The name of the template to be used.
     * @param {Field} [options.parent] The parent field of this field.
     * @return {Handlebars.SafeString} HTML placeholder for the widget.
     */
    field: function(view, options) {
        var field = ViewManager.createField({
            def: this,
            viewDefs: this,
            view: view,
            model: options.hash.model,
            viewName: options.hash.template
        });

        if (options.hash.parent && _.isArray(options.hash.parent.fields)) {
            options.hash.parent.fields.push(field);
        }

        return field.getPlaceholder();
    },

    /**
     * Creates a field widget.
     *
     * This helper is used for fields that don't have a view definition.
     *
     * @param {string} type Field type.
     * @param {string} label Label key.
     * @return {Handlebars.SafeString} HTML placeholder for the widget.
     */
    fieldOfType: function(type, label) {
        var def = {
            type: type,
            name: type,
            label: label
        };

        var field = ViewManager.createField({
            def: def,
            view: this
        });

        return field.getPlaceholder();
    },

    /**
     * Creates a layout or a view in a given layout.
     *
     * @param {View/Layout} layout Layout that the new layout should be created in.
     * @param {string} name Name of the component to be created.
     * @return {Handlebars.SafeString} The placeholder text for the component.
     */
    component: function(layout, name) {
        var viewDef,
            newComponent,
            placeholder = '';

        viewDef = _.find(layout.meta.components, function(defs) {
            var componentName,
                componentDef;

            componentDef = defs.layout || defs.view;

            if (_.isString(componentDef)) {
                componentName = componentDef;
            } else {
                componentName = componentDef.name || componentDef.type;
            }

            return componentName === name;
        });

        if (_.isObject(viewDef) && !_.isEmpty(viewDef)) {
            newComponent = layout.initComponents([viewDef])[0];
            viewDef.initializedInTemplate = true;
            placeholder = newComponent.getPlaceholder();
        }

        return placeholder;
    },

    /**
     * Iterates through options specified by a key.
     *
     * The options collection is retrieved from the language helper.
     *
     * @param {string} key Options key.
     * @param {Function} hbsOptions HBS options.
     * @return {string} HTML string.
     */
    eachOptions: function(key, hbsOptions) {
        var options,
            ret = "",
            iterator;

        // Retrieve app list strings
        options = _.isString(key) ? Language.getAppListStrings(key) : key;

        if (_.isArray(options)) {
            iterator = function(element, index) {
                ret = ret + hbsOptions.fn({key: index, value: element});
            };
        } else if (_.isObject(options)) { // Is object evaluates arrays to true, so put it second
            iterator = function(value, key) {
                ret = ret + hbsOptions.fn({key: key, value: value});
            };
        }
        else {
            options = null;
        }

        // Don't iterate if options is not iterable
        if (options) _.each(options, iterator, this);

        return ret;
    },

    /**
     * Builds a route based on hashes sent on Handlebars helper.
     *
     * Example:
     * ```
     * {{buildRoute context=this.context}}
     * {{buildRoute model=myModel action="create"}}
     * {{buildRoute module="Employees" action="edit"}}
     * ```
     *
     * If both `module` and `model` are sent, `module` will take precedence
     * over `model.module`. Similarly, `id` will take precedence over
     * `model.id`.
     *
     * @param {Object} options Handlebars options hash.
     * @param {Object} options.hash More parameters to be used by this helper.
     *   It needs at least one of `options.hash.module`, `options.hash.model`
     *   or `options.hash.context`.
     * @param {string} [options.hash.module=options.hash.model.module]
     *   The name of the module.
     * @param {Data/Bean} [options.hash.model=options.hash.context.get('model')]
     *   The model to extract the module and/or id.
     * @param {Core/Context} [options.hash.context]
     *   A context to extract the module from.
     * @param {string} [options.hash.id=options.hash.model.id]
     *   The id of the bean record.
     * @param {string} [options.hash.action] The action name.
     * @return {string} The built route.
     */
    buildRoute: function(options) {
        var ctx = options.hash.context,
            model = options.hash.model || ((ctx && ctx.get('model')) ? ctx.get('model') : {}),
            module = options.hash.module || model.module || ((ctx && ctx.get('module')) ? ctx.get('module') : null),
            id = options.hash.id || model.id,
            action = options.hash.action;

        return SUGAR.App.router.buildRoute(module, id, action);
    },

    /**
     * Extracts the value of the given field from the given bean.
     *
     * @param {Data/Bean} bean Bean instance.
     * @param {string} field Field name.
     * @param {Object} [options] Additional options.
     * @param {string} [options.hash.defaultValue=''] Default value to return
     *   if field is not set on the bean.
     * @return {string} Field value of the given bean. If field is not set the
     *   default value or empty string.
     */
    fieldValue: function(bean, field, options) {
        return bean.get(field) || options.hash.defaultValue || '';
    },

    /**
     * Executes a given block if a given array has a value.
     *
     * @param {string|Object} val Value to look for.
     * @param {Object|Array} array Array or hash object to check.
     * @param {Object} options Additional options.
     * @param {Function} options.fn The block to execute if `val` is found.
     * @param {Function} options.inverse The block to execute if `val` is not
     *   found.
     * @return {string} Result of the `block` execution if `array` contains
     *   `val` or the result of the inverse block.
     */
    has: function(val, array, options) {
        // Since we need to check both just val = val 2 and also if val is in an array, we cast
        // non arrays into arrays
        if (!_.isArray(array) && !_.isObject(array)) {
            array = [array];
        }

        return _.include(array, val) ? options.fn(this) : options.inverse(this);
    },

    /**
     * Executes a given block if a given array doesn't have a value.
     *
     * @param {string|Object} val Value to look for.
     * @param {Object|Array} array Array or hash object to check.
     * @param {Object} options Additional options.
     * @param {Function} options.fn The block to execute if `val` is not found.
     * @param {Function} options.inverse The block to execute if `val` is
     *   found.
     * @return {string} Result of the `block` execution if the `array` doesn't
     *   contain `val` or the result of the inverse block.
     */
    notHas: function(val, array, options) {
        var fn = options.fn, inverse = options.inverse;
        options.fn = inverse;
        options.inverse = fn;

        return Handlebars.helpers.has.call(this, val, array, options);
    },

    /**
     * We require sortable to be the default if not defined in either field
     * viewdefs or vardefs. Otherwise, we use whatever is provided in either
     * field vardefs or fields viewdefs where the viewdef has more
     * specificity.
     *
     * @param {string} module Module name.
     * @param {Object} fieldViewdef The field view definition
     *   (e.g. looping through meta.panels.field it will be 'this').
     * @param {Object} options Additional options.
     * @param {Function} options.fn The block to execute if sortable.
     * @return {string} Result of the `block` execution if sortable, otherwise
     *   empty string.
     */
    isSortable: function(module, fieldViewdef, options) {
        if (Utils.isSortable(module, fieldViewdef)) {
            return options.fn(this);
        } else {
            return '';
        }
    },

    /**
     * Executes a given block if given values are equal.
     *
     * @param {string} val1 First value to compare.
     * @param {string} val2 Second value to compare.
     * @param {Object} options Additional options.
     * @param {Function} options.fn The block to execute if the given values
     *   are equal.
     * @param {Function} options.inverse The block to execute if the given
     *   values are not equal.
     * @return {string} Result of the `block` execution if the given values are
     *   equal or the result of the inverse block.
     */
    eq: function(val1, val2, options) {
        return val1 == val2 ? options.fn(this) : options.inverse(this);
    },

    /**
     * Opposite of `eq` helper.
     *
     * @param {string} val1 first value to compare
     * @param {string} val2 second value to compare.
     * @param {Object} options Additional options.
     * @param {Function} options.fn The block to execute if the given values
     *   are not equal.
     * @param {Function} options.inverse The block to execute if the given
     *   values are equal.
     * @return {string} Result of the `block` execution if the given values are
     *   not equal or the result of the inverse block.
     */
    notEq: function(val1, val2, options) {
        return val1 != val2 ? options.fn(this) : options.inverse(this);
    },

    /**
     * Same as the `eq` helper, but the second value is a (string) regex
     * expression. This works around Handlebars' lack of support for regex
     * literals.
     *
     * Note that modifiers are not supported.
     *
     * @param {string} val1 The string to test.
     * @param {string} val2 The expression to match against. It will be passed
     *   directly to the `RegExp` constructor.
     * @param {Object} options Additional options.
     * @param {Function} options.fn The block to execute if `val1` matches
     *   `val2`.
     * @param {Function} options.inverse The block to execute if `val1` does
     *   not match `val2`.
     * @return {string} Result of the `block` execution if `val1` matches
     *   `val2` or the result of the inverse block.
     */
    match: function(val1, val2, options) {
        var re = new RegExp(val2);
        if (re.test(val1)) {
            return options.fn(this);
        } else {
            return options.inverse(this);
        }
    },

    /**
     * Same as the `notEq` helper but second value is a (string) regex
     * expression. This works around Handlebars' lack of support for regex
     * literals.
     *
     * Note that modifiers are not supported.
     *
     * @param {string} val1 The string to test.
     * @param {string} val2 The expression to match against. It will be passed
     *   directly to the `RegExp` constructor.
     * @param {Object} options Additional options.
     * @param {Function} options.fn The block to execute if `val1` does not
     *   match `val2`.
     * @param {Function} options.inverse The block to execute if `val` matches
     *   `val2`.
     * @return {string} Result of the `block` execution if the given values are
     *   not equal or the result of the inverse block.
     */
    notMatch: function(val1, val2, options) {
        var re = new RegExp(val2);
        if (!re.test(val1)) {
            return options.fn(this);
        } else {
            return options.inverse(this);
        }
    },

    /**
     * Logs a value.
     *
     * @param {*} value The value to log.
     */
    log: function(value) {
        SUGAR.App.logger.debug("*****HBS: Current Context*****");
        SUGAR.App.logger.debug(this);
        SUGAR.App.logger.debug("*****HBS: Current Value*****");
        SUGAR.App.logger.debug(value);
        console.log(value);
        SUGAR.App.logger.debug("***********************");
    },

    /**
     * Retrieves an i18n-ed string by key.
     *
     * @param {string} key Key of the label.
     * @param {string} [module] Module name.
     * @param {string} [content] Template content.
     * @return {string} The string for the given label key.
     */
    str: function(key, module, content) {
        module = _.isString(module) ? module : null;
        return Language.get(key, module, content);
    },

    /**
     * Creates a relative time element to display the human readable related
     * time.
     *
     * To provide an automatic update of this relative time, use a plugin like
     * {@link sugar.liverelativetime.js}.
     *
     * @param {string} iso8601 The ISO-8601 date string to be used for a new
     *   date.
     * @param {Object} options Handlebars options hash.
     * @param {Object} options.hash More parameters to be used by this helper.
     * @param {string} [options.hash.title] The title attribute. Defaults to
     *   current user date/time preference format.
     * @param {boolean} [options.hash.dateOnly] Setting this to `true` will
     *   format the `title` attribute with the user-formatted date only.
     * @return {string} The relative time like `10 minutes ago`.
     */
    relativeTime: function(iso8601, options) {
        var date = DateUtils(iso8601);
        var attrs, html;

        options.hash.title = options.hash.title || date.formatUser(options.hash.dateOnly);

        attrs = _.map(options.hash, function(attr, key) {
            return key + '="' + Handlebars.Utils.escapeExpression(attr) + '"';
        }).join(' ');

        // TODO we need to support dateOnly on formatNow().
        html = '<time datetime="' + date.format() + '" ' + attrs + '>' + date.fromNow() + '</time>';

        return new Handlebars.SafeString(html);
    },

    /**
     * Joins all the elements of an array by a glue string.
     *
     * @param {string[]} array Array of strings to join.
     * @param {string} glue The string to join them with.
     * @return {string} All of the strings joined with the given `glue`.
     */
    arrayJoin: function(array, glue) {
        return array.join(glue);
    },

    /**
     * Converts `\r\n`, `\n\r`, `\r`, and `\n` to `<br>`.
     *
     * @param {string} s The raw string to filter.
     * @return {Handlebars.SafeString} The given `s` with all newline
     *   characters converted to `<br>` tags.
     */
    nl2br: function(s) {
        s = Handlebars.Utils.escapeExpression(s);
        var nl2br = s.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1' + '<br>');
        return new Handlebars.SafeString(nl2br);
    },

    /**
     * Formats a given currency amount according to user preferences.
     *
     * @param {number} number The number to format.
     * @param {string|Object} currencyId  The currency identifier.
     * @return {string} The formatted number.
     */
    formatCurrency: function(number, currencyId) {
        if (_.isObject(currencyId)) {
            currencyId = currencyId.hash.currencyId || undefined;
        }
        return Currency.formatAmountLocale(number, currencyId);
    },

    /**
     * Formats a given string by returning the first n characters.
     *
     * @param {string} text The text to trim.
     * @param {number} n The number of characters to return.
     * @return {string} The first `n` characters of `text`.
     */
    firstChars: function(text, n) {
        return text.substring(0, n);
    },

    /**
     * Formats a given date according to user preferences.
     *
     * @param {Date|string} date The date to format.
     * @param {Object} [options] More attributes to be used on this element for
     *   reuse.
     * @param {Object} [options.hash] More parameters to be used by this helper.
     * @param {boolean} [options.hash.dateOnly] Flag to determine whether to
     *   return just date current user date/time preference format.
     * @return {string} The formatted date.
     */
    formatDate: function(date, options) {
        date = DateUtils(date);
        return date.formatUser(options.hash.dateOnly);
    },

    /**
     * Gets the translated module name (plural or singular).
     *
     * For instance, to get the singular version of the module name (make sure
     * `LBL_MODULE_NAME_SINGULAR` is defined in the module language strings of
     * your module):
     *
     * ```
     * {{getModuleName 'Accounts'}}
     * ```
     *
     * To get the plural version, set `plural` to true and make sure
     * `LBL_MODULE_NAME` is defined in the module language strings of your
     * module:
     *
     * ```
     * {{getModuleName 'Accounts' plural=true}}
     * ```
     *
     * You can pass a default value that will be returned in case the module
     * language string of your module is not found. The following example will
     * return 'Module' (since `LBL_MODULE` is defined in the module strings or
     * the app strings):
     *
     * ```
     * {{getModuleName 'undefinedModule' defaultValue='LBL_MODULE'}}
     * ```
     *
     * In the worst case scenario (the module language string is not found and
     * no default value is specified), the module key name is returned. The
     * following example will return 'undefinedModule':
     *
     * ```
     * {{getModuleName 'undefinedModule'}}
     * ```
     *
     * @param {string} module The module defined in the language strings.
     * @param {Object} [options] Optional params to pass to the helper.
     * @param {Object} [options.hash] More parameters to be used by this helper.
     * @param {boolean} [options.hash.plural] Returns the plural form if `true`,
     *   singular otherwise.
     * @param {string} [options.hash.defaultValue] Value to be returned if the
     *   module language string is not found.
     * @return {string} The module name.
     */
    getModuleName: function(module, options) {
        var opts = {
            plural: options.hash.plural,
            defaultValue: options.hash.defaultValue
        };
        return Language.getModuleName(module, opts);
    },

    /**
     * Helper for rendering a partial template. This helper can load a partial
     * from the `templateOptions` or from the same relative location as the
     * current template.
     *
     * ```
     * {{partial 'partial-name' componentFrom defaultProperties dynamicProperty=value}}
     * ```
     *
     * The data supplied to the partial with be an object with the list of
     * `dynamicProperty`s merged into `defaultProperties` object (defaults to
     * empty object if not explicitly passed).
     *
     * For fields:
     *
     * ```
     * {{partial 'edit' this properties fallbackTemplate='detail'}}
     * ```
     *
     * For layouts:
     *
     * ```
     * {{partial 'ActivityStream' this properties}}
     * ```
     *
     * For views:
     *
     * ```
     * {{partial 'record' this properties}}
     * ```
     *
     * @param {string} name Name of the partial.
     * @param {View/Component} component The view component.
     * @param {Object} [properties] Data supplied to the partial. `options.hash`
     *   is merged into this before it is used for the template. This allows the
     *   partial to provide dynamic parameters on top of the default properties.
     *   The original component is kept as `templateComponent` in these
     *   properties.
     * @param {Object} [options] Optional parameters.
     * @param {Object} [options.hash] The hash of the optional params.
     * @param {Object} [options.hash.module=component.module] Module to use.
     * @param {Object} [options.hash.fallbackTemplate] Fallback template for
     *   field partials.
     * @return {Handlebars.SafeString} The HTML of the partial template.
     */
    partial: function(name, component, properties, options) {
        var module, template, data;

        // `properties` is optional, so `options` is `properties` is no `properties` is passed.
        if (!options && properties.hash) {
            options = properties;
            properties = {};
        }

        // Data supplied to the partial
        data = _.extend({templateComponent: component}, properties, options.hash);

        module = options.hash.module || component.module;
        if (component && component.options && component.options.templateOptions && component.options.templateOptions.partials) {
            template = component.options.templateOptions.partials[name];
        }
        // TODO: need to do recursive walk up the tree if template is not found, until we find it.
        // see _super in component.js for an example
        else if (component instanceof Field) {
            var fallbackTemplate = options.hash.fallbackTemplate;
            template = Template.getField(
                component.type, // field type
                name || 'detail', // template name
                module,
                fallbackTemplate);
        }
        else if (component instanceof View) {
            var templateName = component.tplName;

            //FIXME SC-3363 use the real inheritance chain when loading partial templates
            //Try the current component first in case the template was overridden.
            template = Template.getView(component.name + '.' + name, module) ||
                Template.getView(component.name + '.' + name);

            if (!template && templateName) {
                template = Template.getView(templateName + '.' + name, module) ||
                    Template.getView(templateName + '.' + name);
            }
        }
        else if (component instanceof Layout) {
            template = Template.getLayout(component.name + '.' + name, module) ||
                Template.getLayout(component.name + '.' + name);
        }
        return new Handlebars.SafeString(template ? template(data) : '');
    }
};

module.exports = Helpers;