aboutsummaryrefslogtreecommitdiff
path: root/src/content/presenters/FollowPresenter.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/content/presenters/FollowPresenter.ts')
-rw-r--r--src/content/presenters/FollowPresenter.ts134
1 files changed, 134 insertions, 0 deletions
diff --git a/src/content/presenters/FollowPresenter.ts b/src/content/presenters/FollowPresenter.ts
new file mode 100644
index 0000000..f0d115c
--- /dev/null
+++ b/src/content/presenters/FollowPresenter.ts
@@ -0,0 +1,134 @@
+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;
+
+ // eslint-disable-next-line semi
+}
+
+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;
+ }
+}