import {
mergeRecursive,
hasTouch,
filterPropagationModel,
FieldType,
selectElement,
isSimpleObject
} from 'muze-utils';
import { ALL_ACTIONS } from './enums/actions';
import SelectionSet from './selection-set';
import {
initializeBehaviouralActions,
initializeSideEffects,
changeSideEffectAvailability,
initializePhysicalActions,
unionSets,
getSideEffects
} from './helper';
/**
* This class is responsible for dispatching behavioural actions and side effects. It also keeps the information of
* registered physical actions, behavioural actions and side effects. Also, it keeps the map of physical and behavioural
* actions and behavioural actions and side effects. Whenever any behavioural action is dispatched, it also propagates
* the rows which got affected to the other datamodels. This class is initialized by {@link VisualUnit} and legend to
* manage it's interaction.
*
* @public
* @class Firebolt
* @module Firebolt
*/
export default class Firebolt {
constructor (context, actions, sideEffects, behaviourEffectMap) {
this.context = context;
this._sideEffectDefinitions = {};
this._sideEffects = {};
this._propagationInf = {};
this._actions = {
behavioural: {},
physical: {}
};
this._selectionSet = {};
this._volatileSelectionSet = {};
this._propagationFields = {};
this._sourceSideEffects = {
selectionBox: () => false
};
this._propagationBehaviourMap = {};
this._sourceBehaviours = {};
this._actionBehaviourMap = {};
this._config = {};
this._behaviourEffectMap = {};
this._entryExitSet = {};
this._actionHistory = {};
this._queuedSideEffects = {};
this._mappedActions = {};
this.mapSideEffects(behaviourEffectMap);
this.registerBehaviouralActions(actions.behavioural);
this.registerSideEffects(sideEffects);
this.registerPhysicalBehaviouralMap(actions.physicalBehaviouralMap);
this.registerPhysicalActions(actions.physical);
}
config (...config) {
if (config.length) {
const conf = this._config = mergeRecursive(this._config, config[0]);
const sideEffects = this.sideEffects();
for (const key in sideEffects) {
if ({}.hasOwnProperty.call(sideEffects, key)) {
const sideEffectConf = conf[key];
sideEffectConf && sideEffects[key].config(sideEffectConf);
}
}
return this;
}
return this._config;
}
mapSideEffects (behEffectMap) {
const behaviourEffectMap = this._behaviourEffectMap;
for (const key in behEffectMap) {
if ({}.hasOwnProperty.call(behEffectMap, key)) {
const sideEffects = behEffectMap[key] || [];
let preventDefaultActions = false;
let effectNames;
if (isSimpleObject(sideEffects)) {
effectNames = sideEffects.effects;
preventDefaultActions = sideEffects.preventDefaultActions;
} else {
effectNames = sideEffects;
}
!behaviourEffectMap[key] && (behaviourEffectMap[key] = []);
this._behaviourEffectMap[key] = [...new Set(preventDefaultActions ? effectNames :
[...behaviourEffectMap[key], ...effectNames])];
}
}
return this;
}
registerBehaviouralActions (actions) {
const behaviours = initializeBehaviouralActions(this, actions);
this.prepareSelectionSets(behaviours);
Object.assign(this._actions.behavioural, behaviours);
return this;
}
prepareSelectionSets () {
return this;
}
registerSideEffects (sideEffects) {
for (const key in sideEffects) {
this._sideEffectDefinitions[sideEffects[key].formalName()] = sideEffects[key];
}
return this;
}
applySideEffects (sideEffects, selectionSet, payload) {
const sideEffectStore = this.sideEffects();
const actionHistory = this._actionHistory;
const queuedSideEffects = this._queuedSideEffects;
sideEffects.forEach((sideEffect) => {
let options;
let name;
const effects = sideEffect.effects;
const behaviours = sideEffect.behaviours;
const combinedSet = unionSets(this, behaviours, selectionSet);
effects.forEach((effect) => {
if (typeof effect === 'object') {
name = effect.name;
options = effect.options;
} else {
name = effect;
}
const sideEffectInstance = sideEffectStore[name];
if (sideEffectInstance.isEnabled()) {
if (!sideEffectInstance.constructor.mutates() &&
Object.values(actionHistory).some(d => d.isMutableAction)) {
queuedSideEffects[`${name}-${behaviours.join()}`] = {
name,
params: [combinedSet, payload, options]
};
} else {
this.dispatchSideEffect(name, combinedSet, payload, options);
}
}
});
});
return this;
}
dispatchSideEffect (name, selectionSet, payload, options = {}) {
const sideEffectStore = this.sideEffects();
const sideEffect = sideEffectStore[name];
let disable = false;
if (options.filter && options.filter(sideEffect)) {
disable = true;
}
!disable && sideEffectStore[name].apply(selectionSet, payload, options);
}
registerPropagationBehaviourMap (map) {
this._propagationBehaviourMap = Object.assign(this._propagationBehaviourMap, map || {});
return this;
}
dispatchBehaviour (behaviour, payload, propagationInfo = {}) {
const propagate = propagationInfo.propagate !== undefined ? propagationInfo.propagate : true;
const behaviouralActions = this._actions.behavioural;
const action = behaviouralActions[behaviour];
const behaviourEffectMap = this._behaviourEffectMap;
const sideEffects = getSideEffects(behaviour, behaviourEffectMap);
this._propagationInf = propagationInfo;
if (action) {
const selectionSet = action.dispatch(payload);
const propagationSelectionSet = this.getPropagationSelectionSet(selectionSet);
this._entryExitSet[behaviour] = propagationSelectionSet;
const shouldApplySideEffects = this.shouldApplySideEffects(propagate);
if (propagate) {
this.propagate(behaviour, payload, selectionSet.find(d => d.sourceSelectionSet), sideEffects);
}
if (shouldApplySideEffects) {
const applicableSideEffects = this.getApplicableSideEffects(sideEffects, payload, propagationInfo);
this.applySideEffects(applicableSideEffects, propagationSelectionSet, payload);
}
}
return this;
}
getPropagationSelectionSet (selectionSet) {
return selectionSet.find(d => !d.sourceSelectionSet);
}
shouldApplySideEffects () {
return true;
}
changeBehaviourStateOnPropagation (behaviour, value) {
if (value instanceof Function) {
this._sourceBehaviours[behaviour] = value;
} else {
this._sourceBehaviours[behaviour] = () => value;
}
return this;
}
changeSideEffectStateOnPropagation (sideEffect, value) {
if (value instanceof Function) {
this._sourceSideEffects[sideEffect] = value;
} else {
this._sourceSideEffects[sideEffect] = () => value;
}
}
propagate () {
return this;
}
sideEffects (...sideEffects) {
if (sideEffects.length) {
this._sideEffects = sideEffects[0];
return this;
}
return this._sideEffects;
}
enableSideEffects (fn) {
changeSideEffectAvailability(this, fn, true);
return this;
}
disableSideEffects (fn) {
changeSideEffectAvailability(this, fn, false);
return this;
}
dissociateBehaviour (behaviour, physicalAction) {
const actionBehaviourMap = this._actionBehaviourMap;
for (const key in actionBehaviourMap) {
if (key === physicalAction) {
const behaviourMap = actionBehaviourMap[key];
behaviourMap.behaviours = behaviourMap.behaviours.filter(d => d !== behaviour);
}
}
return this;
}
dissociateSideEffect (sideEffect, behaviour) {
const behaviourEffectMap = this._behaviourEffectMap;
behaviourEffectMap[behaviour] = behaviourEffectMap[behaviour].filter(d => (d.name || d) !== sideEffect);
return this;
}
getApplicableSideEffects (sideEffects) {
return sideEffects;
}
attachPropagationListener (dataModel) {
dataModel.unsubscribe('propagation');
dataModel.on('propagation', this.onDataModelPropagation());
return this;
}
onDataModelPropagation () {
return (propValue) => {
const payload = propValue.payload;
const action = payload.action;
this.dispatchBehaviour(action, payload, {
propagate: false
});
};
}
createSelectionSet (uniqueIds, behaviouralActions) {
const behaviours = behaviouralActions || this._actions.behavioural;
const selectionSet = this._selectionSet;
const volatileSelectionSet = this._volatileSelectionSet;
for (const key in behaviours) {
if ({}.hasOwnProperty.call(behaviours, key)) {
selectionSet[key] = new SelectionSet(uniqueIds);
volatileSelectionSet[key] = new SelectionSet(uniqueIds, true);
}
}
this._volatileSelectionSet = volatileSelectionSet;
this.selectionSet(selectionSet);
return this;
}
selectionSet (...selectionSet) {
if (selectionSet.length) {
this._selectionSet = selectionSet[0];
return this;
}
return this._selectionSet;
}
initializeSideEffects () {
const sideEffectDefinitions = this._sideEffectDefinitions;
this.sideEffects(initializeSideEffects(this, sideEffectDefinitions));
return this;
}
registerPhysicalActions (actions) {
const initedActions = initializePhysicalActions(this, actions);
Object.assign(this._actions.physical, initedActions);
return this;
}
/**
* Allows to propagate the datamodel with only the supplied fields. When propagation is done, then the fields
* which are supplied for the specified behavioural action is propagated.
*
* @public
*
* @param {string} action Name of behavioural action. If '*' is specified, then for all behavioural actions it is
* applied.
* @param {Array} fields Array of field names which will be propagated.
* @param {boolean} append If true, then it is appended to the existing propagation data model fields else only
* those fields are projected from propagation data model and propagated.
*
* @return {Firebolt} Instance of firebolt
*/
propagateWith (action, fields, append = false) {
const behaviouralActions = this._actions.behavioural;
if (fields.length) {
if (action === ALL_ACTIONS) {
for (const key in behaviouralActions) {
this._propagationFields[key] = {
fields,
append
};
}
} else {
this._propagationFields[action] = {
fields,
append
};
}
return this;
}
return this._propagationFields;
}
/**
* Map actions and behaviours
* @return {Firebolt} Firebolt instance
*/
mapActionsAndBehaviour () {
const initedPhysicalActions = this._actions.physical;
const map = this._actionBehaviourMap;
const mappedActions = this._mappedActions;
for (const action in map) {
if (!({}).hasOwnProperty.call(action, map)) {
let target;
const mapObj = map[action];
target = mapObj.target;
const touch = mapObj.touch;
if (!target) {
target = this.context.getDefaultTargetContainer();
}
const bind = hasTouch() ? touch === true || touch === undefined : !touch;
const keyName = `${action}-${mapObj.behaviours.join()}`;
bind && !mappedActions[keyName] && this.bindActionWithBehaviour(initedPhysicalActions[action],
target, mapObj.behaviours);
mappedActions[keyName] = true;
}
}
return this;
}
registerPhysicalBehaviouralMap (map) {
Object.assign(this._actionBehaviourMap, map);
return this;
}
/**
* Binds a target element with an action.
*
* @param {Function} action Action method
* @param {string} target Class name of element
* @param {Array} behaviourList Array of behaviours
* @return {FireBolt} Instance of firebolt
*/
bindActionWithBehaviour (action, targets, behaviourList) {
if (typeof (targets) === 'string') {
targets = [targets];
}
targets.forEach((target) => {
const mount = this.context.mount();
const nodes = target.node instanceof Function ? target : selectElement(mount).selectAll(target);
if (behaviourList.length && !nodes.empty()) {
if (nodes instanceof Array) {
nodes.forEach((node) => {
action(selectElement(node), behaviourList);
});
} else {
action(nodes, behaviourList);
}
}
});
return this;
}
getPropagationInf () {
return this._propagationInf;
}
getAddSetFromCriteria (criteria, propagationInf = {}) {
const context = this.context;
const filteredDataModel = propagationInf.data ? propagationInf.data :
context.getDataModelFromIdentifiers(criteria, 'all');
const xFields = context.fields().x || [];
const yFields = context.fields().y || [];
const xMeasures = xFields.every(field => field.type() === FieldType.MEASURE);
const yMeasures = yFields.every(field => field.type() === FieldType.MEASURE);
return {
model: filteredDataModel,
uids: criteria === null ? null : (propagationInf.data ? filterPropagationModel(this.getFullData(),
propagationInf.data[0], xMeasures && yMeasures).getData().uids : filteredDataModel[0].getData().uids)
};
}
getSelectionSets (action) {
const sourceId = this.context.id();
const propagationInf = this._propagationInf || {};
const propagationSource = propagationInf.sourceId;
let applicableSelectionSets = [];
if (propagationSource !== sourceId) {
applicableSelectionSets = [this._volatileSelectionSet[action]];
}
if (propagationSource) {
applicableSelectionSets.push(this.selectionSet()[action]);
}
return applicableSelectionSets;
}
getFullData () {
return this.context.data();
}
resetted () {
return this._resetted;
}
/**
* Returns the entry and exit set information of the specified behavioural action.
*
* @public
*
* @param {string} behaviour Name of behavioural action.
*
* @return {Object} Entry exit set information.
*/
getEntryExitSet (behaviour) {
return this._entryExitSet[behaviour];
}
}