Source: muze/src/action-model.js

import { mergeRecursive } from 'muze-utils';

const defaultPolicy = (registrableComponents) => {
    const aliases = registrableComponents.map(comp => comp.alias());
    return {
        behaviours: {
            '*': (propagationPayload) => {
                const propagationCanvas = propagationPayload.sourceCanvas;
                return propagationCanvas ? aliases.indexOf(propagationCanvas) !== -1 : true;
            }
        }
    };
};

/**
 * This class is initiated only once in lifecycle and is reponsible for regisration of physical and behavioural
 * actions and side effects and the mapping between them.
 *
 * To get the instance of action model
 * ```
 *  const ActionModel = muze.ActionModel;
 * ```
 *
 * @public
 * @module ActionModel
 */
class ActionModel {
    constructor () {
        this._registrableComponents = [];
    }

    /**
     * Takes an array of canvases on which the physical and behavioural actions will get registered.
     *
     * @public
     * @param  {Canvas} components Array of canvases
     *
     * @return {ActionModel} Instance of action model.
     */
    for (...components) {
        this._registrableComponents = components;
        return this;
    }

    /**
     * Registers physical actions on the canvases. It takes an object with key as the name of action and value having
     * the definition of the action.
     *
     * To register a {@link PhysicalAction},
     * ```
     *  const ActionModel = muze.ActionModel;
     *  ActionModel
     *       // Physical actions will be registered on these canvases.
     *      .for(canvas)
     *      .registerPhysicalActions({
     *          // Key is the name of physical action.
     *          ctrlClick: (firebolt) => (targetEl, behaviours) => {
     *              targetEl.on('click', function (data) {
     *                  const event = utils.getEvent();
     *                  const pos = utils.getClientPoint(event, this);
     *                  // Get the data point nearest to the mouse position.
     *                  const nearestPoint = firebolt.context.getNearestPoint(pos, {
     *                      data
     *                  });
     *                  // Prepare the payload
     *                  const payload = {
     *                      criteria: nearestPoint.id
     *                  };
     *                  behaviours.forEach((behaviour) => firebolt.dispatchBehaviour(behaviour, payload));
     *              });
     *          }
     *      });
     * ```
     * @public
     *
     * @param {Object} action Names of physical actions and their definitions.
     *
     * @return {ActionModel} Instance of the action model.
     */
    registerPhysicalActions (action) {
        const canvases = this._registrableComponents;

        canvases.forEach((canvas) => {
            canvas.once('canvas.updated').then((args) => {
                const matrix = args.client.composition().visualGroup.matrixInstance().value;
                matrix.each(cell => cell.valueOf().firebolt().registerPhysicalActions(action));
            });
        });
        return this;
    }

    /**
     * Registers behavioural actions on the canvases. It takes definitions of the behavioural actions and registers
     * them on the canvases. Every behavioural action must inherit the {@link GenericBehaviour} class.
     *
     * To register a behavioural action
     * ```
     *  // Define a new behavioural action
     *  class SingleSelectBehaviour extends GenericBehaviour {
     *      static formalName () {
     *          return 'singleSelect';
     *      }
     *
     *      setSelectionSet (addSet, selectionSet) {
     *          if (addSet === null) {
     *              selectionSet.reset();
     *          } else if (addSet.length) {
     *              const existingAddSet = selectionSet.getExistingEntrySet(addSet);
     *              if (existingAddSet.length){
     *                  selectionSet.reset();
     *              } else {
     *               selectionSet.add(addSet);
     *              }
     *          } else {
     *              selectionSet.reset();
     *          }
     *      }
     * }
     * muze.ActionModel.registerBehaviouralActions(SingleSelectBehaviour);
     * ```
     *
     * @public
     *
     * @param {GenericBehaviour} actions Definition of behavioural actions.
     *
     * @return {ActionModel} Instance of action model.
     */
    registerBehaviouralActions (...actions) {
        const canvases = this._registrableComponents;

        canvases.forEach((canvas) => {
            canvas.once('canvas.updated').then(() => {
                const matrix = canvas.composition().visualGroup.matrixInstance().value;
                matrix.each(cell => cell.valueOf().firebolt().registerBehaviouralActions(...actions));
            });
        });
        return this;
    }

    /**
     * Registers the mapping of physical and behavioural actions. This mapping is used to establish which behavioural
     * actions should be dispatched on any triggering of physical actions.
     *
     * To map physical actions with behavioural actions,
     * ```
     *  muze.ActionModel.registerPhysicalBehaviouralMap({
     *      ctrlClick: {
     *          behaviours: ['select'] // This array must be the formal names of the behavioural actions.
     *      }
     *  });
     * ```
     *
     * @public
     * @param {Object} map Contains the physical and behavioural action map.
     * ```
     *   {
     *      // Name of the physical action
     *      click: {
     *          // Target element selector. This is an optional field. If not passed, then by default takes the
     *          // container element of visual unit.
     *          target: '.muze-layers-area path',
     *          // Behaviours which should be dispatched on triggering of the mapped physical action.
     *          behaviours: ['select']
     *      }
     *   }
     * ```
     *
     * @return {ActionModel} Instance of action model.
     */
    registerPhysicalBehaviouralMap (map) {
        const canvases = this._registrableComponents;

        canvases.forEach((canvas) => {
            canvas.once('canvas.updated').then((args) => {
                const matrix = args.client.composition().visualGroup.matrixInstance().value;
                matrix.each(cell => cell.valueOf().firebolt().registerPhysicalBehaviouralMap(map));
            });
        });
        return this;
    }

    /**
     * Registers what behaviour to propagate on dispatch of a certain behavioural action. By default, when any
     * behavioural action is dispatched, then the same behavioural action gets propagated to all the other canvases.
     * This can be changed using this api.
     *
     * To register what behaviour should be propagated on dispatch of any behavioural action,
     * ```
     *  ActionModel.for(canvas1, canvas2).registerPropagationBehaviourMap({
     *      select: 'filter',
     *      brush: 'filter'
     *  });
     * ```
     *
     * @public
     * @param {Object} map Propagation behaviour map.
     *
     * @return {ActionModel} Instance of action model.
     */
    registerPropagationBehaviourMap (map) {
        const canvases = this._registrableComponents;

        canvases.forEach((canvas) => {
            canvas.once('canvas.updated').then((args) => {
                const matrix = args.client.composition().visualGroup.matrixInstance().value;
                matrix.each(cell => cell.valueOf().firebolt().registerPropagationBehaviourMap(map));
            });
        });
        return this;
    }

    /**
     * Registers the mapping of side effects and behavioural actions. It takes an object which contains key as the
     * name of behavioural action and the side effects which will be linked to it.
     *
     * To map side effects to select behaviour,
     * ```
     *  muze.ActionModel.mapSideEffects({
     *      select: ['infoBox'] // This array must be the formal names of the side effects
     *  });
     * ```
     *
     * To map side effects to select behaviour, but disable all the default side effects attached with this behaviour,
     * ```
     *  muze.ActionModel.mapSideEffects({
     *      select: {
     *          effects: ['infoBox'],
     *          preventDefaultActions: true
     *      }
     *  });
     * @public
     * @param {Object} map Mapping of behavioural actions and side effects.
     * ```
     *     {
     *          select: ['infoBox']
     *     }
     * ```
     * @return {ActionModel} Instance of action model.
     */
    mapSideEffects (map) {
        const canvases = this._registrableComponents;

        canvases.forEach((canvas) => {
            canvas.once('canvas.updated').then(() => {
                const matrix = canvas.composition().visualGroup.matrixInstance().value;
                matrix.each(cell => cell.valueOf().firebolt().mapSideEffects(map));
            });
        });
        return this;
    }

    /**
     * Registers the side effects on the registered canvases. It takes definitions of side effects and registers them
     * on the canvases. Every side effect must inherit the base class {@link GenericSideEffect} or
     * {@link SpawnableSideEffect} or {@link SurrogateSideEffect} class.
     *
     * ```
     * // Define a custom side effect
     *  class InfoBox extends SpawnableSideEffect {
     *      static formalName () {
     *          return 'infoBox';
     *      }
     *
     *      apply (selectionSet) {
     *      }
     *  }
     *  muze.ActionModel.registerSideEffects(InfoBox);
     * ```
     * @public
     * @param  {GenericSideEffect} sideEffects Definition of side effects.
     *
     * @return {ActionModel} Instance of action model.
     */
    registerSideEffects (...sideEffects) {
        const registrableComponents = this._registrableComponents;

        registrableComponents.forEach((canvas) => {
            canvas.once('canvas.updated').then((args) => {
                const matrix = args.client.composition().visualGroup.matrixInstance().value;
                matrix.each(cell => cell.valueOf().firebolt().registerSideEffects(sideEffects));
            });
        });

        return this;
    }

    /**
     * Breaks the link between behavioural actions and physical actions. It takes an array of behavioural action
     * and it's physical action.
     *
     * To dissociate behavioural actions from physical actions
     * ```
     *  muze.ActionModel.dissociateBehaviour(['select', 'click'], ['highlight', 'hover']);
     * ```
     * @public
     * @param  {Array} maps Array of behavioural action and physical action.
     *
     * @return {ActionModel} Instance of action model.
     */
    dissociateBehaviour (...maps) {
        const registrableComponents = this._registrableComponents;

        registrableComponents.forEach((canvas) => {
            canvas.once('canvas.updated').then((args) => {
                const matrix = args.client.composition().visualGroup.matrixInstance().value;
                matrix.each((cell) => {
                    maps.forEach(val => cell.valueOf().firebolt().dissociateBehaviour(val[0], val[1]));
                });
            });
        });

        return this;
    }

    /**
     * Breaks the link between side effects and behavioural actions. It takes an array of formal names of the side
     * effects and it's linked behavioural action.
     *
     * To dissociate side effects from behavioural actions
     * ```
     *  muze.ActionModel.dissociateSideEffect(['crossline', 'highlight'], ['selectionBox', 'brush']);
     * ```
     * @public
     * @param  {Array} maps Array of side effects and behavioural actions.
     *
     * @return {ActionModel} Instance of action model.
     */
    dissociateSideEffect (...maps) {
        const registrableComponents = this._registrableComponents;

        registrableComponents.forEach((canvas) => {
            canvas.once('canvas.updated').then((args) => {
                const matrix = args.client.composition().visualGroup.matrixInstance().value;
                matrix.each((cell) => {
                    maps.forEach(val => cell.valueOf().firebolt().dissociateSideEffect(val[0], val[1]));
                });
            });
        });

        return this;
    }

    /**
     * By default cross interactivity is disabled between canvases. This enables cross interaction between the canvases
     * so that any action happening in one canvas gets dispatched on other canvases as well. An optional policy can also
     * be passed in this method. The policy can be used to control on which canvases the behavioural actions and
     * side effects gets dispatched.
     *
     * To just enable cross interactivity between two canvases,
     * ```
     *  ActionModel.for(canvas1, canvas2)
     *      .enableCrossInteractivity();
     * ```
     *
     * To enable cross interactivity but enable any behavioural action only when it is triggered from canvas1.
     * ```
     *  ActionModel.for(canvas1, canvas2)
     *      .enableCrossInteractivity({
     *          behaviours: {
     *              // Here * stands for any behavioural action name. We can also give any name of behavioural action
     *              // in place of this.
     *              '*': (propPayload, context) => {
     *                  return propPayload.sourceCanvas === canvas1.alias();
     *              }
     *          }
     *      });
     * ```
     *
     * To enable cross interactivity but enable tooltip effect if the action is propagated from canvas1,
     * ```
     *  ActionModel.for(canvas1, canvas2)
     *      .enableCrossInteractivity({
     *          sideEffects: {
     *              tooltip: (propPayload, context) => {
     *                  return propPayload.sourceCanvas === canvas1.alias();
     *              }
     *          }
     *      });
     * ```
     * @public
     * @param {Object} policy Policy definition.
     *
     * @return {ActionModel} Instance of action model.
     */
    enableCrossInteractivity (policy = {}) {
        const registrableComponents = this._registrableComponents;
        const mergedPolicy = mergeRecursive(mergeRecursive({}, defaultPolicy(registrableComponents)), policy);

        registrableComponents.forEach((canvas) => {
            canvas.firebolt().crossInteractionPolicy(mergedPolicy);
        });

        return this;
    }
}

export const actionModel = (() => new ActionModel())();