aurelia/aurelia

View on GitHub
packages/__tests__/src/router/_shared/transition-viewport.ts

Summary

Maintainability
F
4 days
Test Coverage
import { DeferralJuncture, SwapStrategy } from './create-fixture.js';
import { Component } from './component.js';
import { HookName, MaybeHookName } from './hook-invocation-tracker.js';
import { Transition } from './transition.js';

export class TransitionViewport {
  public hooks: string[] = [];

  public canUnload: boolean = true;
  public canLoad: boolean = true;
  public unloading: boolean = true;
  public loading: boolean = true;
  public deactivate: boolean = true;

  public static routingHooks: HookName[] = ['canUnload', 'canLoad', 'unloading', 'loading'];
  public static addHooks: HookName[] = ['binding', 'bound', 'attaching', 'attached'];
  public static removeHooks: HookName[] = ['detaching', 'unbinding', 'dispose'];

  public static getPrepended(prefix: string, component: string, ...hooks: (HookName | '')[]) {
    return hooks.map(hook => hook !== '' ? `${prefix}.${component}.${hook}` : '');
  }

  public static getInterweaved(...lists) {
    const hooks = [];
    while (lists.length > 0) {
      for (let i = 0, ii = lists.length; i < ii; ++i) {
        const list = lists[i];
        if (list.length === 0) {
          lists.splice(i, 1);
          --i;
          --ii;
        } else {
          let value;
          do {
            value = list.shift();
            if (value !== void 0) {
              hooks.push(value);
            }
          } while (value);
        }
      }
    }
    return hooks;
  }

  public static applyDelays(deferUntil: DeferralJuncture, viewports: TransitionViewport[], addViewports: TransitionViewport[], removeViewports: TransitionViewport[]) {
    let delayed: boolean;
    let guard = 100;
    do {
      let before = JSON.parse(JSON.stringify(viewports.map(viewport => viewport.hooks)));
      delayed = false;

      delayed = TransitionViewport.ensureViewportHookOrder(removeViewports, addViewports) || delayed;
      if (delayed) {
        console.log('delayed within viewport', before, JSON.parse(JSON.stringify(viewports.map(viewport => viewport.hooks))));
        before = JSON.parse(JSON.stringify(viewports.map(viewport => viewport.hooks)));
        delayed = false;

      }

      delayed = TransitionViewport.ensureConfiguredHookOrder(deferUntil, viewports, removeViewports, addViewports) || delayed;
      if (delayed) {
        console.log('delayed between viewports', before, JSON.parse(JSON.stringify(viewports.map(viewport => viewport.hooks))));
      }
      guard--;
    } while (delayed && guard > 0);
  }

  public static delayHook(earlierViewport: TransitionViewport | string[], laterViewport: TransitionViewport | string[], hook: HookName): boolean {
    const check = `.${hook}`;

    const earlierViewportHooks = earlierViewport instanceof TransitionViewport ? earlierViewport.hooks : earlierViewport;
    const laterViewportHooks = laterViewport instanceof TransitionViewport ? laterViewport.hooks : laterViewport;

    let index = TransitionViewport.findLastIndex(earlierViewportHooks, item => item !== void 0 && item.endsWith(check));
    if (index === -1) {
      return false;
    }
    const earlierTick = TransitionViewport.getTick(earlierViewportHooks, index);

    index = laterViewportHooks.findIndex(item => item && item.endsWith(check));
    if (index === -1) {
      return false;
    }
    const laterTick = TransitionViewport.getTick(laterViewportHooks, index);
    if (earlierTick > laterTick) {
      // TODO: Might want to find previous blank first
      const insert = new Array(earlierTick - laterTick + 1).fill('');
      laterViewportHooks.splice(index, 0, ...insert);
      return true;
    }
    return false;
  }

  public constructor(public readonly transition: Transition, public readonly isTop: boolean) {
    if (transition.from.isEmpty) {
      this.canUnload = false;
      this.unloading = false;
      this.deactivate = false;
    }
    if (transition.to.isEmpty) {
      this.canLoad = false;
      this.loading = false;
    }
  }

  public get from(): Component {
    return this.transition.from;
  }
  public get to(): Component {
    return this.transition.to;
  }

  public get isAdd(): boolean {
    return !this.to.isEmpty;
  }
  public get isRemove(): boolean {
    return !this.from.isEmpty;
  }

  // Get and remove the hooks so far, but keep the final blanks/ticks
  public retrieveHooks(): string[] {
    let lastNonBlank = this.hooks.length - 1;
    while (this.hooks[lastNonBlank] === '' && lastNonBlank >= 0) {
      lastNonBlank--;
    }
    lastNonBlank = lastNonBlank < 0 ? this.hooks.length : lastNonBlank + 1;

    return this.hooks.splice(0, lastNonBlank);
  }

  // public setRoutingHooks(deferUntil: DeferralJuncture, swapStrategy: SwapStrategy, componentKind: 'all-sync' | 'all-async', phase: string, hook, transitions) {
  //   const hooks = getInitialViewports(transitions);

  //   switch (hook) {
  //     case 'canUnload':
  //       for (let i = transitions.length - 1; i >= 0; i--) {
  //         const { from } = transitions[i];

  //         const j = transitions.length - 1 - i;
  //         if (!from.isEmpty) {
  //           hooks[j].hooks.push(...getPrepended(phase, from.name, ...from.getTimed(hook)));
  //         }
  //       }
  //       break;
  //     case 'unloading':
  //       if (deferUntil === 'load-hooks') {
  //         for (let i = transitions.length - 1; i >= 0; i--) {
  //           const { from } = transitions[i];

  //           const j = transitions.length - 1 - i;
  //           if (!from.isEmpty) {
  //             hooks[j].hooks.push(...getPrepended(phase, from.name, ...from.getTimed(hook)));
  //           }
  //         }
  //       }
  //       break;
  //     case 'canLoad':
  //       {
  //         const len = deferUntil === 'guard-hooks' || deferUntil === 'load-hooks' ? transitions.length : 1;
  //         for (let i = 0; i < len; i++) {
  //           const { to } = transitions[i];

  //           if (!to.isEmpty) {
  //             hooks[0].hooks.push(...getPrepended(phase, to.name, ...to.getTimed(hook)));
  //           }
  //         }
  //       }
  //       break;
  //     case 'loading':
  //       {
  //         const len = deferUntil === 'load-hooks' ? transitions.length : 1;
  //         for (let i = 0; i < len; i++) {
  //           const { to } = transitions[i];

  //           if (!to.isEmpty) {
  //             hooks[0].hooks.push(...getPrepended(phase, to.name, ...to.getTimed(hook)));
  //           }
  //         }
  //       }
  //       break;
  //   }

  //   return hooks;
  // }

  // Set the appropriate routing hooks either during the routing step or the lifecycle step
  public setRoutingHooks(deferUntil: DeferralJuncture, phase: string, routingStep: boolean, topViewport: TransitionViewport, removeViewports: TransitionViewport[]) {
    // canUnload is always known and where it starts so it's always added
    if (routingStep && this.canUnload) {
      if (this.isTop) {
        // TransitionViewport.setRemoveHooks(deferUntil, phase, 'canUnload', false, topViewport, removeViewports);
        TransitionViewport.getRemoveHooks(deferUntil, phase, 'canUnload', topViewport, removeViewports).forEach(hooks => this.hooks.push(...hooks));
      }
      this.canUnload = false;
    }

    if (!routingStep || deferUntil === 'guard-hooks' || deferUntil === 'load-hooks' && this.canLoad) {
      this.setRoutingHook(phase, 'canLoad');
      if (deferUntil === 'guard-hooks' || deferUntil === 'load-hooks') {
        this.hooks.push('');
      }
      this.canLoad = false;
    }

    if (!routingStep || deferUntil === 'load-hooks' && this.unloading) {
      //   if (deferUntil === 'guard-hooks') {
      //     this.setRoutingHook(phase, 'unloading');
      //     this.hooks.push('');
      // } else {
      if (this.isTop) {
        // TransitionViewport.setRemoveHooks(deferUntil, phase, 'unloading', false, topViewport, removeViewports);
        TransitionViewport.getRemoveHooks(deferUntil, phase, 'unloading', topViewport, removeViewports).forEach(hooks => this.hooks.push(...hooks));
      }
      // }
      this.unloading = false;
    }

    if (!routingStep || deferUntil === 'load-hooks' && this.loading) {
      this.setRoutingHook(phase, 'loading');
      if (deferUntil === 'load-hooks') {
        this.hooks.push('');
      }
      this.loading = false;
    }
  }

  // Set the remaining routing hooks and the lifecycle hooks
  public setLifecycleHooks(deferUntil: DeferralJuncture, swapStrategy: SwapStrategy, phase: string, topViewport: TransitionViewport, removeViewports: TransitionViewport[]) {
    const { from, to } = this.transition;

    // Set the remaining routing hooks
    this.setRoutingHooks(deferUntil, phase, false, topViewport, removeViewports);

    switch (swapStrategy) {
      case 'parallel-remove-first':
        {
          const hooks = [];
          if (!from.isEmpty && this.isTop) {
            // hooks.push(...TransitionViewport.getRemoveHooks(deferUntil, phase, 'deactivate', topViewport, removeViewports));
            hooks.push(...TransitionViewport.getDeactivateHooks(phase, topViewport, removeViewports));
          }
          if (!to.isEmpty) {
            hooks.push(TransitionViewport.getPrepended(phase, to.name, ...to.getTimed(...TransitionViewport.addHooks)));
          }
          this.hooks.push(...TransitionViewport.getInterweaved(...hooks));
        }
        break;
      case 'sequential-add-first':
        // this.hooks.push(...getInterweaved(
        //   to ? getPrepended(phase, to.name, ...to.getTimed(...addHooks)) : [],
        //   from ? getPrepended(phase, from.name, ...from.getTimed(...removeHooks)) : [],
        // ));
        if (!to.isEmpty) {
          this.hooks.push(...TransitionViewport.getPrepended(phase, to.name, ...to.getTimed(...TransitionViewport.addHooks)));
        }
        // if (!from.isEmpty) { this.hooks.push(...TransitionViewport.getPrepended(phase, from.name, ...from.getTimed(...TransitionViewport.removeHooks))); }
        if (!from.isEmpty && this.isTop) {
          // TransitionViewport.getRemoveHooks(deferUntil, phase, 'deactivate', topViewport, removeViewports).forEach(hooks => this.hooks.push(...hooks));
          TransitionViewport.getDeactivateHooks(phase, topViewport, removeViewports).forEach(hooks => this.hooks.push(...hooks));
        }
        break;
      case 'sequential-remove-first':
        // if (!from.isEmpty) { this.hooks.push(...TransitionViewport.getPrepended(phase, from.name, ...from.getTimed(...TransitionViewport.removeHooks))); }
        if (!from.isEmpty && this.isTop) {
          // TransitionViewport.getRemoveHooks(deferUntil, phase, 'deactivate', topViewport, removeViewports).forEach(hooks => this.hooks.push(...hooks));
          TransitionViewport.getDeactivateHooks(phase, topViewport, removeViewports).forEach(hooks => this.hooks.push(...hooks));
        }
        if (!to.isEmpty) {
          this.hooks.push(...TransitionViewport.getPrepended(phase, to.name, ...to.getTimed(...TransitionViewport.addHooks)));
        }
        break;
    }
  }

  private setRoutingHook(phase: string, hook: HookName, onlyDelay = false): string[] {
    if (this[hook] as boolean) {
      const component = hook === 'canUnload' || hook === 'unloading' ? this.from : this.to;
      const hooks = TransitionViewport.getPrepended(phase, component.name, ...component.getTimed(hook));
      if (onlyDelay) {
        this.hooks.push('', ...hooks.slice(1));
      } else {
        this.hooks.push(...hooks);
      }
      this[hook] = false;
      return hooks;
    }
    return [];
  }

  private setDeactivateHook(phase: string, onlyDelay = false): string[] {
    if (this.deactivate as boolean) {
      const { from } = this;
      const hooks = TransitionViewport.getPrepended(phase, from.name, ...from.getTimed(...TransitionViewport.removeHooks));
      if (onlyDelay) {
        this.hooks.push(...(new Array(hooks.length).fill('')));
      } else {
        this.hooks.push(...hooks);
      }
      this.deactivate = false;
      return hooks;
    }
    return [];
  }

  private static getRemoveHooks(deferUntil: DeferralJuncture, phase: string, hook: HookName | 'deactivate', topViewport: TransitionViewport, removeViewports: TransitionViewport[]): string[][] {
    const viewportHooks = [];
    if (topViewport === void 0) {
      return [];
    }
    for (const viewport of removeViewports) {
      if (viewport === void 0) {
        continue;
      }
      let prevLen: number;
      // If it's the top viewport, it'll get hooks added so note original length...
      if (viewport.isTop) {
        prevLen = viewport.hooks.length;
      }
      viewportHooks.push(hook === 'deactivate'
        ? viewport.setDeactivateHook(phase, !viewport.isTop)
        : viewport.setRoutingHook(phase, hook, !viewport.isTop));
      // ...and remove the added hooks
      if (viewport.isTop) {
        viewport.hooks.splice(prevLen);
      }
    }
    // The deactivate hooks are syncing on slowest hook
    // if (hook === 'deactivate') {
    // }
    if ((hook === 'canUnload' && deferUntil === 'guard-hooks') || deferUntil === 'load-hooks') {
      viewportHooks.push(['']);
    }
    return viewportHooks;
  }

  private static getDeactivateHooks(phase: string, topViewport: TransitionViewport, removeViewports: TransitionViewport[]): string[][] {
    const deactivateHooks = [...TransitionViewport.removeHooks];
    const dispose = deactivateHooks.pop();
    const viewportHooks = [];
    if (topViewport === void 0) {
      return [];
    }
    for (const hook of deactivateHooks) {
      const maxTiming = Math.max(...removeViewports.map(viewport => viewport.from.getTiming(hook)));
      for (const viewport of removeViewports) {
        const { from } = viewport;
        viewportHooks.push(...TransitionViewport.getPrepended(phase, from.name, hook));
      }
      viewportHooks.push(...Array(removeViewports.length + ((maxTiming + 1) * removeViewports.length)).fill(''));
    }
    for (let i = removeViewports.length - 1; i >= 0; i--) {
      const { from } = removeViewports[i];
      viewportHooks.push(...TransitionViewport.getPrepended(phase, from.name, dispose));
    }
    return [viewportHooks];
  }

  private static ensureConfiguredHookOrder(deferUntil: DeferralJuncture, viewports: TransitionViewport[], removeViewports: TransitionViewport[], addViewports: TransitionViewport[]): boolean {
    let delayed = false;

    delayed = TransitionViewport.delayHooks(viewports, 'canUnload', 'canLoad') || delayed;
    delayed = TransitionViewport.delayHooks(viewports, 'canUnload', 'unloading') || delayed;
    delayed = TransitionViewport.delayHooks(viewports, 'canUnload', 'loading') || delayed;

    // for (let i = 0; i <= removeViewports.length - 2; i++) {
    //   if (delayHooks(removeViewports, `${removeViewports[i].from.name}.canUnload`, `${removeViewports[i + 1].from.name}.canUnload`)) {
    //     delayed = true;
    //     // console.log('delaying canUnload', removeViewports[i].from.name, removeViewports);
    //   }
    // }

    if (deferUntil === 'guard-hooks' || deferUntil === 'load-hooks') {
      delayed = TransitionViewport.delayHooks(viewports, 'canLoad', 'unloading') || delayed;
      delayed = TransitionViewport.delayHooks(viewports, 'canLoad', 'loading') || delayed;

      for (let i = 0; i <= addViewports.length - 2; i++) {
        delayed = TransitionViewport.delayHook(addViewports[i], addViewports[i + 1], 'canLoad') || delayed;
      }
    }

    if (deferUntil === 'load-hooks') {
      delayed = TransitionViewport.delayHooks(viewports, 'unloading', 'loading') || delayed;

      // for (let i = 0; i <= removeViewports.length - 2; i++) {
      //   if (delayHooks(removeViewports, `${removeViewports[i].from.name}.unloading`, `${removeViewports[i + 1].from.name}.unloading`)) {
      //     // console.log('delaying unload', removeViewports[i].from.name, removeViewports);
      //   }
      // }

      for (let i = 0; i <= addViewports.length - 2; i++) {
        delayed = TransitionViewport.delayHook(addViewports[i], addViewports[i + 1], 'loading') || delayed;
      }
      for (let i = 0; i <= addViewports.length - 2; i++) {
        delayed = TransitionViewport.delayHook(addViewports[i], addViewports[i + 1], 'binding') || delayed;
      }
    }

    return delayed;
  }

  private static ensureViewportHookOrder(removeViewports: TransitionViewport[], addViewports: TransitionViewport[]): boolean {
    const minLength = Math.min(removeViewports.length, addViewports.length);
    let delayed = false;

    // Start at 1 and -2 since first viewport is (if applicable) both add and remove and don't need processing
    for (let i = 1, j = removeViewports.length - 2; i < minLength; i++, j--) {
      delayed = TransitionViewport.delayHooks([removeViewports[j], addViewports[i]], 'canUnload', 'canLoad') || delayed;
      delayed = TransitionViewport.delayHooks([removeViewports[j], addViewports[i]], 'canLoad', 'unloading') || delayed;
      delayed = TransitionViewport.delayHooks([removeViewports[j], addViewports[i]], 'unloading', 'loading') || delayed;
    }
    return delayed;
  }

  private static delayHooks(viewports: TransitionViewport[] | string[][], check: string, delay: string): boolean {
    check = `.${check}`;
    delay = `.${delay}`;
    let delayed = false;

    const viewportHooks = viewports[0] instanceof TransitionViewport
      ? (viewports as TransitionViewport[]).filter(viewport => viewport !== void 0).map(viewport => viewport.hooks)
      : viewports as string[][];

    let highestTick = -Infinity;
    for (const hooks of viewportHooks) {
      const index = TransitionViewport.findLastIndex(hooks, item => item !== void 0 && item.endsWith(check));
      if (index === -1) {
        continue;
      }
      const tick = TransitionViewport.getTick(hooks, index);
      if (tick > highestTick) {
        highestTick = tick;
      }
    }
    if (highestTick === -Infinity) {
      return;
    }

    for (const hooks of viewportHooks) {
      const index = hooks.findIndex(item => item && item.endsWith(delay));
      if (index === -1) {
        continue;
      }
      const tick = TransitionViewport.getTick(hooks, index);
      if (highestTick >= tick) {
        // TODO: Might want to find previous blank first
        const insert = new Array(highestTick - tick + 1).fill('');
        hooks.splice(index, 0, ...insert);
        delayed = true;
      }
    }
    return delayed;
  }

  private static findLastIndex(arr, check): number {
    const reverse = [...arr];
    reverse.reverse();
    const index = reverse.findIndex(check);
    return index >= 0 ? reverse.length - index - 1 : -1;
  }

  private static getTick(arr, index): number {
    let tick = 0;
    for (let i = 0; i < index - 1; i++) {
      tick++;
      while (arr[i] && arr[i + 1] && i < index - 1) {
        i++;
      }
    }
    return tick;
  }
}