UNPKG

minsky-kit

Version:
317 lines (260 loc) 8.66 kB
// 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 } ] } */