JavaScript Extension Development API for Oracle Visual Builder Cloud Service - Classic Applications

Source: pages.dt/js/api/ViewGeneratorSupport.js

define([
    'module',
    'core/js/api/utils/StringUtils',
    'pages.dt/js/ViewFactory',
    'pages.dt/js/api/ViewGeneratorRegistry',
    'pages.dt/js/api/ViewGeneratorSession',
    'translations/js/api/Translatable'
], function(
        module,
        StringUtils,
        ViewFactory,
        viewGeneratorRegistry,
        viewGeneratorSession,
        Translatable
        ) {

    'use strict';

    /**
     * Support class for view generators.
     * <p>Provides number of helper methods useful for view generators building
     * view DOMs such as applying view properties and binding to HTML templates
     * or building child view DOMs.</p>
     *
     * @AbcsExtension stable
     * @exports pages.dt/js/api/ViewGeneratorSupport
     * @constructor
     * @private
     * @see {@link components.dt/js/spi/generators/Generator Generator}
     * @version 16.3.5
     */
    var ViewGeneratorSupport = function() {
        AbcsLib.throwStaticClassError();
    };

    ViewGeneratorSupport._LOGGER = Logger.get(module.id);

    ViewGeneratorSupport._CLONIG_VIEW_FACTORY = new ViewFactory();

    ViewGeneratorSupport._FAKE_PAGE = {
        getViewFactory: function() {
            return new ViewFactory();
        },
        getViewModelDefinition: function() {
            return {};
        }
    };

    ViewGeneratorSupport._VIEWTYPE_CLASSSUFFIX_MAP = {
        'comboBoxField': 'combobox',
        'radioButtonsField': 'radiobuttons',
        'attachment-component': 'attachment'
    };

    /**
     * Applies {@link pages.dt/js/api/Binding Binding} object to the given string
     * template.
     *
     * <p>
     * This helper method for view generators allows you to define HTML for
     * generated view DOMs in isolated template files and apply view bindings
     * separately on the template content.
     * </p>
     *
     * <p>It iterates over all key-value pairs in the Binding object and
     * replaces all occurrences of the <code>key</code> with <code>value</code>
     * in the given template content.</p>
     *
     * <p>See {@link pages.dt/js/api/ViewGeneratorSupport.applyMap ViewGeneratorSupport.applyMap}
     * for the format of binding tokens you can use in templates.</p>
     *
     * @param {String} template HTML template containing <code>$something$</code> tokens.
     * @param {pages.dt/js/api/Binding} [binding] Binding object to apply to the template.
     * If <code>undefined</code> the original template will be immediately returned
     * without any replacements.
     * @returns {String} template updated HTML template with applied bindings
     * @see {@link pages.dt/js/api/ViewGeneratorSupport.applyMap ViewGeneratorSupport.applyMap}
     * for the format of binding tokens you can use in templates.
     * @see {@link pages.dt/js/api/Binding Binding} for how and when to use Binding object.
     * @example <caption>How to write template files with binding tokens</caption>
     * <div>
     *     <p data-bind="text: $textValueObservable$"></p>
     * </div>
     *
     * @example <caption>How to apply binding to a template</caption>
     * define([
     *     'pages.dt/js/api/ViewGeneratorSupport',
     *     'text!myComponent/templates/componentTemplate.html' // see the above example for its content
     * ], function (
     *     ViewGeneratorSupport,
     *     componentTemplate
     *     ) {
     *
     *     'use strict';
     *
     *     var MyViewGenerator = function() {
     *     };
     *
     *     MyViewGenerator.prototype.buildView = function (view, page) {
     *         var binding = view.getBinding();
     *         var template = ViewGeneratorSupport.applyBinding(componentTemplate, binding);
     *         var $element = $(template);
     *         return $element;
     *     };
     *
     *     return MyViewGenerator;
     * });
     */
    ViewGeneratorSupport.applyBinding = function(template, binding) {
        AbcsLib.checkDefined(template, 'template');
        if (binding) {
            return ViewGeneratorSupport.applyMap(template, binding.getDefinition());
        }
        return template;
    };

    /**
     * Applies {@link pages.dt/js/api/Properties Properties} object to the given
     * string template.
     *
     * <p>
     * This helper method for view generators allows you to define HTML for
     * generated view DOMs in isolated template files and apply view properties
     * separately on the template content.
     * </p>
     *
     * <p>It iterates over all key-value pairs in the Properties object and
     * replaces all occurrences of the <code>key</code> with <code>value</code>
     * in the given template content.</p>
     *
     * <p>See {@link pages.dt/js/api/ViewGeneratorSupport.applyMap ViewGeneratorSupport.applyMap}
     * for the format of property tokens you can use in templates.</p>
     *
     * @AbcsExtension stable
     * @version 16.3.5
     * @param {String} template HTML template containing <code>$something$</code> tokens.
     * @param {pages.dt/js/api/Properties} [properties] Properties object to apply to the template.
     * If <code>undefined</code> the original template will be immediately returned
     * without any replacements.
     * @returns {String} template updated HTML template with applied properties
     * @see {@link pages.dt/js/api/ViewGeneratorSupport.applyMap ViewGeneratorSupport.applyMap}
     * for the format of property tokens you can use in templates.
     * @see {@link pages.dt/js/api/Properties Properties} for how and when to use Properties object.
     * @see {@link pages.dt/js/api/Binding Binding} for how and when to use Binding object.
     * @example <caption>How to write template files with property tokens</caption>
     * <div>
     *     <p>$textValue$</p>
     * </div>
     *
     * @example <caption>How to apply properties to a template</caption>
     * define([
     *     'pages.dt/js/api/ViewGeneratorSupport',
     *     'text!myComponent/templates/componentTemplate.html' // see the above example for its content
     * ], function (
     *     ViewGeneratorSupport,
     *     componentTemplate
     *     ) {
     *
     *     'use strict';
     *
     *     var MyViewGenerator = function() {
     *     };
     *
     *     MyViewGenerator.prototype.buildView = function (view, page) {
     *         var properties = view.getProperties();
     *         var template = ViewGeneratorSupport.applyProperties(componentTemplate, properties);
     *         var $element = $(template);
     *         return $element;
     *     };
     *
     *     return MyViewGenerator;
     * });
     */
    ViewGeneratorSupport.applyProperties = function(template, properties) {
        AbcsLib.checkDefined(template, 'template');
        if (properties) {
            return ViewGeneratorSupport.applyMap(template, properties.getDefinition());
        }
        return template;
    };

    /**
     * Applies object key-value pairs to the given string template.
     *
     * <p>
     * This helper method for view generators allows you to define HTML for
     * generated view DOMs in isolated template files containing placeholder
     * tokens that are replaced with actual values at the time the generator
     * is building the view's DOM.
     * </p>
     *
     * <p>It iterates over all key-value pairs in the object and replaces all
     * occurrences of all <code>"$key$"</code> tokens with the <code>value</code>
     * in the given template content.</p>
     *
     * <p>Your template may contain placeholder tokens consisting of english
     * <strong>alphanumeric characters or underscores (<code>_</code>) only</strong>,
     * prefixed and suffixed with the dollar <code>$</code> character, for example:</p>
     * <ul>
     * <li>$textValue$</li>
     * <li>$text_value$</li>
     * <li>$textValue123$</li>
     * </ul>
     *
     * <p>Tokens may be suffixed with a <code>/</code> directive specifying the
     * type of value conversion that will take place before replacing in the
     * template. This way you can easily tell Abcs to e.g. escape all special
     * characters to prevent from a text be interpreted as HTML.</p>
     * <dl>
     * <dt><code>/html</code></dt>
     * <dd>Replaces all occurrences of HTML special characters (<code>&lt;, &gt;, &amp;, &quot;</code>)
     * with HTML entities (&amp;lt;, &amp;gt;, &amp;amp;, &amp;quot;). That allows
     * you to prevent from interpretting values as HTML or place tokens into attributes
     * inside HTML tags. See the example below.</dd>
     * </dl>
     *
     * @AbcsExtension stable
     * @version 16.3.5
     * @param {String} template HTML template containing <code>$something$</code> tokens.
     * @param {Object} map map with key-value pairs to apply onto the template.
     * @returns {String} template updated HTML template with applied replacements
     * @example <caption>How to use applyMap</caption>
     * var htmlTemplate = '<p>$textValue$</p><p>$text_value123$</p>';
     * var tokenMap = {
     *     textValue: 'VALUE 1',
     *     text_value123: 'VALUE 2'
     * };
     * htmlTemplate = ViewGeneratorSupport.applyMap(htmlTemplate, tokenMap);
     * // htmlTemplate will contain:
     * // <p>VALUE 1</p><p>VALUE 2</p>
     *
     * @example <caption>How to use conversions</caption>
     * // without using conversion
     * var htmlTemplate = '<p data-custom="$attributeValue$">$textValue$</p>';
     * var tokenMap = {
     *     textValue: '<p>TEXT</p>',
     *     attributeValue: 'attribute "VALUE"'
     * };
     * htmlTemplate = ViewGeneratorSupport.applyMap(htmlTemplate, tokenMap);
     * // htmlTemplate will contain:
     * // <p data-custom="attribute "VALUE""><p>TEXT</p></p>
     * // which is obviously an invalid HTML
     *
     * // now with conversions
     * htmlTemplate = '<p data-custom="$attributeValue/html$">$textValue/html$</p>';
     * htmlTemplate = ViewGeneratorSupport.applyMap(htmlTemplate, tokenMap);
     * // htmlTemplate will contain:
     * // <p data-custom="attribute &quot;VALUE&quot;">&lt;p&gt;TEXT&lt;/p&gt;</p>
     *
     */
    ViewGeneratorSupport.applyMap = function(template, map) {
        AbcsLib.checkDefined(template, 'template');
        AbcsLib.checkDefined(map, 'map');

        // Replace property key name with optional formatting function.
        // Supports strings like $key/fn$, e.g. $label/html$.
        var varRegExp = /\$(\w+)(?:\/([^\$]+))?\$/g;

        // Method replace takes a function as the last parameter here. The
        // function is called for each matched string and returns custom
        // replacement string. In this case, it takes three arguments, the
        // first one is the whole match, the second one is the first
        // parenthesized submatch string, i.e. variable name, and the third one
        // is second second submatch string, i.e. escpaing mark. Thus, argument
        // variable contains key to replace, and escapingMark contains optional
        // formatting flag. E.g. for key "term" and template variable
        // $term/html$, the value of variable will be "term" and value of
        // escapingMark will be "html".
        return template.replace(varRegExp, function(match, variable, escapingMark) {

            if (!(variable in map)) {
                return match; // if key is not in passed map, do not replace
            }
            var origReplacingValue = map[variable];

            var replacingValue = origReplacingValue;
            // empty array is coverted to '' by toString
            if (replacingValue instanceof Array) {
                if (!replacingValue.length) {
                    replacingValue = '[]';
                }
            }
            var val;
            switch (escapingMark) {
                case 'html':
                    val = StringUtils.escapeHtml(replacingValue);
                    break;
                case 'stringJS':
                    val = StringUtils.escapeStringJS(replacingValue);
                    break;
                case 'objectJS':
                    val = StringUtils.escapeObjectJS(origReplacingValue);
                    break;
                case 'i18n':
                    val = StringUtils.escapeTranslatable(origReplacingValue);
                    break;
                case 'help':
                    val = StringUtils.escapeHelp(origReplacingValue);
                    break;
                case 'helpURL':
                    val = StringUtils.escapeHelpURL(replacingValue);
                    break;
                default:
                    val = replacingValue;
                    break;
            }
            return val;
        });
    };

    /**
     * Builds the DOM tree for the given view.
     * <p>Finds proper generator bound with the given view and runs it. This
     * method is useful when your view contains child views and you intend to
     * build them and append their DOM trees into the top view's DOM.</p>
     *
     * @AbcsExtension stable
     * @version 16.3.5
     * @param {pages.dt/js/api/View} view view to build DOM for.
     * @param {pages.dt/js/api/Page} page page containing the view.
     * @example
     * <caption>Building DOM elements for child views</caption>
     * MyViewGenerator.prototype.buildView = function (view, page) {
     *     // build view DOM element
     *     var $element = $('<div>');
     *
     *     // iterate all children and build their DOM elements
     *     view.getChildren().forEach(function (childView) {
     *         // build child view element
     *         var $childElement = ViewGeneratorSupport.buildElement(childView, page);
     *         $element.append($childElement);
     *     });
     *
     *     return $element;
     * };
     */
    ViewGeneratorSupport.buildElement = function (view, page) {
        AbcsLib.checkDefined(view, 'view');
        AbcsLib.checkDefined(page, 'page');
        var $element;
        var mode = viewGeneratorSession.getGeneratorMode();
        try {
            if (page === ViewGeneratorSupport._FAKE_PAGE) {
                page = undefined;
            }
            $element = ViewGeneratorSupport._prepareElement(view, page);
        } catch (exception) {
            var msg = 'Error building DOM element for view ' + JSON.stringify(view.getDefinition(), null, 4);
            ViewGeneratorSupport._LOGGER.exception(exception, msg);
            $element = ViewGeneratorSupport._prepareBrokenElement(view, exception);
        }
        ViewGeneratorSupport._modifyElementForDev($element, view, mode);
        return $element;
    };

    /**
     * Starts a new generator session with the passed generator mode and
     * builds a view DOM element within the session.
     *
     * @param {View} view
     * @param {ViewGeneratorModes.Mode} mode
     * @param {Page} page
     * @returns {jQuery}
     */
    ViewGeneratorSupport.buildElementInMode = function (view, mode, page) {
        var $element;
        viewGeneratorSession.runSession(mode, function() {
            page = page || ViewGeneratorSupport._FAKE_PAGE;
            var clonedView = ViewGeneratorSupport._cloneView(view, page);
            $element = ViewGeneratorSupport.buildElement(clonedView, page);
        });
        return $element;
    };

    ViewGeneratorSupport._prepareBrokenElement = function (view) {
        var $element = $('<div></div>');
        var viewId = view.getId();
        $element.attr({
            'data-view-id': viewId,
            'class': 'broken-view'
        });
        $element.append('Invalid Component \'' + viewId + '\'');
        viewGeneratorSession.getProblems(viewId).forEach(function (problemDescription) {
            $element.append('<br>' + problemDescription);
        });
        return $element;
    };

    ViewGeneratorSupport._cloneView = function(view, page) {
        //clone the view itself
        var clonedView = ViewGeneratorSupport._CLONIG_VIEW_FACTORY.createViewFromDefinition(view.getDefinition(), page.getViewModelDefinition());
        //owner - undefined after cloning by definition but mandatory for getting enclosing archetype
        clonedView.setOwner(view.getOwner());
        return clonedView;
    };

    ViewGeneratorSupport._modifyElementForDev = function ($element, view, mode) {
        if (mode && mode.isDesigner()) {
            var parent = view.getParent();
            $element.attr({
                'data-parent-view-id': parent ? parent.getId() : '',
                'data-view-type': view.getType(),
                'data-is-wrappable': view.isWrappable(),
                'data-is-droppable': view.isDroppable(),
                'data-is-vertically-resizable': view.isVerticallyResizable(),
                'data-is-horizontally-resizable': view.isHorizontallyResizable(),
                'data-is-rerender-parent': view.isRerenderParent()
            });
            $element.addClass('abcs-component');
        }
    };

    ViewGeneratorSupport._prepareElement = function (view, page) {
        var viewGenerator = viewGeneratorRegistry.getViewGenerator(view);
        if (!viewGenerator) {
            throw new Error('can\'t find view generator for type ' + view.getType() + ', id: ' + view.getId());
        }
        var $element = viewGenerator.buildView(view, page);

        if (!$element) {
            throw new Error('The ViewGenerator \'' + view.getType() + '\' has not returned a DOM element!');
        }
        $element.attr({
            'data-view-id': view.getId(),
            'data-component-type': view.getType()
        });
        ViewGeneratorSupport._addComponentClass($element, view);
        return $element;
    };

    ViewGeneratorSupport._addComponentClass = function ($element, view) {
        var cls = ViewGeneratorSupport._userFriendlyClassSuffix(view.getType());
        if (cls) {
            $element.addClass('abcs-component-' + cls);
        }
    };

    ViewGeneratorSupport._userFriendlyClassSuffix = function (viewType) {
        if (!viewType) {
            return undefined;
        }
        // Check if there is a custom value and if so, return it.
        var custom = ViewGeneratorSupport._VIEWTYPE_CLASSSUFFIX_MAP[viewType];
        if (custom) {
            return custom;
        }
        // Convert to lower-case and remove non-alphanumeric characters.
        var converted = viewType.toLowerCase().replace(/[^a-z0-9]/gi, '');
        return converted;
    };

    /**
     * Create and serialize an object to be used inside data-bind attribute. It
     * can be used where JSON.stringify is not sufficient because it doesn't
     * support function definitions.
     *
     * @param {function} fn - Function that return an object. It will be passed
     * a single argument, called 'makeExpression' which you can use to create a
     * literal expressions that will not be stringified. Function makeExpression
     * gets two arguments, the first one is the actual value, and the second
     * one, optional, is an object that can be applied with
     * ViewGenerator.applyMap on the first argument.
     *
     * @param {boolean} suppressParentheses - True to suppress parentheses at
     * the first level of the hierarchy. It is useful when putting the result
     * string directly into "data-bind" HTML parameter.
     *
     * @returns {string}
     */
    ViewGeneratorSupport.serializeObject = function (fn, suppressParentheses) {

        var str = function (val) {
            if (val === undefined) {
                return 'undefined';
            }
            return JSON.stringify(val);
        };

        // Expression node.
        var E = function (val, map) {
            this.v = map ? ViewGeneratorSupport.applyMap(val, map) : val;
        };

        E.prototype.asExpression = function () {
            return this.v;
        };

        // Expression generator.
        var e = function (val, map) {
            return new E(val, map);
        };

        var serialize, serializeObject, serializeArray, serializeTranslatable; //Define before use, hm.

        serialize = function (value, suppressParens) {
            if (value && value.constructor === E) {
                return value.v;
            } else if (AbcsLib.isInstanceof(value, Translatable)) {
                return serializeTranslatable(value);
            } else if (AbcsLib.isObject(value)) {
                return serializeObject(value, suppressParens);
            } else if (AbcsLib.isArray(value)) {
                return serializeArray(value, suppressParens);
            } else {
                return str(value);
            }
        };

        serializeObject = function (obj, suppressParens) {
            var res = suppressParens ? '' : '{';
            var first = true;
            for (var name in obj) {
                if (obj.hasOwnProperty(name)) {
                    var prop = obj[name];
                    var seg = JSON.stringify(name) + ':' + serialize(prop, false);
                    res = res + (first ? seg : ',' + seg);
                    first = false;
                }
            }
            return res + (suppressParens ? '' : '}');
        };

        serializeArray = function (array, suppressParens) {
            var res = suppressParens ? '' : '[';
            array.forEach(function (val, index) {
                if (index) {
                    res += ',';
                }
                res += serialize(val, false);
            });
            return res + (suppressParens ? '' : ']');
        };

        serializeTranslatable = function (translatable) {
            return translatable.toTranslatableString();
        };

        var obj = fn(e);
        return serialize(obj, suppressParentheses);
    };

    return ViewGeneratorSupport;

});