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

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

define([
    'core/js/api/Listenable',
    'pages.dt/js/api/ViewGeneratorSession',
    'translations.dt/js/spi/TranslatableMixin'
], function(Listenable,
            ViewGeneratorSession,
            TranslatableMixin) {

    'use strict';

    /**
     * Holds {@link pages.dt/js/api/View View}'s properties. A view keeps its
     * properties in a Properties object which is usually accessed by a property
     * inspector for modifications and by a
     * {@link components.dt/js/spi/generators/Generator Generator} to build
     * a specific DOM element according to the property values.
     *
     * @AbcsExtension stable
     * @version 16.3.5
     *
     * @exports pages.dt/js/api/Properties
     * @constructor
     * @param {Object} [propertyMap] key-value pair map of property names and values.
     * If defined the newly created instance will be initialized with keys and values
     * from the map.
     *
     * @see {@link pages.dt/js/api/View#getProperties View.getProperties} to get view's
     * properties object
     *
     * @example <caption>How to create and use Properties</caption>
     * var map = {
     *     myProp1: 'value 1',
     *     myProp2: 'value 2'
     * };
     * // create a initialized instance
     * var properties = new Properties(map);
     * // will yield 'value 1'
     * properties.getValue('myProp1');
     * // updated the stored value
     * properties.setValue('myProp1', 'modification');
     * // returns updated value: 'modification'
     * properties.getValue('myProp1');
     */
    var Properties = function(propertyMap) {
        AbcsLib.checkThis(this);
        AbcsLib.checkParameterCount(arguments, 0, 1);
        Listenable.call(this);

        this._map = propertyMap ? AbcsLib.clone(propertyMap) : {};
        this._initSilentProperties();

        TranslatableMixin.call(this);
        TranslatableMixin.prototype.__initTranslatables.call(this);
    };
    AbcsLib.mixin(Properties, TranslatableMixin, Listenable);

    Properties.EVENT_PROPERTY_REMOVED = 'propertyRemoved';
    Properties.EVENT_PROPERTY_SET = 'propertySet';
    Properties.EVENT_SILENT_PROPERTY_SET = 'silentPropertySet';
    Properties.EVENT_SILENT_PROPERTY_REMOVED = 'silentPropertyRemoved';
    Properties._SILENT_PROPERTIES_KEY = '__silentProperties';
    Properties._ERROR_REDEFINED_SILENT_PROPERTIES = 'It is not allowed to manually redefine Silent Properties value!';

    /**
     * Sets silent properties.
     *
     * @param {Object} [silentProperties] - key-value map of silent properties
     * (it is used ONLY if there is NO silent properties map already in {map} or the existing map is empty!)
     *
     */
    Properties.prototype.setSilentProperties = function (silentProperties) {
        this._initSilentProperties(silentProperties);
    };

    Properties.prototype._initSilentProperties = function (silentProperties) {
        if (!this._map[Properties._SILENT_PROPERTIES_KEY] || !Object.keys(this._map[Properties._SILENT_PROPERTIES_KEY]).length) {
            this._map[Properties._SILENT_PROPERTIES_KEY] = silentProperties || {};
        }
    };

    Properties.prototype.isEmpty = function() {
        return this.getKeys().length === 0;
    };

    /**
     * Gets all keys which are stored in the {@link pages.dt/js/api/Properties Properties}
     * object.
     *
     * <p>One can use this method i.e. to check the existence of the key
     * or to iterate the whole keys list. Values for the given key can be obtained
     * by the {@link pages.dt/js/api/Properties#getValue Properties.getValue} method then.</p>
     *
     *
     * @returns {String[]} list of Properties's keys
     * @see {@link pages.dt/js/api/Properties#getValue Properties.getValue}
     *
     * @example <caption>Seeks whether the key was defined in the Properties object.</caption>
     * // let's assume we are getting properties instance which we want to check
     * var keyToCheck = 'coolCustomKey';
     * // checks the list of all keys for the keyToCheck availability
     * var keyFound = properties.getKeys().indexOf(keyToCheck) !== -1;
     *
     * @example <caption>Iterates the properties's keys and gather values defined by several specific keys.</caption>
     * // let's assume we are getting properties instance which we want to prune
     * var result = [];
     * properties.getKeys().forEach(function(key) {
     *     switch (key) {
     *         case 'special':
     *         ...
     *         case 'extra':
     *             result.push(properties.getValue(key));
     *             break;
     *         default:
     *             // not special keys are no-ops
     *     }
     * });
     */
    Properties.prototype.getKeys = function() {
        AbcsLib.checkParameterCount(arguments, 0);
        return Object.keys(this._map);
    };

    /**
     * Returns the value stored in the {@link pages.dt/js/api/Properties properties}
     * object under the given key or <code>undefined</code> if the key does not exist.
     *
     * @AbcsExtension stable
     * @version 16.3.5
     *
     * @param {String} key property name
     * @returns {*} property value, may be <code>undefined</code> if such key doesn't exist
     *
     * @see {@link pages.dt/js/api/Properties#setValue Properties.setValue} which sets values for keys
     *
     * @example <caption>Getting values from the Properties object.</caption>
     * // creation of the new properties object
     * var properties = new Properties({
     *     'firstProperty': 'hello',
     *     'secondProperty': 'world'
     * });
     * // will yield 'hello world'
     * var welcomeText = properties.getValue('firstProperty')
     *         + ' ' + properties.getValue('secondProperty');
     * // will yield 'undefined' value
     * properties.getValue('nonExistingValue');
     */
    Properties.prototype.getValue = function(key) {
        AbcsLib.checkParameterCount(arguments, 1);
        AbcsLib.checkDefined(key, 'key');

        if (ViewGeneratorSession.isInSession()) {
            return this._getRawValue(key);
        } else {
            return AbcsLib.clone(this._getRawValue(key));
        }
    };

    Properties.prototype._getRawValue = function (key) {
        return this._map[key];
    };

    /**
     * Removes entry from the properties object under the given key.
     *
     * <p>If the given key (entry) dosn't exist no operation is performed.</p>
     *
     * @param {String} key property name to be removed
     *
     * @example <caption>Cleans up all fields of the properties object.</caption>
     * // iterates the properties
     * properties.getKeys().forEach(function(propertyKey) {
     *     // removes every entry of the properties instance
     *     properties.removeEntry(propertyKey);
     * });
     */
    Properties.prototype.removeEntry = function(key) {
        AbcsLib.checkParameterCount(arguments, 1);
        AbcsLib.checkDefined(key, 'key');

        // events are fired only in case real property removal
        if (this._map[key]) {
            delete this._map[key];
            this.fireEvent(Properties.EVENT_PROPERTY_REMOVED, key);
        }
    };

    /**
     * Puts the passed value into the {@link pages.dt/js/api/Properties properties} object
     * under the given key. The method returns the old value for the key or <code>undefined</code>
     * if the key didn't exist yet.
     *
     * <p><strong>You should not call this method inside a {@link components.dt/js/spi/generators/Generator Generator}</strong>.
     * Setting a property value may trigger another page rebuild and a subsequent
     * call to your Generator, so you may introduce an endless loop.</p>
     *
     * @AbcsExtension stable
     * @version 16.3.5
     *
     * @param {String} key property name used for properties entry identification
     * @param {String|translations/js/api/Translatable} value property value to be set into the properties entry
     * @returns {*} old property value or <code>undefined</code> if the key didn't exist yet
     *
     * @see {@link pages.dt/js/api/Properties#getValue Properties.getValue} which gets values for keys
     *
     * @example <caption>Sets values into the Properties object.</caption>
     * // creation of the new properties object
     * var properties = new Properties({
     *     'firstProperty': 'hello'
     * });
     * // will yield undefined as the previous value
     * properties.setValue('secondProperty', 2);
     * // will yield 'hello'
     * properties.getValue('firstProperty');
     * // will overwrite the 'firstProperty' with the value 'bye'
     * var formerValue = properties.setValue('firstProperty', 'bye');
     * console.log('Previous \'firstProperty\' value was \'' + formerValue + '\'.');
     */
    Properties.prototype.setValue = function(key, value) {
        AbcsLib.checkParameterCount(arguments, 2);
        AbcsLib.checkDefined(key, 'key');
        if (key === Properties._SILENT_PROPERTIES_KEY) {
            throw new Error(Properties._ERROR_REDEFINED_SILENT_PROPERTIES);
        }
        var old = this._getRawValue(key);
        if (value !== old) {
            this._map[key] = AbcsLib.clone(value);
            this.fireEvent(Properties.EVENT_PROPERTY_SET, {key: key, value: value});
        }
        return old;
    };

    Properties.prototype.setSilentValue = function(key, value) {
        var old = this._getRawSilentValue(key);
        if (value !== old) {
            this._map[Properties._SILENT_PROPERTIES_KEY][key] = AbcsLib.clone(value);
            this.fireEvent(Properties.EVENT_SILENT_PROPERTY_SET, {key: key, value: value});
        }
        return old;
    };

    Properties.prototype.getSilentValue = function(key) {
        if (ViewGeneratorSession.isInSession()) {
            return this._getRawSilentValue(key);
        } else {
            return AbcsLib.clone(this._getRawSilentValue(key));
        }
    };

    Properties.prototype._getRawSilentValue = function(key) {
        return this._map[Properties._SILENT_PROPERTIES_KEY][key];
    };

    Properties.prototype.removeSilentEntry = function(key) {
        delete this._map[Properties._SILENT_PROPERTIES_KEY][key];
        this.fireEvent(Properties.EVENT_SILENT_PROPERTY_REMOVED, key);
    };

    /**
     * Gets the serialized Properties object (as JSON).
     *
     * <p>Serialized version of the {@link pages.dt/js/api/Properties Properties} object is a simple
     * hash map containing all properties's key-value entries which can be used also in the Properties
     * constructor for creation of a new instance (that leads to the properties's clone).</p>
     *
     * @AbcsExtension stable
     * @version 16.3.5
     * @returns {Object} JSON holding all properties's key-value entries
     * @see {@link pages.dt/js/api/Properties Properties.constructor} which accepts the definition object
     *
     * @example <caption>Merges two existing properties instances together.</caption>
     * // exports properties as maps
     * var definition1 = properties1.getDefinition();
     * var definition2 = properties2.getDefinition();
     * // uses jquery framework to merge the JSON objects
     * var mergedDefinition = $.extend({}, definition1, definition2);
     * // creates new properties with all values
     * var mergedProperties = new Properties(mergedDefinition);
     */
    Properties.prototype.getDefinition = function() {
        AbcsLib.checkParameterCount(arguments, 0);
        var self = this;
        var def = {};
        self.getKeys().forEach(function(key) {
            def[key] = self.getValue(key);
        });
        return def;
    };

    /**
     * Temporarily disconnects firing events until the <code>task</code> callback
     * finishes.
     * @param {Function} task
     */
    Properties.prototype.runSilently = function (task) {
        var fireEvent = this.fireEvent;
        try {
            // set fireEvent to no-op in selent mode
            this.fireEvent = function () {};
            task();
        } finally {
            this.fireEvent = fireEvent;
        }
    };

    // TranslatableMixin support

    /**
     * Adds translatable support to the properties object. The method will
     * set the translation root of the current properties instance. The
     * translation root will prefix every individual translatable key
     * stored in the translation metadata table.
     *
     * @AbcsExtension unstable
     * @version 16.3.1
     * @see {@link translations/js/api/I18n#key}
     * @param {String} translationRoot - the translation root for <b>this</b> properties
     */
    Properties.prototype.addTranslationSupport = function (translationRoot) {
        AbcsLib.checkParameterCount(arguments, 1);
        AbcsLib.checkDefined(translationRoot, 'translation root');

        var self = this;

        self._setTranslatableRoot(translationRoot);

        // When the component root is available, re-scan all translatables and update
        // the keys
        self.getTranslatableKeys().forEach(function (key) {
            self.__updateKey(key, self._map[key]);
        });
    };

    Properties.prototype.copyTranslatablesForNewRoot = function (newTranslationRoot) {
        AbcsLib.checkParameterCount(arguments, 1);
        AbcsLib.checkDefined(newTranslationRoot, 'newTranslationRoot');
        if (this.hasTranslatableRoot()) {
            this._copyTranslatablesForNewRoot(newTranslationRoot);
        }
    };

    Properties.prototype._copyTranslatablesForNewRoot = function(newTranslationRoot) {
        var self = this;
        var oldTranslationRoot = self.getTranslatableRoot();
        self._setTranslatableRoot(newTranslationRoot);
        self.getTranslatableKeys().forEach(function (key) {
            var nlsValue = self._map[key];
            if (oldTranslationRoot !== newTranslationRoot && oldTranslationRoot !== TranslatableMixin.__DEFAULT_I18N_ROOT
                    && oldTranslationRoot !== TranslatableMixin.__NO_I18N_KEYS_ROOT && !AbcsLib.Translations.isKeyDtOnly(nlsValue.key())) {
                self._processNlsValueForNewRoot(nlsValue, key);
            } else {
                self.__updateKey(key, self._map[key]);
            }
        });
    };

    Properties.prototype._processNlsValueForNewRoot = function (nlsValue, key) {
        if (nlsValue.hasTranslation()) {
            this.setValue(key, this.createTranslatable(key).store(nlsValue.toString()));
        } else if (!nlsValue.isEmptyStringWrapper()) {
            nlsValue.key(this.__getGloballyUniqueKey(key));
        }
    };

    /**
     * Checks if this properties instance has translation support enabled.
     *
     * @version 16.3.1
     */
    Properties.prototype.hasTranslationSupport = function () {
        return this.hasTranslatableRoot();
    };

    Properties.prototype._setTranslatableRoot = function (translatableRoot) {
        AbcsLib.checkParameterCount(arguments, 1);
        AbcsLib.checkDefined(translatableRoot, 'translatable root');

        this.setSilentValue(TranslatableMixin.TRANSLATABLE_ROOT, translatableRoot);
    };

    /** @inheritdoc **/
    Properties.prototype.hasTranslatableRoot = function () {
        AbcsLib.checkParameterCount(arguments, 0);
        var translatableRoot = this.getSilentValue(TranslatableMixin.TRANSLATABLE_ROOT);
        return translatableRoot && TranslatableMixin.__NO_I18N_KEYS_ROOT !== translatableRoot;
    };

    /** @inheritdoc **/
    Properties.prototype.getTranslatableRoot = function () {
        AbcsLib.checkParameterCount(arguments, 0);
        return this.getSilentValue(TranslatableMixin.TRANSLATABLE_ROOT);
    };

    /**
     * Get a list of all translatable keys.
     *
     * @private
     * @returns {String[]} all translatable keys for the mixin
     */
    Properties.prototype.getTranslatableKeys = function () {
        var keys = this.getSilentValue(TranslatableMixin.TRANSLATABLE_KEYS) || [];
        return keys;
    };

    return Properties;

});