aboutsummaryrefslogblamecommitdiff
path: root/src/content/presenters/FollowPresenter.ts
blob: 0132621fb37c368b83e10f5023cc3791310da6c1 (plain) (tree)





































































                                                                                

                                                             
                        























































                                                                               
import Hint, { InputHint, LinkHint } from './Hint';
import * as doms from '../../shared/utils/dom';

const TARGET_SELECTOR = [
  'a', 'button', 'input', 'textarea', 'area',
  '[contenteditable=true]', '[contenteditable=""]', '[tabindex]',
  '[role="button"]', 'summary'
].join(',');

interface Size {
  width: number;
  height: number;
}

interface Point {
  x: number;
  y: number;
}

const inViewport = (
  win: Window,
  element: Element,
  viewSize: Size,
  framePosition: Point,
): boolean => {
  let {
    top, left, bottom, right
  } = doms.viewportRect(element);
  let doc = win.document;
  let frameWidth = doc.documentElement.clientWidth;
  let frameHeight = doc.documentElement.clientHeight;

  if (right < 0 || bottom < 0 || top > frameHeight || left > frameWidth) {
    // out of frame
    return false;
  }
  if (right + framePosition.x < 0 || bottom + framePosition.y < 0 ||
      left + framePosition.x > viewSize.width ||
      top + framePosition.y > viewSize.height) {
    // out of viewport
    return false;
  }
  return true;
};

const isAriaHiddenOrAriaDisabled = (win: Window, element: Element): boolean => {
  if (!element || win.document.documentElement === element) {
    return false;
  }
  for (let attr of ['aria-hidden', 'aria-disabled']) {
    let value = element.getAttribute(attr);
    if (value !== null) {
      let hidden = value.toLowerCase();
      if (hidden === '' || hidden === 'true') {
        return true;
      }
    }
  }
  return isAriaHiddenOrAriaDisabled(win, element.parentElement as Element);
};

export default interface FollowPresenter {
  getTargetCount(viewSize: Size, framePosition: Point): number;

  createHints(viewSize: Size, framePosition: Point, tags: string[]): void;

  filterHints(prefix: string): void;

  clearHints(): void;

  getHint(tag: string): Hint | undefined;
}

export class FollowPresenterImpl implements FollowPresenter {
  private hints: Hint[];

  constructor() {
    this.hints = [];
  }

  getTargetCount(viewSize: Size, framePosition: Point): number {
    let targets = this.getTargets(viewSize, framePosition);
    return targets.length;
  }

  createHints(viewSize: Size, framePosition: Point, tags: string[]): void {
    let targets = this.getTargets(viewSize, framePosition);
    let min = Math.min(targets.length, tags.length);
    for (let i = 0; i < min; ++i) {
      let target = targets[i];
      if (target instanceof HTMLAnchorElement ||
        target instanceof HTMLAreaElement) {
        this.hints.push(new LinkHint(target, tags[i]));
      } else {
        this.hints.push(new InputHint(target, tags[i]));
      }
    }
  }

  filterHints(prefix: string): void {
    let shown = this.hints.filter(h => h.getTag().startsWith(prefix));
    let hidden = this.hints.filter(h => !h.getTag().startsWith(prefix));

    shown.forEach(h => h.show());
    hidden.forEach(h => h.hide());
  }

  clearHints(): void {
    this.hints.forEach(h => h.remove());
    this.hints = [];
  }

  getHint(tag: string): Hint | undefined {
    return this.hints.find(h => h.getTag() === tag);
  }

  private getTargets(viewSize: Size, framePosition: Point): HTMLElement[] {
    let all = window.document.querySelectorAll(TARGET_SELECTOR);
    let filtered = Array.prototype.filter.call(all, (element: HTMLElement) => {
      let style = window.getComputedStyle(element);

      // AREA's 'display' in Browser style is 'none'
      return (element.tagName === 'AREA' || style.display !== 'none') &&
        style.visibility !== 'hidden' &&
        (element as HTMLInputElement).type !== 'hidden' &&
        element.offsetHeight > 0 &&
        !isAriaHiddenOrAriaDisabled(window, element) &&
        inViewport(window, element, viewSize, framePosition);
    });
    return filtered;
  }
}