comindware.core.ui
Version:
Comindware Core UI provides the basic components like editors, lists, dropdowns, popups that we so desperately need while creating Marionette-based single-page applications.
324 lines (263 loc) • 12.4 kB
text/typescript
import { GraphModel, NodeConfig, NestingOptions } from '../types';
import DiffItem from '../classes/DiffItem';
import ConfigDiff from '../classes/ConfigDiff';
import Backbone from 'backbone';
import _ from 'underscore';
type TreeDiffControllerOptions = {
configDiff: ConfigDiff,
graphModel: GraphModel,
initialModel: GraphModel,
calculateDiff: boolean,
reqres: Backbone.Radio.Channel,
nestingOptions?: NestingOptions
};
interface CollectionWithInitIalConfig extends Backbone.Collection {
initialConfig: string[];
}
// TODO: option
const observableProperties = ['index', 'isHidden', 'width', 'sorting', 'filter', 'isLazyLoading', 'viewType'];
const findDuplicates = (array: string[]): string[] => {
const count = (arr: string[]) => arr.reduce<Record<string, number>>((acc, name) => ({ ...acc, [name]: (acc[name] || 0) + 1 }), {});
const duplicates = (dict: Record<string, number>) => Object.keys(dict).filter((a: string) => dict[a] > 1);
return duplicates(count(array));
};
const findAllDescendants = (model: GraphModel, predicate?: (model: GraphModel) => boolean) => {
if (!model.isContainer || !model.childrenAttribute) {
return [];
}
const result: GraphModel[] = [];
const children: Backbone.Collection<GraphModel> = typeof model.getChildren === 'function' ? model.getChildren() : model.get(model.childrenAttribute);
children.forEach(c => {
if (!predicate || predicate(c)) {
result.push(c);
}
const r = findAllDescendants(c, predicate);
if (r && r.length > 0) {
result.push(...r);
}
});
return result;
};
const filterDescendants = (model: GraphModel, filterFn: (graphModel: GraphModel) => boolean) => {
if (!model.isContainer) {
return [];
}
const result: GraphModel[] = [];
let children;
if (typeof model.getChildren === 'function') {
children = model.getChildren();
}
if (model.childrenAttribute) {
children = model.get(model.childrenAttribute);
}
if (!children) {
return [];
}
children.forEach((c: GraphModel) => {
result.push(c);
if (!filterFn(c)) {
return;
}
const r = filterDescendants(c, filterFn);
if (r && r.length > 0) {
result.push(...r);
}
});
return result;
};
export default class TreeDiffController {
configDiff: ConfigDiff;
configuredCollectionsSet: Set<Backbone.Collection>;
filteredDescendants: Map<string, GraphModel>;
descendants: Map<string, GraphModel>;
reqres: Backbone.Radio.Channel;
constructor(options: TreeDiffControllerOptions) {
const { configDiff, graphModel, reqres, nestingOptions, calculateDiff, initialModel } = options;
this.reqres = reqres;
this.configuredCollectionsSet = new Set();
this.__initDescendants(graphModel, nestingOptions, calculateDiff, initialModel);
this.__initConfiguration(configDiff, calculateDiff);
reqres.reply('treeEditor:setWidgetConfig', (widgetId: string, config: NodeConfig) => this.__setNodeConfig(widgetId, config));
}
reset() {
this.configDiff.clear();
this.applyDataToModel({ fromInitialConfig: true });
}
set(configDiff: ConfigDiff) {
this.__passConfigDiff(configDiff);
this.applyDataToModel({});
}
__setNodeConfig(widgetId: string, keyValue: NodeConfig) {
this.configDiff.set(widgetId, keyValue);
}
__initConfiguration(configDiff: ConfigDiff, calculateDiff: boolean) {
if (calculateDiff) {
this.applyModelToDiff();
} else {
this.__passConfigDiff(configDiff);
this.applyDataToModel();
}
}
__passConfigDiff(configDiff: ConfigDiff) {
if (configDiff.initialConfig) {
configDiff.initialConfig.forEach((value, key) => this.configDiff.set(key, configDiff.get(key) || value));
} else {
configDiff.forEach((value, key) => this.configDiff.set(key, value));
}
}
__initDescendants(graphModel: GraphModel, nestingOptions: NestingOptions = {}, calculateDiff: boolean, initialModel: GraphModel) {
const { hasControllerType } = nestingOptions;
const filterFn = (model: GraphModel) => {
const type = model.get('type');
const fieldType = model.get('fieldType');
if (!hasControllerType) {
return true;
}
const result = Array.isArray(hasControllerType)
? hasControllerType.includes(type) || hasControllerType.includes(fieldType)
: type === hasControllerType || fieldType === hasControllerType;
return !result;
};
const filteredDestsArray = filterDescendants(graphModel, filterFn);
this.filteredDescendants = new Map(filteredDestsArray.map((model: GraphModel) => [model.id, model]));
const findAllDescendantsFunc = graphModel.findAllDescendants;
const descendantsArr = typeof findAllDescendantsFunc === 'function' ? findAllDescendantsFunc.call(graphModel) : findAllDescendants(graphModel);
descendantsArr.unshift(graphModel);
const descendants = (this.descendants = new Map(descendantsArr.map((model: GraphModel) => [model.id, model])));
const collectionsSet = new Set();
let initialDescendants = new Map();
if (calculateDiff) {
const initialDescendantsArr = typeof findAllDescendantsFunc === 'function' ? findAllDescendantsFunc.call(initialModel) : findAllDescendants(initialModel);
initialDescendantsArr.push(initialModel);
initialDescendants = new Map(initialDescendantsArr.map((model: GraphModel) => [model.id, model]));
}
descendants.forEach((model: GraphModel) => {
if (model.collection) {
collectionsSet.add(model.collection);
}
});
collectionsSet.forEach((coll: CollectionWithInitIalConfig) => {
if (!coll.initialConfig) {
Object.defineProperty(coll, 'initialConfig', {
writable: false,
value: coll.map(m => m.id)
});
}
});
const reducer = (initialConfig: Map<string, DiffItem>, model: GraphModel, index: number) => {
if (!model.initialConfig) {
const modelToPick = calculateDiff ? initialDescendants.get(model.id) || model : model;
const pick = {
...modelToPick.pick(observableProperties),
index: modelToPick.collection?.indexOf(modelToPick),
};
Object.defineProperty(model, 'initialConfig', {
writable: false,
value: pick
});
}
initialConfig.set(model.id, new DiffItem(model.initialConfig));
return initialConfig;
};
const initialConfig = descendantsArr.reduce(reducer, new Map());
this.configDiff = new ConfigDiff(initialConfig);
const nonUniqueList = findDuplicates(descendantsArr.map((model: GraphModel) => model.id));
if (nonUniqueList.length) {
Core.InterfaceError.logError(`Error: graph models has non-unique ids: ${nonUniqueList}. Unpredictable behavior is possible.`);
}
}
/**
* Apply current state of graph models to diff
* @param properties - property names to pick
*/
applyModelToDiff(properties: string[] = ['width, index', 'isHidden']) {
this.descendants.forEach((model, modelId) => {
const diffObject: Record<string, any> = {};
properties.forEach(property => {
let propertyValue;
switch (property) {
case 'index':
if (model.collection) {
propertyValue = model.collection.indexOf(model);
}
break;
default:
propertyValue = model.get(property);
break;
}
diffObject[property] = propertyValue;
});
this.__setNodeConfig(modelId, diffObject);
});
}
applyInitialConfigToModel(properties: string[]) {
this.applyDataToModel({ properties, fromInitialConfig: true });
}
applyDiffToModel(properties: string[]) {
this.applyDataToModel({ properties });
}
// apply data to graphModel
applyDataToModel({ fromInitialConfig = false, properties = ['index', 'width', 'isHidden'], descendants = this.descendants }
: { fromInitialConfig?: boolean, properties?: string[], descendants?: Map<string, GraphModel> } = {}) {
descendants.forEach((model, modelId) => {
const configMap = this.configDiff.get(modelId) || this.configDiff.initialConfig.get(modelId);
const configObject = configMap.toObject();
if (configMap.size && !fromInitialConfig) {
model.set({ ...configMap.initialConfig, ...configObject });
} else {
model.set(_.pick(configMap.initialConfig, properties));
}
const collection = model.collection;
if (collection) {
const indexObject = fromInitialConfig ? configMap.initialConfig : configObject;
const configIndex = indexObject.index == null ? collection.initialConfig.indexOf(model.id) : indexObject.index;
if (configIndex !== collection.indexOf(model)) {
this.configuredCollectionsSet.add(collection);
}
}
});
this.configuredCollectionsSet.forEach((coll: CollectionWithInitIalConfig) => {
this.__reorderCollectionByIndex(coll, fromInitialConfig);
if (coll.initialConfig.every((id, i) => coll.at(i).id === id)) {
// that means the collection has returned to its initial state
this.configuredCollectionsSet.delete(coll);
}
});
this.reqres.trigger('treeEditor:diffApplied');
}
__reorderCollectionByIndex(collection: CollectionWithInitIalConfig, fromInitialConfig: boolean) {
const getModelsIndex = (id: string) => {
const configMap = this.configDiff.get(id) || this.configDiff.initialConfig.get(id);
const configIndex = fromInitialConfig ? configMap.initialConfig.index : configMap.get('index');
return configIndex != null ? configIndex : collection.initialConfig.indexOf(id);
};
const collectionConfig = collection.map((model: GraphModel) => model.id).sort((a: string, b: string) => getModelsIndex(a) - getModelsIndex(b));
const collIndexes = collection.map(model => model.id);
const groupsToReorder = this.__groupDeviantItems(collectionConfig, collIndexes);
const getConfigIndex = (model: GraphModel) => collectionConfig.indexOf(model.id);
groupsToReorder.map((group: string[]) => {
const modelsGroup = [...collection.filter((model: GraphModel) => group.includes(model.id))];
const insertIndex = collection.indexOf(modelsGroup[0]);
modelsGroup.sort((a: GraphModel, b: GraphModel) => getConfigIndex(a) - getConfigIndex(b));
collection.remove(modelsGroup);
collection.add(modelsGroup, { at: insertIndex });
});
collection.trigger('columns:move', collectionConfig);
}
__groupDeviantItems(deviantArray: Array<string | number | boolean>, templateArray: Array<string | number | boolean>) {
const groups = deviantArray.reduce(
(groupsAccumulator: [string[]], currentId: string, i: number) => {
if (currentId !== templateArray[i]) {
groupsAccumulator[groupsAccumulator.length - 1].push(currentId);
} else {
if (groupsAccumulator[groupsAccumulator.length - 1].length) {
groupsAccumulator.push([]);
}
}
return groupsAccumulator;
},
[[]]
);
return groups.filter((group: string[]) => group.length);
}
}