minsky-kit
Version:
Kit of components for MINSKY web agency
317 lines (260 loc) • 8.66 kB
JavaScript
// imports
import EventDispatcher from './EventDispatcher';
import Ticker from './Ticker';
// static properties
// class definition
export default class DomStateManager extends EventDispatcher {
// constructor
constructor (args = {}, objectName = 'State MGR') {
// call super
super(args, objectName);
// properties
this.currentState = '';
this.states = {};
this.currentState = null;
this.currentStepIndex = 0;
this.el = args.el;
// set defaults if passed
args.defaults = args.defaults || {};
this.defaults = {
immediate: args.defaults || true,
steps: {
duration: 0,
},
};
// init ticker to use for stepchanges during state change
this.ticker = new Ticker();
this.ticker.on('timeout', this.blm.add('onStateTick', onStateTick));
}
// methods
add (state, steps = null) {
switch (true) {
case (typeof state === 'string' && Array.isArray(steps)):
this.states[state] = {
name: state,
immediate: this.defaults.immediate,
steps,
};
break;
case (Array.isArray(state)):
for (let i = 0; i < state.length; i++) {
this.add(state[i]);
}
break;
default:
for (let i = 0; i < state.steps.length; i++) {
state.steps[i].duration = state.steps[i].duration || this.defaults.steps.duration;
}
this.states[state.name] = {
name: state.name,
steps: state.steps,
immediate: state.immediate || this.defaults.immediate,
};
}
return this;
}
set (state, args = {}) {
let stateDef = {};
const immediate = args.immediate ? args.immediate : null;
const instant = args.instant ? args.instant : null;
// block if el is not set
if (!this._el) {
this.log('State could not be set, el is unknown');
return false;
}
// cancel state transition
if (this.ticker.running) {
this.ticker.stop(true);
this.currentStepIndex = 0;
}
// get clone of state (as values may be changed)
if (this.states[state]) {
stateDef = { ...this.states[state] };
}
else {
this.log('Passed state (', state, ') is not present in state definitions');
return undefined;
}
// apply changes if necessary && set flag
if (immediate) {
stateDef.immediate = immediate;
stateDef.alteredOnSet = true;
}
// set current def
this.currentState = stateDef;
// set currentStepIndex
if (!instant) {
this.currentStepIndex = 0;
}
else {
this.currentStepIndex = this.currentState.steps.length - 1;
}
// start ticker
runStep.call(this, this.currentState.steps[this.currentStepIndex]);
return this;
}
get (state) {
return this.states[state];
}
nextStep () {
this.log('Next step called');
// verify if current step is known and next step is present
let step;
if (this.currentState) {
step = this.currentState.steps[++this.currentStepIndex];
if (!step) return false;
}
this.log('Next step found', step);
runStep.call(this, step);
// return self to notifie successfull step change
return this;
}
tick () {
this.log('Tick');
// use current step getter to apply its settings
const { currentStep } = this;
if (currentStep) {
if (currentStep.removeClass) cssClassAction(this.el, currentStep.removeClass, 'remove');
if (currentStep.addClass) cssClassAction(this.el, currentStep.addClass, 'add');
if (currentStep.removeAttribute) attributeAction(this.el, currentStep.removeAttribute, 'remove');
if (currentStep.addAttribute) attributeAction(this.el, currentStep.addAttribute, 'add');
// event data object
const evtData = {
index: this.currentStep,
step: currentStep,
state: this.currentState,
};
// defines events to dispatch
const events = [currentStep.name, 'stepChange'];
if (!this.nextStep()) {
events.push(this.currentState.name);
events.push('stateChange');
}
// dispatch event
this.dispatch(events, evtData);
}
}
destroy () {
// destroy instances (listners will die with it)
this.ticker.destroy();
// clear arrays & object references
this.states.length = 0;
this.states = this.currentState = null;
// destroy super
super.destroy();
}
// getters & setters
get state () {
return this.currentState;
}
set state (value) {
this.activate(value);
}
get currentStep () {
return (this.currentState) ? this.currentState.steps[this.currentStepIndex] : null;
}
get running () {
return this.ticker.running;
}
get allClasses () {
// loop over states & steps to list all possible classes the instance may use
let all = [];
for (const key of Object.keys(this.states)) {
for (const step of this.states[key].steps) {
if (step.removeClass) all = all.concat(step.removeClass);
if (step.addClass) all = all.concat(step.addClass);
}
}
// return filtered all
return all.filter((value, index, self) => self.indexOf(value) === index);
}
get el () {
return this._el;
}
set el (value) {
if (this._el !== value) {
// disconnect el if one was already in use
if (this._el) {
if (this.allClasses) {
// remove all classes
this._el.classList.remove(...this.allClasses);
}
}
// change the el
this._el = value;
// grab current step
const { currentStep } = this;
// apply current state/step immediately
if (this._el && currentStep) {
if (currentStep.removeClass) this._el.classList.remove(currentStep.removeClass);
if (currentStep.addClass) this._el.classList.add(currentStep.addClass);
}
}
}
// statics
}
// handlers and helper functions
function onStateTick () {
this.tick();
}
function runStep (step) {
this.log('Run step', step);
// prepare ticker for next step and run
this.ticker.timeout = step.duration;
this.ticker.start(true);
}
function cssClassAction (el, cssClass, action) {
// loop over all items if array is provided
// and self execute
if (Array.isArray(cssClass)) {
for (let i = 0; i < cssClass.length; i++) {
cssClassAction(el, cssClass[i], action);
}
// block further logic
return;
}
// apply action
switch (action) {
case 'add': el.classList.add(cssClass); break;
case 'remove': el.classList.remove(cssClass); break;
default:
}
}
function attributeAction (el, attribute, action) {
// loop over all items if array is provided
// and self execute
if (Array.isArray(attribute)) {
for (let i = 0; i < attribute.length; i++) {
attributeAction(el, attribute[i], action);
}
// block further logic
return;
}
if (typeof attribute === 'object') {
// loop over all properties
// and self execute
for (const p of Object.keys(attribute)) {
attributeAction(el, p, attribute[p]);
}
}
// apply action
switch (action) {
case 'add': el.setAttribute(attribute); break;
case 'remove': el.removeAttribute(attribute); break;
default:
}
}
/*
{
name: '',
immediate: true,
steps: [
{
name: '',
cssClass: '', // or array of classes (will join anyway)
attributes: {} // or array of objects,
duration: 0
}
]
}
*/