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

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

define([
    'core/js/api/Listenable',
    'core/js/api/ListenerBuilder',
    'layout/js/api/LayoutConstants',
    'pages.dt/js/api/Binding',
    'pages.dt/js/api/Children',
    'pages.dt/js/api/CompatibilitySupport',
    'pages.dt/js/api/Properties'
], function(
        Listenable,
        ListenerBuilder,
        LayoutConstants,
        Binding,
        Children,
        compatibilitySupport,
        Properties
        ) {

    'use strict';

    /**
     * Represents the model of a UI component. Abcs designer does not modify page
     * UI components directly but rather using a view. It modifies the UI component's
     * behavior and properties by updating its {@link pages.dt/js/api/Properties properties}.
     * The UI component is then rebuilt by a {@link components.dt/js/spi/generators/Generator Generator}
     * to reflect the view changes.
     *
     * <p>In an ABCS page views are organized into a tree structure which
     * corresponds to the nesting of UI components into one another. There is always one top
     * level {@link pages.dt/js/api/View view} which is held by {@link pages.dt/js/api/Page page}
     * object itself and holds components in its nested lists of children.
     * Since the complex UI components are being composed from several low-level
     * components, {@link pages.dt/js/api/View Views} are simulating similar structure.
     * Every single {@link pages.dt/js/api/View View} can have many children views and
     * one single parent.</p>
     *
     * <p>View is usually created in a {@link components.dt/js/spi/creators/Creator Creator}.
     * You do not create a view directly by calling its constructor but rather using
     * the {@link components.dt/js/api/ComponentFactory#createView ComponentFactory.createView} factory method.
     * Views are then transformed into UI components that browser can understand
     * with a {@link components.dt/js/spi/generators/Generator Generator} building
     * a jQuery element as the view's UI representation.</p>
     *
     * @AbcsExtension stable
     * @version 16.3.5
     *
     * @exports pages.dt/js/api/View
     * @constructor
     * @private
     *
     * @param {String|Object} idOrOptions - unique id of the component in the application (or options object)
     * @param {ViewType} type - type of the component
     * @param {Binding} binding - the binding of the componenent to the view model
     * @param {Properties} properties - represenation of the properties section of the view definition
     * @param {Children} children - children views
     * @param {Archetype} archetype - archetype used by this view
     * @param {String} displayName - human readable view name
     *
     * @see {@link components.dt/js/api/ComponentFactory ComponentFactory} on how to create view
     * @see {@link components.dt/js/spi/creators/Creator Creator} on how to create views for your components
     * @see {@link components.dt/js/spi/generators/Generator Generator} on how to transform your views into UI elements.
     */
    var View = function(idOrOptions, type, binding, children, properties, archetype, displayName) {
        AbcsLib.checkDefined(idOrOptions, 'idOrOptions');
        AbcsLib.checkThis(this);
        Listenable.apply(this, arguments);

        var self = this;

        if (typeof idOrOptions === 'object') {
            AbcsLib.checkDefined(idOrOptions.id, 'idOrOptions.id');
            type = idOrOptions.type;
            binding = idOrOptions.binding;
            children = idOrOptions.children;
            properties = idOrOptions.properties;
            archetype = idOrOptions.archetype;
            displayName = idOrOptions.displayName;
            idOrOptions = idOrOptions.id;
        }

        // hacky way of dealing with backwards compatiblity after renaming pageTop app layout element to abcs-page-top
        // TODO - once there is migration script on backend for this renaming, this should be rolled back to: self._id = idOrOptions;
        self._id = (idOrOptions === 'pageTop' || idOrOptions === 'abcs-pageTop') ? LayoutConstants.PAGE_TOP_VIEW_ID : idOrOptions;
        self._type = type;
        self._binding = binding ? binding : new Binding();
        self._properties = properties || new Properties();
        self._children = children || new Children();
        //sets a Children -> View reference so we can do view.getParent()
        self._children.setOwnerView(self);
        self._owner = null;
        self._archetype = archetype;
        self._displayName = displayName || type;

        // private fields used in runtime only
        self._isBeingDeletedFlag = false;

        //start listening to mutable members
        self._bindingListener = new ListenerBuilder(Binding.EVENT_BINDING_SET, function () {
            self.fireEvent(View.EVENT_VIEW_CHANGED, self);
        }).build();
        self._binding.addListener(self._bindingListener);
        self._propertiesListener = new ListenerBuilder(Properties.EVENT_PROPERTY_REMOVED, function () {
            self.fireEvent(View.EVENT_VIEW_CHANGED, self);
        }).event(Properties.EVENT_PROPERTY_SET, function () {
            self.fireEvent(View.EVENT_VIEW_CHANGED, self);
        }).event(Properties.EVENT_SILENT_PROPERTY_SET, function () {
            self.fireEvent(View.EVENT_VIEW_CHANGED_SILENTLY, self);
        }).event(Properties.EVENT_SILENT_PROPERTY_REMOVED, function () {
            self.fireEvent(View.EVENT_VIEW_CHANGED_SILENTLY, self);
        }).build();
        self._properties.addListener(self._propertiesListener);
        self._childrenListener = new ListenerBuilder(Children.EVENT_INSERTED, function () {
            self.fireEvent(View.EVENT_VIEW_CHANGED, self);
        }).event(Children.EVENT_MOVED, function () {
            self.fireEvent(View.EVENT_VIEW_CHANGED, self);
        }).event(Children.EVENT_REMOVED, function (removedView) {
            self.fireEvent(View.EVENT_VIEW_REMOVED, removedView);
            self.fireEvent(View.EVENT_VIEW_CHANGED, self);
        }).event(Children.EVENT_ADDED, function (addedView) {
            self.fireEvent(View.EVENT_VIEW_ADDED, addedView);
        }).build();
        self._children.addListener(self._childrenListener);
    };
    AbcsLib.mixin(View, Listenable);

    View.EVENT_VIEW_CHANGED = 'viewChanged';
    View.EVENT_VIEW_CHANGED_SILENTLY = 'viewChangedSilently';
    View.EVENT_VIEW_REMOVED = 'viewRemoved';
    View.EVENT_VIEW_ADDED = 'viewAdded';
    View.EVENT_TYPE_SET = 'typeSet';
    View.EVENT_BINDING_SET = 'bindingSet';
    View._ERROR_TYPE = 'The "type" argument can not be undefined!';
    View._ERROR_BINDING = 'The "binding" argument can not be undefined!';
    View.IS_DROPPABLE_KEY = 'designerIsDroppable';
    View.IS_DRAGGABLE_KEY = 'designerIsDraggable';
    View.IS_WRAPPABLE_KEY = 'designerIsWrappable';
    View.IS_VERTICALLY_RESIZABLE_KEY = 'designerIsVerticallyResizable';
    View.IS_HORIZONTALLY_RESIZABLE_KEY = 'designerIsHorizontallyResizable';
    View.MIN_WIDTH_KEY = 'designerMinWidth';
    View.IS_RERENDER_PARENT = 'designerRerenderParent';
    View.IS_BEING_DELETED = 'designerIsBeingDeleted';

    /**
     * Returns the unique identifier of the view.
     *
     * <p>View IDs across whole ABCS application should be
     * unique which means that ID can be used for seeking or referencing
     * particular view instance.</p>
     *
     * @AbcsExtension stable
     * @version 16.3.5
     *
     * @returns {String} ID of the view
     *
     * @example <caption>Gets ID of the newly created View object.</caption>
     * // creates instance of the view
     * var view = ComponentFactory.createView({
     *     displayName: 'customButton',
     *     type: 'org.components.customButton',
     * });
     * // view generates the unique ID by itself from the given type
     * console.log('You view\'s ID = ' + view.getId());
     */
    View.prototype.getId = function() {
        AbcsLib.checkParameterCount(arguments, 0);
        return this._id;
    };

    /**
     * Returns type of this view.
     *
     * <p>Type defines what PI will be shown when the
     * view is selected, what {@link components.dt/js/spi/generators/Generator Generator}
     * will be used to transform the view into a UI element etc.</p>
     *
     * @AbcsExtension stable
     * @version 16.3.5
     *
     * @returns {String} type of the View
     *
     * @example <caption>Gets all other views of the same type on the active page.</caption>
     * // gets the view's type
     * var type = view.getType();
     * // seeks the active page's views of the same type
     * var Navigation = require('pages.dt/js/api/Navigation');
     * var PageUtils = require('pages.dt/js/api/PageUtils');
     * var viewsOfTheType = PageUtils.findViewsByType(Navigation.getActivePage(), type);
     */
    View.prototype.getType = function() {
        AbcsLib.checkParameterCount(arguments, 0);
        return this._type;
    };

    View.prototype.setType = function(type) {
        if (!type) {
            throw new Error(View._ERROR_TYPE);
        }
        this._type = type;
        this.fireEvent(View.EVENT_TYPE_SET, type);
    };

    /**
     * Checks if this {@link View} is a leaf view.
     *
     * @returns {Boolean} true if this is a leaf view, false otherwise
     */
    View.prototype.isLeafView = function() {
        return !this.hasChildren();
    };

    /**
     * Checks if this {@link View} has some children or not.
     *
     * @returns {Boolean} true if this View has some children, false otherwise
     */
    View.prototype.hasChildren = function() {
        return !this._children.isEmpty();
    };

    /**
     * Returns {@link viewmodel/js/api/Archetype Archetype} owned by this View.
     * Every view can own single archetype or none.
     *
     * <p>Archetypes are abstraction layer useful for work over Business Objects.
     * See {@link viewmodel/js/api/Archetype Archetypes} for further summary
     * and information about archetype types supported by ABCS.</p>
     *
     * @AbcsExtension stable
     * @version 16.3.5
     *
     * @returns {viewmodel/js/api/Archetype|undefined} archetype owned by the view or
     *          undefined if no such <code>archetype</code> exists
     *
     * @see {@link viewmodel/js/api/Archetype Archetype} for further information about Archetypes
     *
     * @example <caption>Prints name of the entity owned by the current view.</caption>
     * // get the view's archetype
     * var archetype = view.getArchetype();
     * // checks whether such archetype even exists
     * if (archetype) {
     *     // will yield the bound entity name
     *     var entityName = archetype.getEntity().getSingularName();
     *     console.log('Bound entity: ' + entityName);
     * }
     */
    View.prototype.getArchetype = function() {
        AbcsLib.checkParameterCount(arguments, 0);
        return this._archetype;
    };

    View.prototype.setArchetype = function(archetype) {
        this._archetype = archetype;
        this.fireEvent(View.EVENT_VIEW_CHANGED, this);
    };

    View.prototype.getArchetypeTypes = function() {
        var archetypeTypes = [];
        if (this._archetype) {
            archetypeTypes.push(this._archetype.getType());
        }
        return archetypeTypes;
    };

    /**
     * @returns {Binding}
     */
    View.prototype.getBinding = function() {
        var self = this;
        var binding = self._binding;
        binding.runSilently(function () {
            compatibilitySupport.updateViewBinding(self, binding);
        });
        return binding;
    };

    View.prototype.setBinding = function(binding) {
        if (!binding) {
            throw new Error(View._ERROR_BINDING);
        }
        //update the listeners
        if (this._binding) {
            this._binding.removeListener(this._bindingListener);
        }
        binding.addListener(this._bindingListener);

        this._binding = binding;

        this.fireEvent(View.EVENT_BINDING_SET, binding);
    };

    /**
     * Gets {@link pages.dt/js/api/Properties properties} of the view.
     *
     * <p>Any data one wants to preserve in the {@link pages.dt/js/api/View View} should
     * be stored inside its properties. {@link pages.dt/js/api/Properties Properties} are used in the
     * property inspector for displaying bound UI component setup
     * or in the {@link components.dt/js/spi/generators/Generator Generators}
     * for building DOM elements.
     *
     * @AbcsExtension stable
     * @version 16.3.5
     *
     * @returns {pages.dt/js/api/Properties} view's properties
     *
     * @see {@link pages.dt/js/api/Properties Properties} for further information about the properties holder
     * @see {@link components.dt/js/spi/propertyinspectors/PropertyInspector PropertyInspector}'s example for one of
     *      usages of the View.getProperties()
     *
     * @example <caption>Updating properties of the view.</caption>
     * // get the view's properties
     * var properties = view.getProperties();
     * // setting new value
     * properties.setValue('myKeyToUpdate', 'newValue');
     */
    View.prototype.getProperties = function() {
        AbcsLib.checkParameterCount(arguments, 0);
        var self = this;
        var properties = self._properties;
        properties.runSilently(function () {
            compatibilitySupport.updateViewProperties(self, properties);
        });
        return properties;
    };

    /**
     * Gets all <strong>direct</strong> child {@link pages.dt/js/api/View Views} held by this view.
     *
     * <p><strong>Note that this is not a recursive method and gets only direct children.</strong>
     * All obtained children have this view set as their parent.</p>
     *
     * @AbcsExtension stable
     * @version 16.3.5
     *
     * @returns {pages.dt/js/api/View[]} direct child views
     *
     * @see {@link pages.dt/js/api/View#getParent View.getParent} how to get the view's parent View
     * @see {@link pages.dt/js/api/View View} description how the views are structured
     *
     * @example <caption>Method for deep seeking of all view's children (including the nested ones).</caption>
     * function getAllChildViews(view) {
     *     var result = [];
     *     // gets list of direct children
     *     var children = view.getChildren();
     *     for (var i = 0; i < children.length; i++) {
     *         var child = children[i];
     *         // gathers the child itself
     *         result.push(child);
     *         // gathers list of child's views and its descendants
     *         result = result.concat(getAllChildViews(child));
     *     }
     *     return result;
     * }
     *
     * @example <caption>Tells whether the given view contains children or not.</caption>
     * var containsChildren = view.getChildren().length > 0;
     */
    View.prototype.getChildren = function() {
        AbcsLib.checkParameterCount(arguments, 0);
        return this._children.getAll();
    };

    View.prototype.hasChildOfType = function(childType) {
        return !!this.findChildOfType(childType);
    };

    View.prototype.findChildOfType = function(childType) {
        var result = null;
        var children = this.getChildren();
        var childrenLength = children.length;
        for (var i = 0; i < childrenLength; i++) {
            var child = children[i];
            if (child.getType() === childType) {
                result = child;
                break;
            } else {
                result = child.findChildOfType(childType);
            }
            if (result) {
                break;
            }
        }
        return result;
    };

    /**
     * Adds given {@link View} as a child of this view.
     *
     * @param {View} view - view to be added
     * @param {Integer} [index] where to put new child in children object
     */
    View.prototype.addChild = function(view, index) {
        if (this.isRerenderParent()) {
            view.setRerenderParent(true);
        }
        this._children.add(view, index);
    };

    View.prototype.getChild = function (viewId) {
        var result = null;
        var children = this.getChildren();
        if (children && children.length) {
            for (var i = 0; i < children.length; i++) {
                var child = children[i];
                if (child.getId() === viewId) {
                    result = child;
                    break;
                }
            }
        }
        return result;
    };

    /**
     * Moves given {@link View} within children list of this view.
     *
     * @param {View} view view to be moved to different position in children
     * @param {Number} toIndex target index where the view should be placed;
     *                 index should be >= 0; the current view is not considered
     *                 as an item in the list, so the toIndex should target list
     *                 of children without the given view
     */
    View.prototype.moveChild = function(view, toIndex) {
        this._children.move(view, toIndex);
    };

    /**
     * Removes a child {@link View} from this view.
     *
     * @param {View} view - view to be removed
     */
    View.prototype.removeChild = function(view) {
        this._children.remove(view);
    };

    View.prototype.getDefinition = function() {
        var def = {};
        def.id = this.getId();
        def.type = this.getType();
        def.displayName = this.getDisplayName();
        if (this.getArchetype()) {
            def.archetypeId = this.getArchetype().getId();
        }
        if (!this.getProperties().isEmpty()) {
            def.properties = this.getProperties().getDefinition();
        }
        if (this.getBinding()) {
            def.binding = this.getBinding().getDefinition();
        }
        if (!this._children.isEmpty()) {
            def.children = this._children.getDefinition();
        }
        return def;
    };

    /**
     * Gets the direct parent {@link pages.dt/js/api/View View}.
     *
     * <p>Since the complex UI components are being composed from several low-level
     * components, {@link pages.dt/js/api/View Views} are simulating similar structure.
     * Every single {@link pages.dt/js/api/View View} can have many children views and
     * one single parent.</p>
     *
     * @AbcsExtension stable
     * @version 16.3.5
     *
     * @returns {pages.dt/js/api/View} direct view's parent View if any, <code>undefined</code> otherwise
     *
     * @see {@link pages.dt/js/api/View#getChildren View.getChildren} how the views are structured
     * @see {@link components.dt/js/spi/generators/Generator Generator} for more information about following example
     *
     * @example <caption>Generator's method leverages parent's property in rendering.</caption>
     * // template.html content
     * <p>Use the parent's property called 'forChild' here: $forChild$</p>
     *
     * // MyCustomGenerator.js content
     * define(['text!myComponent/templates/template.html'], function (template) {
     *
     *     'use strict';
     *
     *     var MyCustomGenerator = function() {
     *     };
     *
     *     MyCustomGenerator.prototype.buildView = function (view, page) {
     *         // we presume that view has parent with property 'forChild'
     *         var parent = view.getParent();
     *         // gets the properties definition object
     *         var parentPropertiesDef = parent.getProperties().getDefinition();
     *         // applies the parent's properties over the template
     *         var tplWithProperties = ViewGeneratorSupport.applyProperties(template, parentPropertiesDef);
     *         // creates jQuery element to be returned by Generator
     *         var $element = $(tplWithProperties);
     *         return $element;
     *     };
     *
     *     return MyCustomGenerator;
     * });
     */
    View.prototype.getParent = function() {
        AbcsLib.checkParameterCount(arguments, 0);
        var children = this.getOwner();
        return children ? children.getOwnerView() : undefined;
    };

    View.prototype.findParentOfType = function(parentType) {
        var parent = this.getParent();
        if (!parent) {
            return null;
        }
        if (parent.getType() === parentType) {
            return parent;
        }
        return parent.findParentOfType(parentType);
    };

    View.prototype.isChildOf = function(view) {
        var result = false;
        if (view) {
            var parent = this.getParent();
            while (parent) {
                if (parent === view) {
                    result = true;
                    break;
                }
                parent = parent.getParent();
            }
        }
        return result;
    };

    View.prototype.getParentViewIds = function () {
        var result = [];
        var parent = this.getParent();
        while (parent) {
            result.push(parent.getId());
            parent = parent.getParent();
        }
        return result;
    };

    View.prototype.getChildViewIds = function () {
        var result = [];
        var children = this.getChildren();
        for (var i = 0; i < children.length; i++) {
            var child = children[i];
            result.push(child.getId());
            result = result.concat(child.getChildViewIds());
        }
        return result;
    };

    /**
     * @param {Children} children
     */
    View.prototype.setOwner = function(children) {
        this._owner = children;
    };

    /**
     * @returns {Children}
     */
    View.prototype.getOwner = function() {
        return this._owner;
    };

    /**
     * @param {String} displayName
     */
    View.prototype.setDisplayName = function(displayName) {
        this._displayName = displayName;
    };

    /**
     * @returns {String}
     */
    View.prototype.getDisplayName = function() {
        return this._displayName;
    };

    /**
     * Returns {@link viewmodel/js/api/Archetype Archetype} owned by this or
     * the nearest enclosing {@link pages.dt/js/api/View View}.
     *
     * <p>As the views are usually nested, this method helps you to find the
     * closest related {@link viewmodel/js/api/Archetype Archetype} you need
     * to work with inside i.e. {@link components.dt/js/spi/creators/Creator Creator}.</p>
     *
     * <p>Archetypes are abstraction layer useful for work over Business Objects.
     * See {@link viewmodel/js/api/Archetype Archetypes} for further summary
     * and {@link viewmodel/js/api/ArchetypeType ArchetypeType}s supproted by ABCS.</p>
     *
     * @AbcsExtension stable
     * @version 16.3.5
     *
     * @returns {viewmodel/js/api/Archetype|undefined} the nearest archetype of one of the parent's
     *          views or undefined if no such archetype exists
     *
     * @see {@link viewmodel/js/api/Archetype Archetype} for further information about Archetypes
     * @see {@link pages.dt/js/api/View View} description how the views are structured
     *
     * @example <caption>Writes name of the entity bound with the current view if any.</caption>
     * // get the view's archetype
     * var archetype = view.getEnclosingArchetype();
     * // checks whether such archetype even exists
     * if (archetype) {
     *     // will yield the enclosing entity name
     *     var entityName = archetype.getEntity().getSingularName();
     *     console.log('Enclosing entity: ' + entityName);
     * }
     */
    View.prototype.getEnclosingArchetype = function() {
        AbcsLib.checkParameterCount(arguments, 0);
        var result = this.getArchetype();
        if (!result) {
            var parent = this.getParent();
            if (parent) {
                result = parent.getEnclosingArchetype();
            }
        }
        return result;
    };

    View.prototype.isWrappable = function() {
        return this.getProperties().getValue(View.IS_WRAPPABLE_KEY);
    };

    View.prototype.isDroppable = function() {
        return this.getProperties().getValue(View.IS_DROPPABLE_KEY);
    };

    View.prototype.isDraggable = function() {
        return this.getProperties().getValue(View.IS_DRAGGABLE_KEY);
    };

    View.prototype.isVerticallyResizable = function() {
        return this.getProperties().getValue(View.IS_VERTICALLY_RESIZABLE_KEY);
    };

    View.prototype.isHorizontallyResizable = function() {
        return this.getProperties().getValue(View.IS_HORIZONTALLY_RESIZABLE_KEY);
    };

    View.prototype.getMinWidth = function() {
        return this.getProperties().getValue(View.MIN_WIDTH_KEY) || 1;
    };

    View.prototype.setMinWidth = function(minWidth) {
        this.getProperties().setValue(View.MIN_WIDTH_KEY, minWidth);
        return this;
    };

    View.prototype.isRerenderParent = function () {
        return this.getProperties().getValue(View.IS_RERENDER_PARENT);
    };

    View.prototype.setRerenderParent = function (rerenderParent) {
        this.getProperties().setValue(View.IS_RERENDER_PARENT, rerenderParent);
        return this;
    };

    View.prototype.isBeingDeleted = function () {
        return this._isBeingDeletedFlag;
    };

    View.prototype.setBeingDeleted = function (isBeingDeleted) {
        this._isBeingDeletedFlag = isBeingDeleted;
        return this;
    };

    return View;
});