define([
'components/js/utils/Currency',
'components.dt/js/api/ComponentProviderRegistry',
'components.dt/js/api/constants/Style',
'components.dt/js/utils/ActionUtils',
'components.dt/js/utils/TableColumn',
'entity/js/api/Property',
'entity/js/api/PropertyType',
'pagedesigner.dt/js/api/grid/DeleteSupport',
'pages.dt/js/ViewFactory',
'pages.dt/js/api/Navigation',
'pages.dt/js/api/View',
'pages.dt/js/api/ViewType',
'translations/js/api/I18n',
'translations/js/api/Translatable'
], function (
Currency,
ComponentProviderRegistry,
Style,
ActionUtils,
TableColumn,
Property,
PropertyType,
deleteSupport,
ViewFactory,
navigation,
View,
ViewType,
I18n,
Translatable
) {
'use strict';
/*
* =========================================================================
* Column
* =========================================================================
*/
/**
* @constructor
* @private
*
* @param {Property} property
* @param {View} view
*/
var Column = function (property, view) {
this._property = property;
this._view = view;
};
Column.prototype.getProperty = function () {
return this._property;
};
Column.prototype.getView = function () {
return this._view;
};
Column.prototype.getDisplayName = function () {
var custom = this.getView().getProperties().getTranslatableValue(TableColumn.TITLE);
return custom || this.getProperty().getName();
};
/**
* Get expression for getting translated display name of the column.
*
* @returns {string} JS expression that evaluates to translated column name.
*/
Column.prototype.getDisplayNameTranslatable = function () {
var custom = this.getView().getProperties().getValue(TableColumn.TITLE);
return custom || this.getProperty().getNameAsTranslatable();
};
Column.prototype.getComplexName = function () {
var orig = this.getProperty().getName();
var custom = this.getView().getProperties().getValue(TableColumn.TITLE);
if (custom && custom.toString() !== orig.toString()) {
return orig + ' (' + custom + ')';
} else {
return orig;
}
};
Column.prototype.getType = function () {
return this.getProperty().getType();
};
Column.prototype.getPropertyId = function () {
return this.getProperty().getId();
};
Column.prototype.getColumnId = function () {
return this.getPropertyId();
};
Column.prototype.getRenderingProps = function () {
var props = this.getView().getProperties();
var allKeys = props.getKeys();
var nonRenderingKeys = ['__silentProperties', View.IS_WRAPPABLE_KEY,
View.IS_DRAGGABLE_KEY, View.IS_DROPPABLE_KEY,
View.IS_HORIZONTALLY_RESIZABLE_KEY, View.IS_BEING_DELETED,
View.IS_VERTICALLY_RESIZABLE_KEY, View.IS_RERENDER_PARENT,
TableColumn.TITLE, TableColumn.FIELD_ID, TableColumn.WIDTH_TYPE,
TableColumn.WIDTH_VALUE_PERCENT, TableColumn.WIDTH_VALUE_PX,
TableColumn.LONG_VALUES_TYPE, ActionUtils.PROPS_METHODS];
var renderingKeys = allKeys.filter(function (key) {
return nonRenderingKeys.indexOf(key) === -1;
});
var res = {};
renderingKeys.forEach(function (key) {
res[key] = props.getValue(key);
});
return res;
};
/**
* Create a new column for specified property.
*
* @param {Property} property
* @param {View} view
*
* @returns {Column}
*/
Column.create = function (property, view) {
AbcsLib.checkParameterCount(arguments, 2);
AbcsLib.checkParametersType(arguments, [Property, View]);
return new Column(property, view);
};
/*
* =========================================================================
* InvalidProperty
* =========================================================================
*/
/**
*
* @param {String} id
* @private
* @constructor
*/
var InvalidProperty = function (id) {
this._id = id;
};
InvalidProperty.prototype.getId = function () {
return this._id;
};
InvalidProperty.prototype.isReference = function () {
return false;
};
InvalidProperty.prototype.getType = function () {
return '';
};
/*
* =========================================================================
* InvalidColumn
* =========================================================================
*/
var InvalidColumnTranslatable = function (id) {
this.id = id;
};
AbcsLib.extend(InvalidColumnTranslatable, Translatable);
InvalidColumnTranslatable.prototype.toTranslatableString = function () {
return 'AbcsLib.i18n(\'components.columnPropertyNotFound\', {id: \'' + this.id + '\'})';
};
/**
* @constructor
* @private
*
* @param {InvalidProperty} invalidProperty
*/
var InvalidColumn = function (invalidProperty) {
this._invalidProperty = invalidProperty;
this.invalid = true;
};
InvalidColumn.prototype.getDisplayName = function () {
return this._invalidProperty.getId();
};
InvalidColumn.prototype.getComplexName =
InvalidColumn.prototype.getDisplayName;
InvalidColumn.prototype.getType = function () {
return 'invalid';
};
InvalidColumn.prototype.getProperty = function () {
return this._invalidProperty;
};
InvalidColumn.prototype.getPropertyId = function () {
return this._invalidProperty.getId();
};
InvalidColumn.prototype.getColumnId = function () {
return this.getPropertyId();
};
InvalidColumn.prototype.getRenderingProps = function () {
return {
flags: {
invalid: true
}
};
};
InvalidColumn.prototype.getDisplayNameTranslatable = function () {
return new InvalidColumnTranslatable(this._invalidProperty.getId());
};
InvalidColumn.prototype.getView = function () {
return null;
};
/*
* =========================================================================
* NewColumn
* =========================================================================
*/
/**
* @constructor
* @private
*
* @param {Property} property
*/
var NewColumn = function (property) {
this._property = property;
this._view = null;
};
AbcsLib.extend(NewColumn, Column);
NewColumn.prototype.getDisplayName = function () {
return this.getProperty().getName();
};
/**
* Get expression for getting translated display name of the column.
*
* @returns {string} JS expression that evaluates to translated column name.
*/
NewColumn.prototype.getDisplayNameTranslatable = function () {
return this.getProperty().getNameAsTranslatable();
};
NewColumn.prototype.getComplexName = function () {
return this.getProperty().getName();
};
NewColumn.prototype.getRenderingProps = function () {
return {};
};
/*
* =========================================================================
* TableColumns
* =========================================================================
*/
/**
* @constructor
* @private
*
* @param {Boolean} newTable - Mark that the table is being configured.
* @param {View} view - Table view.
* @param {Entity} entity
*/
var TableColumns = function (newTable, view, entity) {
this._newTable = newTable;
this._view = view;
this._columns = ko.observableArray();
this._viewListener = undefined;
this._listeningOffLevel = 0;
this._entity = entity;
this._suppressChangeNotifications = 0;
// Read-only, exposed, observable array of columns
this.columns = ko.pureComputed(function () {
return this._columns();
}.bind(this));
// Read-only, exposed, observable array of assigned entity properties
this.selectedProperties = ko.pureComputed(function () {
return this._getSelectedProperties();
}.bind(this));
if (!newTable || entity) {
this._reload();
}
};
/**
* Basically reverse operation to the {@link TableColumns.formatColumns} method.
*
* <p>
* It takes a JSON file containing columns in JET table format and parse them to get a list
* of properties.
* </p>
*
* @param {JSON} columnsJSON
* @param {Entity} entity
* @param {function} [invalidCreator] Function that creates placeholders for
* invalid properties. It will be passed
* id of each invalid property.
*
* @returns {Array[]} array of {@link Property properties}
*/
TableColumns.parseColumnsJSON = function (columnsJSON, entity, invalidCreator) {
columnsJSON = columnsJSON && columnsJSON.replace(/\'/g, '\"') || '[]';
var visibleColumns = [];
JSON.parse(columnsJSON).forEach(function(column) {
var id = column.field;
if (id) {
var property = entity.getProperty(id);
if (property) {
visibleColumns.push(property);
} else if (AbcsLib.isDefined(invalidCreator)) {
visibleColumns.push(invalidCreator(id));
}
}
});
return visibleColumns;
};
/**
* Formats the given properties to a JSON compliant with the JET table.
*
* @param {Property[]} properties
* @returns {String}
*/
TableColumns.formatColumns = function (properties) {
var visibleProperties = [];
properties.forEach(function(property) {
var desc = {
field: property.getId()
};
visibleProperties.push(desc);
});
// Note: This is awkward. The array should be stored as is. If needed,
// it should be escaped in template using escaping mark objectJS, or at
// least should be escaped using StringUtils.escapeObjectJS() and
// returned. But keeping this for backward compatibility with already
// created table views. Fortunately, field ids cannot contain special
// characters.
var result = JSON.stringify(visibleProperties);
// Replace all double quotes for single once, because JET is shitty and can't handle both
result = result.replace(/\"/g, '\'');
return result;
};
/**
* Similar to TableColumns.parseColumnsJSON but returns list of invalid (non-existent) fields.
* @param {String} columnsJSON
* @param {Entity} entity
* @returns {Array} array of invalid field ids.
*/
TableColumns.getInvalidColumns = function (columnsJSON, entity) {
columnsJSON = columnsJSON && columnsJSON.replace(/\'/g, '\"') || '[]';
var invalidColumns = [];
JSON.parse(columnsJSON).forEach(function(column) {
var id = column.field;
if (id) {
if (!entity.getProperty(id)) {
invalidColumns.push(id);
}
}
});
return invalidColumns;
};
TableColumns.prototype._getEntity = function () {
if (this._entity) {
return this._entity;
}
var view = this._view;
var archetype = view.getEnclosingArchetype();
var entity = archetype.getEntity();
return entity;
};
/**
* Reload column definitions for the view.
*
* @returns {undefined}
*/
TableColumns.prototype._reload = function () {
var entity = this._getEntity();
var columnViews = this._getColumnChildren();
this._loadColumnsFromChildViews(entity, columnViews);
};
/**
* Create column view for a property.
*
* @param {Property} prop
* @param {Page} [pageOpt] - Page, optional. If not specified, active page
* will be used.
*
* @returns {View} View of type table-column.
*/
TableColumns.prototype._createViewForProperty = function (prop, pageOpt) {
var page = pageOpt || navigation.getActivePage();
var vf = page.getViewFactory();
var viewDefinition = {
id: this.getTableView().getId() + '-col-' + prop.getId() + '-0',
type: ViewType.TABLE_COLUMN,
displayName: 'Column'
};
var v = vf.createView(viewDefinition);
var p = v.getProperties();
var i18n = I18n.key(I18n.Type.Page, page.getId(), this.getTopTableContainerView().getId(), 'columns', v.getId());
p.addTranslationSupport(i18n);
p.setValue(TableColumn.FIELD_ID, prop.getId());
p.setTranslatableValue(TableColumn.TITLE, prop.getName());
// default column alignment
if (!p.getValue(Style.TEXT_ALIGNMENT)) {
var defaultAlign = Style.getDefaultAlignment(prop.getType());
p.setValue(Style.TEXT_ALIGNMENT, defaultAlign);
}
this._setDefaultFormattingProperties(prop.getType(), p);
var propertyCreator;
switch (prop.getType()) {
case PropertyType.URL:
propertyCreator = ComponentProviderRegistry.findCreator(ViewType.INPUT_FIELD_URL);
break;
case PropertyType.EMAIL:
propertyCreator = ComponentProviderRegistry.findCreator(ViewType.INPUT_FIELD_EMAIL);
break;
case PropertyType.PHONE:
propertyCreator = ComponentProviderRegistry.findCreator(ViewType.INPUT_FIELD_PHONE);
break;
default: // nothing
}
if (propertyCreator && AbcsLib.isFunction(propertyCreator.createDefaultAction)) {
var archetype = this.getTopTableContainerView().getEnclosingArchetype();
propertyCreator.createDefaultAction(page, archetype, v, false);
}
return v;
};
TableColumns.prototype._setDefaultFormattingProperties = function (propertyType, properties) {
if (propertyType === PropertyType.NUMBER) {
properties.setValue('useGrouping', true);
} else if (propertyType === PropertyType.CURRENCY) {
properties.setValue('currencyCode', Currency.DEFAULT_CURRENCY_CODE);
properties.setValue('fractionDigits', 2);
} else if (propertyType === PropertyType.PERCENTAGE) {
properties.setValue('maximumFractionDigits', 3);
}
};
/**
* Load column definitions from child views.
*
* @param {Entity} entity
* @param {Array} columnViews - Array of child views.
*/
TableColumns.prototype._loadColumnsFromChildViews = function (entity, columnViews) {
var cols = columnViews.map(function (childView) {
var p = childView.getProperties();
var fieldId = p.getValue(TableColumn.FIELD_ID);
var property = fieldId && entity.getProperty(fieldId);
if (property) {
return new Column(property, childView);
} else {
var invalidProperty = new InvalidProperty(fieldId || '?');
return new InvalidColumn(invalidProperty);
}
});
this._columns(cols);
};
TableColumns.prototype._getColumnChildren = function () {
var tableView = this.getTableView();
return tableView.getChildren().filter(function (childView) {
return childView.getType() === ViewType.TABLE_COLUMN;
});
};
/**
* Create invalid property for incorrect or missing property id.
*
* @param {string} id - Property id.
*
* @returns {InvalidProperty}
*/
TableColumns.prototype._createInvalidProperty = function (id) {
return new InvalidProperty(id);
};
/**
* Change order of one of columns.
*
* @param {number} fromIndex - Original column index.
* @param {number} toIndex - New column index.
*/
TableColumns.prototype.reorderColumn = function (fromIndex, toIndex) {
var cols = this.columns().slice();
var len = cols.length;
if (fromIndex < len && toIndex < len && fromIndex >= 0 && toIndex >= 0) {
var movedCol = cols.splice(fromIndex, 1)[0];
cols.splice(toIndex, 0, movedCol);
var tableView = this.getTableView();
var movedChild = tableView.getChildren()[fromIndex];
if (movedChild) {
tableView.moveChild(movedChild, toIndex);
}
// Fire change event in order to update the view correctly.
// (Without this, no data are displayed in the table.)
this._view.fireEvent(ViewFactory.EVENT_VIEW_CHANGED, this._view);
this._columns([]);
this._columns(cols);
}
return Promise.resolve();
};
/**
* Add a new column.
*
* @param {object} propertyOrId - Id of property.
* @param {number} [index] - Index of new column.
* @param {page} [page] - Page to which the table belongs. Optional, if not
* specified, active page will be used.
*/
TableColumns.prototype.addColumn = function (propertyOrId, index, page) {
var self = this;
var property = TableColumns._getProperty(propertyOrId, self._getEntity());
var newView = self._createViewForProperty(property, page);
var tableView = self.getTableView();
var column = Column.create(property, newView);
if (AbcsLib.hasValue(index)) {
this._columns.splice(index, 0, column);
} else {
this._columns.push(column);
}
tableView.addChild(newView, index);
this.notifyChanged();
return Promise.resolve();
};
/*
* Get property from an argument which may be a property directly or a
* property id.
*/
TableColumns._getProperty = function (propertyOrId, entity) {
if (propertyOrId instanceof Property) {
return propertyOrId;
} else if (AbcsLib.isString(propertyOrId)) {
return entity.getProperty(propertyOrId);
} else {
throw new Error('Expected property or property id.');
}
};
/**
* Remove one of columns.
*
* @param {number} index - Column index.
*/
TableColumns.prototype.removeColumn = function (index) {
var cols = this.columns();
if (cols.length > index && index >= 0) {
var removed = cols.splice(index, 1);
if (removed.length) {
return this._removeView(removed[0].getView()).then(function () {
return this.notifyChanged();
}.bind(this));
}
}
return Promise.resolve();
};
/**
* Dispose this TableColumns instance. Remove all listeners and release
* all other allocated resources.
*/
TableColumns.prototype.dispose = function () {
var view = this._view;
if (this._bindingListener) {
var binding = view.getBinding();
binding.removeListener(this._bindingListener);
}
};
/**
* Compare list of properties with list of column views that migh represent
* the same list of visible table columns.
*
* @param {Array} properties - Array of properties.
* @param {Array} views - Array of vies of type table-column.
*
* @returns True if properties and views represent the same list of
* visible table columns, false otherwise.
*/
TableColumns.prototype._comparePropsAndViews = function (properties, views) {
if (properties.length !== views.length) {
return false;
}
for (var i = 0; i < views.length; i++) {
var v = views[i];
var p = properties[i];
var viewFieldId = v.getProperties().getValue(TableColumn.FIELD_ID);
var propertyId = p.getId();
if (!viewFieldId || !propertyId || viewFieldId !== propertyId) {
return false;
}
}
return true;
};
TableColumns.prototype._getSelectedProperties = function () {
return this._columns().map(function (column) {
return column.getProperty();
}).filter(function (prop) {
return !TableColumns.isInvalidProperty(prop);
}).filter(function (prop, index, array) {
return array.indexOf(prop) === index; // unique only
});
};
/**
* Perform some operation with listeners disabled.
*
* @param {function} fn Function to run without listening.
*/
TableColumns.prototype.withoutListeners = function (fn) {
this._listeningOffLevel += 1;
try {
return fn.call(this);
} finally {
this._listeningOffLevel -= 1;
}
};
TableColumns.prototype.getTableView = function () {
return this._view.findChildOfType(ViewType.TABLE);
};
TableColumns.prototype.getTopTableContainerView = function () {
return this._view;
};
TableColumns.prototype.getColumnView = function (columnIndex) {
return TableColumns._getColumnView(this.columns(), columnIndex);
};
TableColumns._getColumnView = function (columns, columnIndex) {
var result = null;
if (columnIndex < columns.length && columnIndex >= 0) {
result = columns[columnIndex].getView();
}
return result;
};
/**
* Remove a table column view.
*
* @param {View} view
*/
TableColumns.prototype._removeView = function (view) {
return deleteSupport.deleteView(view).then(function () {
var entity = this._getEntity();
var children = this._getColumnChildren();
this._loadColumnsFromChildViews(entity, children);
}.bind(this));
};
/**
* Called from table wizard if entity is changed.
*
* @param {Entity} entity
*/
TableColumns.prototype.reset = function (entity) {
this._entity = entity;
var columns = this._columns().slice();
columns.forEach(function (col) {
this._removeView(col.getView());
}.bind(this));
this._columns([]);
};
TableColumns.prototype.notifyChanged = function () {
if (this._suppressChangeNotifications === 0) {
this._view.fireEvent(ViewFactory.EVENT_VIEW_CHANGED, this._view);
}
};
TableColumns.prototype._withoutChangeNotifications = function (fn, context) {
this._suppressChangeNotifications += 1;
try {
fn.apply(context);
} finally {
this._suppressChangeNotifications -= 1;
}
};
/**
* Create an intance that can schedule a set of changes and then approve
* them or cancel them. This should be used in dialogs with OK/Cancel
* buttons.
*
* @returns {VetoableTableColumns}
*/
TableColumns.prototype.createTentativeInstance = function () {
return new TableColumns._Tentative(this);
};
/**
* @param {TableColumns} parent - Parent TableColumns object.
*
* @constructor
*/
TableColumns._Tentative = function (parent) {
this.parent = parent;
this._columns = ko.observableArray(parent.columns().slice());
this.columns = ko.pureComputed(function () {
return this._columns();
}.bind(this));
};
TableColumns._Tentative.prototype.approve = function () {
var self = this;
var original = this.parent.columns();
var edited = this.columns();
// Current index of a column
var curIdx = function (col) {
var columnViewArray = self.parent.columns().map(function (column) {
return column.getView();
});
return columnViewArray.indexOf(col.getView());
};
var deleted = original.filter(function (col) {
return edited.indexOf(col) === -1; // in orig, not in edited
});
var reordered = edited.filter(function (col) {
return original.indexOf(col) !== -1; // in edited and in orig
});
var added = edited.filter(function (col) {
return original.indexOf(col) === -1; // in edited, not in orig
});
var res = Promise.resolve();
self.parent._withoutChangeNotifications(function () {
deleted.forEach(function (del) {
res = res.then(function () {
return self.parent.removeColumn(curIdx(del));
});
});
reordered.forEach(function (reord, index) {
res = res.then(function () {
var currIndex = curIdx(reord);
if (index !== currIndex) {
return self.parent.reorderColumn(currIndex, index);
}
});
});
added.forEach(function (add) {
res = res.then(function () {
var index = edited.indexOf(add);
return self.parent.addColumn(add.getProperty(), index);
});
});
});
return res.then(function () {
if (deleted.length || added.length) { // not needed for reorder changes
self.parent.notifyChanged();
}
});
};
TableColumns._Tentative.prototype.addColumn = function (propertyOrId, index) {
var entity = this.parent._getEntity();
var property = TableColumns._getProperty(propertyOrId, entity);
var newCol = new NewColumn(property);
this._columns.splice(index, 0, newCol);
};
TableColumns._Tentative.prototype.reorderColumn = function (fromIndex, toIndex) {
var cols = this._columns();
var deleted = cols.splice(fromIndex, 1);
cols.splice(toIndex, 0, deleted[0]);
this._columns([]);
this._columns(cols);
};
TableColumns._Tentative.prototype.removeColumn = function (index) {
this._columns.splice(index, 1);
};
/**
* Create table columns instance for a table view.
*
* @param {View} view - Table view (of type top-table-container).
*
* @returns {TableColumns}
*/
TableColumns.forTable = function (view) {
AbcsLib.checkParameterCount(arguments, 1, 0);
var type = view.getType();
if (type !== ViewType.TOP_TABLE_CONTAINER) {
throw new Error('Expected view of type top-table-container, not ' + type);
}
return new TableColumns(false, view);
};
TableColumns.forNewTable = function (view, entity) {
AbcsLib.checkParameterCount(arguments, 2, 0);
if (view.getType() !== ViewType.TOP_TABLE_CONTAINER) {
throw new Error('Expected view of type top-table-container.');
}
return new TableColumns(true, view, entity);
};
/**
* Check if the passed top-table-container view is associated with any
* columns.
*
* @param {View} view - View of type top-table-container.
* @returns {Boolean}
*/
TableColumns.hasColumns = function (view) {
var tableView = view.findChildOfType(ViewType.TABLE);
return !!(tableView && tableView.getChildren().length > 0);
};
/**
* Check whether passed object represents placeholder for some invalid
* property.
*
* @param {Object} obj
* @returns {boolean}
*/
TableColumns.isInvalidProperty = function (obj) {
return obj instanceof InvalidProperty;
};
TableColumns.Column = Column;
return TableColumns;
});