parent
17dc2bb5ec
commit
a88324acd9
9 changed files with 359 additions and 180 deletions
@ -0,0 +1,47 @@ |
|||||||
|
import * as messages from '../../shared/messages'; |
||||||
|
import { Key } from '../../shared/utils/keys'; |
||||||
|
|
||||||
|
export default interface FollowMasterClient { |
||||||
|
startFollow(newTab: boolean, background: boolean): void; |
||||||
|
|
||||||
|
responseHintCount(count: number): void; |
||||||
|
|
||||||
|
sendKey(key: Key): void; |
||||||
|
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
} |
||||||
|
|
||||||
|
export class FollowMasterClientImpl implements FollowMasterClient { |
||||||
|
private window: Window; |
||||||
|
|
||||||
|
constructor(window: Window) { |
||||||
|
this.window = window; |
||||||
|
} |
||||||
|
|
||||||
|
startFollow(newTab: boolean, background: boolean): void { |
||||||
|
this.postMessage({ |
||||||
|
type: messages.FOLLOW_START, |
||||||
|
newTab, |
||||||
|
background, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
responseHintCount(count: number): void { |
||||||
|
this.postMessage({ |
||||||
|
type: messages.FOLLOW_RESPONSE_COUNT_TARGETS, |
||||||
|
count, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
sendKey(key: Key): void { |
||||||
|
this.postMessage({ |
||||||
|
type: messages.FOLLOW_KEY_PRESS, |
||||||
|
key: key.key, |
||||||
|
ctrlKey: key.ctrlKey || false, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private postMessage(msg: messages.Message): void { |
||||||
|
this.window.postMessage(JSON.stringify(msg), '*'); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,76 @@ |
|||||||
|
import * as messages from '../../shared/messages'; |
||||||
|
|
||||||
|
interface Size { |
||||||
|
width: number; |
||||||
|
height: number; |
||||||
|
} |
||||||
|
|
||||||
|
interface Point { |
||||||
|
x: number; |
||||||
|
y: number; |
||||||
|
} |
||||||
|
|
||||||
|
export default interface FollowSlaveClient { |
||||||
|
filterHints(prefix: string): void; |
||||||
|
|
||||||
|
requestHintCount(viewSize: Size, framePosition: Point): void; |
||||||
|
|
||||||
|
createHints(viewSize: Size, framePosition: Point, tags: string[]): void; |
||||||
|
|
||||||
|
clearHints(): void; |
||||||
|
|
||||||
|
activateIfExists(tag: string, newTab: boolean, background: boolean): void; |
||||||
|
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
} |
||||||
|
|
||||||
|
export class FollowSlaveClientImpl implements FollowSlaveClient { |
||||||
|
private target: Window; |
||||||
|
|
||||||
|
constructor(target: Window) { |
||||||
|
this.target = target; |
||||||
|
} |
||||||
|
|
||||||
|
filterHints(prefix: string): void { |
||||||
|
this.postMessage({ |
||||||
|
type: messages.FOLLOW_SHOW_HINTS, |
||||||
|
prefix, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
requestHintCount(viewSize: Size, framePosition: Point): void { |
||||||
|
this.postMessage({ |
||||||
|
type: messages.FOLLOW_REQUEST_COUNT_TARGETS, |
||||||
|
viewSize, |
||||||
|
framePosition, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
createHints(viewSize: Size, framePosition: Point, tags: string[]): void { |
||||||
|
this.postMessage({ |
||||||
|
type: messages.FOLLOW_CREATE_HINTS, |
||||||
|
viewSize, |
||||||
|
framePosition, |
||||||
|
tags, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
clearHints(): void { |
||||||
|
this.postMessage({ |
||||||
|
type: messages.FOLLOW_REMOVE_HINTS, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
activateIfExists(tag: string, newTab: boolean, background: boolean): void { |
||||||
|
this.postMessage({ |
||||||
|
type: messages.FOLLOW_ACTIVATE, |
||||||
|
tag, |
||||||
|
newTab, |
||||||
|
background, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private postMessage(msg: messages.Message): void { |
||||||
|
this.target.postMessage(JSON.stringify(msg), '*'); |
||||||
|
} |
||||||
|
} |
@ -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 new issue