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;
});