Define client and presenter for follow
This commit is contained in:
parent
17dc2bb5ec
commit
a88324acd9
9 changed files with 358 additions and 179 deletions
134
src/content/presenters/FollowPresenter.ts
Normal file
134
src/content/presenters/FollowPresenter.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
Reference in a new issue