spritejs/sprite-core

View on GitHub
src/modules/actions/node.js

Summary

Maintainability
D
2 days
Test Coverage
import BaseSprite from '../../core/basesprite';

function doActions(ret, target, act) {
  const actions = target.children.map(c => c[act]()).filter(d => d.promise);
  if(ret.promise) {
    actions.unshift(ret);
  }
  if(actions.length) {
    const deferred = {
      promise: Promise.all(actions.map(d => d.promise)),
      resolve() {
        actions.forEach(d => d.resolve());
        return this.promise;
      },
    };
    return deferred;
  }
  return null;
}

const _resolveState = Symbol('resolveState'),
  _changeStateAction = Symbol('changeStateAction'),
  _show = Symbol('show'),
  _hide = Symbol('hide'),
  _enter = Symbol('enter');

const enter = BaseSprite.prototype.enter;
const exit = BaseSprite.prototype.exit;

Object.assign(BaseSprite.prototype, {
  changeState(fromState, toState, action) {
    let animation;
    if(this[_changeStateAction]) {
      const currentAnim = this[_changeStateAction].animation;
      if(this[_changeStateAction].reversable && (currentAnim.playState === 'running' || currentAnim.playState === 'pending')
        && this[_changeStateAction].fromState === toState && this[_changeStateAction].toState === fromState) {
        currentAnim.playbackRate = -currentAnim.playbackRate;
        animation = currentAnim;
        animation.__reversed = this[_changeStateAction].action;
      } else {
        currentAnim.finish();
      }
    }
    if(!animation) {
      // const [_fromState, _toState] = [Object.assign({}, fromState), Object.assign({}, toState)];
      // delete _fromState.__default;
      // delete _toState.__default;
      const _fromState = {},
        _toState = {};
      Object.entries(fromState || {}).forEach(([key, value]) => {
        if(key !== '__default') {
          if(typeof value === 'function') {
            _fromState[key] = this.attr(key);
          } else {
            _fromState[key] = value;
          }
        }
      });
      Object.entries(toState || {}).forEach(([key, value]) => {
        if(key !== '__default') {
          if(typeof value === 'function') {
            _toState[key] = value(this.attr(key));
          } else {
            _toState[key] = value;
          }
        }
      });
      animation = this.animate([_fromState, _toState], Object.assign({fill: 'forwards'}, action));
      animation.finished.then(() => {
        if(this[_changeStateAction] && this[_changeStateAction].animation === animation) delete this[_changeStateAction];
      });
    }
    this[_changeStateAction] = {animation, fromState, toState, action, reversable: action.reversable !== false};
    return animation;
  },
  resolveStates(states, before, after) {
    let currentAnimation = null,
      resolved = false;

    const _states = [];
    let prev = null;
    for(let i = 0; i < states.length; i++) {
      const s = states[i];
      if(prev !== s) {
        prev = s;
        _states.push(s);
      }
    }
    states = _states;

    const _resolveStates = () => {
      this.__ignoreAction = false;
      let fromState = this.attr('state');
      if(fromState === states[0]) {
        states.shift();
      }

      const len = states.length;
      const resolveState = (state, i) => {
        const promise = new Promise((resolve) => {
          this.once(`state-to-${state}`, () => {
            fromState = state;
            if(i === len - 1) { // lastState
              delete this[_resolveState];
            }
            if(after) after.call(this, states);
            resolve(this);
          });
          this.once(`state-from-${fromState}`, ({animation}) => {
            if(animation && resolved) animation.finish();
            else currentAnimation = animation;
          });
          this.attr('state', state);
        });
        return promise;
      };

      let promise = Promise.resolve();
      states.forEach((state, i) => {
        promise = promise.then(() => {
          return resolveState(state, i);
        });
      });

      const ret = {
        get animation() {
          return currentAnimation;
        },
        states,
        resolve() {
          resolved = true;
          if(currentAnimation) currentAnimation.finish();
          return promise;
        },
        promise,
      };
      this[_resolveState] = ret;
      return ret;
    };
    const rs = this[_resolveState];
    if(rs) {
      rs.resolve();
      this.__ignoreAction = true;
      const promise = rs.promise.then(() => {
        if(before) before.call(this, states);
        return _resolveStates().promise;
      });
      return {
        promise,
        resolve() {
          resolved = true;
          if(currentAnimation) currentAnimation.finish();
          return promise;
        },
      };
    }
    if(before) before.call(this, states);
    return _resolveStates();
  },
  // state: original -> show -> hide -> show -> original
  show() {
    if(this[_show]) return this[_show];

    const originalDisplay = this.attr('__originalDisplay') || '';
    const originalState = this.attr('__originalState') || 'default';

    const states = this.attr('states');

    if(states.show) {
      const _st = ['show', originalState];
      if(states.beforeShow) {
        _st.unshift('beforeShow');
      }
      const deferred = this.resolveStates(_st, () => {
        const state = this.attr('state');
        if(state === 'hide') {
          this.once('state-from-hide', () => {
            this.attr('display', originalDisplay);
          });
        }
      });
      deferred.promise = deferred.promise.then(() => {
        if(!this[_hide]) {
          delete this.__attr.__originalDisplay;
          delete this.__attr.__originalState;
          if(states.show.__default) {
            delete states.show;
            this.attr('states', states);
          }
        }
        delete this[_show];
      });
      this[_show] = deferred;
      return deferred;
    }

    const rs = this[_resolveState];
    if(rs) {
      rs.resolve();
      rs.promise.then(() => {
        this.attr('state', originalState);
        this.attr('display', originalDisplay);
      });
      return rs;
    }

    this.attr('state', originalState);
    this.attr('display', originalDisplay);
    return this;
  },
  hide() {
    const state = this.attr('state');
    if(this[_hide] || state === 'hide' || state === 'afterExit' || state === 'beforeExit') return this[_hide];
    const __originalDisplay = this.attr('__originalDisplay');
    if(__originalDisplay == null) {
      const display = this.attr('display');

      this.attr({
        __originalDisplay: display !== 'none' ? display : '',
        __originalState: state !== 'hide' ? state : 'default',
      });
    }

    const states = this.attr('states');

    if(states.hide) {
      const deferred = this.resolveStates(['show', 'hide'], () => {
        if(!states.show) {
          const beforeHide = {__default: true};
          if(states.beforeShow) {
            Object.keys(states.beforeShow).forEach((key) => {
              beforeHide[key] = this.attr(key);
            });
          }
          Object.keys(states.hide).forEach((key) => {
            beforeHide[key] = this.attr(key);
          });
          states.show = beforeHide;
          this.attr('states', states);
        }
      });
      deferred.promise = deferred.promise.then(() => {
        this.attr('display', 'none');
        delete this[_hide];
        return this;
      });
      this[_hide] = deferred;
      return deferred;
    }

    const rs = this[_resolveState];
    if(rs) {
      rs.resolve();
      rs.promise.then(() => {
        this.attr('state', 'hide');
        this.attr('display', 'none');
      });
      return rs;
    }

    this.attr('state', 'hide');
    this.attr('display', 'none');
    return this;
  },
  enter(toState) {
    const states = this.attr('states');
    let ret;
    if(states && (states.beforeEnter || states.afterEnter)) {
      const deferred = this.resolveStates(['beforeEnter', 'afterEnter'], (_states) => {
        const state = this.attr('state');
        _states.push(toState || state);
        if(state !== 'beforeEnter' && state !== 'afterEnter' && (!states.afterEnter || states.afterEnter.__default)) {
          const afterEnter = {__default: true};
          Object.keys(states.beforeEnter).forEach((key) => {
            afterEnter[key] = this.attr(key);
          });
          states.afterEnter = afterEnter;
          this.attr('states', states);
        }
      });
      ret = deferred;
    } else {
      ret = enter.call(this);
    }

    this[_enter] = ret;
    if(this.children) {
      const enterMode = this.attr('enterMode');
      if(enterMode === 'onebyone' || enterMode === 'onebyone-reverse') {
        let promise = null;
        let resolved = false;
        if(ret.promise) {
          promise = ret.promise;
        } else {
          promise = Promise.resolve(this);
        }

        let children = this.children;
        if(enterMode === 'onebyone-reverse') {
          children = [...children].reverse();
        }

        let currentTask = ret;
        children.forEach((c) => {
          const states = c.attr('states');
          if(states && (states.beforeEnter || states.afterEnter)) {
            if(!states.afterEnter || states.afterEnter.__default) {
              const afterEnter = {__default: true};
              Object.keys(states.beforeEnter).forEach((key) => {
                afterEnter[key] = c.attr(key);
              });
              states.afterEnter = afterEnter;
              c.attr('states', states);
            }
          }
          const toState = c.attr('state');
          c.attr('state', 'beforeEnter');
          promise = promise.then(() => {
            const d = c.enter(toState);
            if(d.promise) {
              currentTask = d;
              if(resolved && d.resolve) {
                d.resolve();
              }
              return d.promise;
            }
            return d;
          });
        });

        this[_enter] = {
          promise,
          resolve() {
            if(currentTask && currentTask.resolve) currentTask.resolve();
            resolved = true;
          },
        };
      } else {
        const deferred = doActions(ret, this, 'enter');
        if(deferred) this[_enter] = deferred;
      }
    }

    return this[_enter];
  },
  exit(toState, onbyone = false) {
    const _exit = () => {
      const states = this.attr('states');
      let ret;
      const afterEnter = states.afterEnter || {};
      if(states && (states.beforeExit || states.afterExit)) {
        let state;
        const deferred = this.resolveStates(['beforeExit', 'afterExit'], () => {
          state = this.attr('state');
          if(state !== 'beforeExit' && state !== 'afterExit' && (!states.beforeExit || states.beforeExit.__default)) {
            states.beforeExit = Object.assign({}, afterEnter);
            states.beforeExit.__default = true;
            this.attr('states', states);
          }
        });
        deferred.promise.then(() => {
          if(!onbyone) {
            this.attr(afterEnter);
            this.__attr.quietSet('state', toState || state);
          }
          return this;
        });
        ret = deferred;
      } else {
        const rs = this[_resolveState];
        if(rs) {
          rs.resolve();
          rs.promise.then(() => {
            this.attr(afterEnter);
            return exit.call(this);
          });
          ret = rs;
        } else {
          ret = exit.call(this);
          this.attr(afterEnter);
        }
      }

      if(this.children) {
        const exitMode = this.attr('exitMode');
        if(exitMode === 'onebyone' || exitMode === 'onebyone-reverse') {
          let promise = Promise.resolve(this);
          let resolved = false;

          let children = this.children;
          if(exitMode === 'onebyone-reverse') {
            children = [...children].reverse();
          }

          let currentTask = null;
          children.forEach((c) => {
            const states = c.attr('states');
            if(states && (states.beforeExit || states.afterExit)) {
              if(!states.beforeExit || states.beforeExit.__default) {
                states.beforeExit = Object.assign({}, afterEnter);
                states.beforeExit.__default = true;
                c.attr('states', states);
              }
            }
            const toState = c.attr('state');
            c.attr('state', 'beforeExit');
            promise = promise.then(() => {
              const d = c.exit(toState, true);
              if(d.promise) {
                currentTask = d;
                if(resolved && d.resolve) d.resolve();
                return d.promise;
              }
              return d;
            });
            c.__toState = toState;
          });

          promise = promise.then(() => {
            const p = ret.promise || Promise.resolve(this);
            currentTask = ret;
            return p.then(() => {
              this.children.forEach((c) => {
                const states = c.attr('states');
                c.attr(states.afterEnter);
                c.__attr.quietSet('state', c.__toState);
                delete c.__toState;
              });
            });
          });

          return {
            promise,
            resolve() {
              if(currentTask && currentTask.resolve) currentTask.resolve();
              resolved = true;
            },
          };
        }

        const deferred = doActions(ret, this, 'exit');
        if(deferred) return deferred;
      }

      return ret;
    };

    if(this[_enter] && this[_enter].promise) {
      let resolved = false;
      this[_enter].resolve();
      const promise = this[_enter].promise.then(() => {
        const deferred = _exit();
        if(resolved && deferred.resolve) {
          deferred.resolve();
        }
        return deferred.promise;
      });
      return {
        promise,
        resolve() {
          resolved = true;
        },
      };
    }
    return _exit();
  },
});