Define client and presenter for follow
This commit is contained in:
parent
17dc2bb5ec
commit
a88324acd9
9 changed files with 358 additions and 179 deletions
|
@ -1,14 +1,16 @@
|
|||
import { Message, valueOf } from '../shared/messages';
|
||||
|
||||
export type WebMessageSender = Window | MessagePort | ServiceWorker | null;
|
||||
export type WebExtMessageSender = browser.runtime.MessageSender;
|
||||
|
||||
export default class MessageListener {
|
||||
onWebMessage(
|
||||
listener: (msg: Message, sender: WebMessageSender) => void,
|
||||
listener: (msg: Message, sender: Window) => void,
|
||||
) {
|
||||
window.addEventListener('message', (event: MessageEvent) => {
|
||||
let sender = event.source;
|
||||
if (!(sender instanceof Window)) {
|
||||
return;
|
||||
}
|
||||
let message = null;
|
||||
try {
|
||||
message = JSON.parse(event.data);
|
||||
|
|
|
@ -9,11 +9,13 @@ import AddonEnabledUseCase from '../usecases/AddonEnabledUseCase';
|
|||
import ClipboardUseCase from '../usecases/ClipboardUseCase';
|
||||
import { SettingRepositoryImpl } from '../repositories/SettingRepository';
|
||||
import { ScrollPresenterImpl } from '../presenters/ScrollPresenter';
|
||||
import { FollowMasterClientImpl } from '../client/FollowMasterClient';
|
||||
|
||||
let addonEnabledUseCase = new AddonEnabledUseCase();
|
||||
let clipbaordUseCase = new ClipboardUseCase();
|
||||
let settingRepository = new SettingRepositoryImpl();
|
||||
let scrollPresenter = new ScrollPresenterImpl();
|
||||
let followMasterClient = new FollowMasterClientImpl(window.top);
|
||||
|
||||
// eslint-disable-next-line complexity, max-lines-per-function
|
||||
const exec = async(
|
||||
|
@ -63,11 +65,7 @@ const exec = async(
|
|||
scrollPresenter.scrollToEnd(smoothscroll);
|
||||
break;
|
||||
case operations.FOLLOW_START:
|
||||
window.top.postMessage(JSON.stringify({
|
||||
type: messages.FOLLOW_START,
|
||||
newTab: operation.newTab,
|
||||
background: operation.background,
|
||||
}), '*');
|
||||
followMasterClient.startFollow(operation.newTab, operation.background);
|
||||
break;
|
||||
case operations.MARK_SET_PREFIX:
|
||||
return markActions.startSet();
|
||||
|
|
47
src/content/client/FollowMasterClient.ts
Normal file
47
src/content/client/FollowMasterClient.ts
Normal file
|
@ -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), '*');
|
||||
}
|
||||
}
|
76
src/content/client/FollowSlaveClient.ts
Normal file
76
src/content/client/FollowSlaveClient.ts
Normal file
|
@ -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), '*');
|
||||
}
|
||||
}
|
|
@ -1,17 +1,18 @@
|
|||
import MessageListener from '../../MessageListener';
|
||||
import Hint, { LinkHint, InputHint } from '../../presenters/Hint';
|
||||
import * as dom from '../../../shared/utils/dom';
|
||||
import { LinkHint, InputHint } from '../../presenters/Hint';
|
||||
import * as messages from '../../../shared/messages';
|
||||
import * as keyUtils from '../../../shared/utils/keys';
|
||||
import { Key } from '../../../shared/utils/keys';
|
||||
import TabsClient, { TabsClientImpl } from '../../client/TabsClient';
|
||||
import FollowMasterClient, { FollowMasterClientImpl }
|
||||
from '../../client/FollowMasterClient';
|
||||
import FollowPresenter, { FollowPresenterImpl }
|
||||
from '../../presenters/FollowPresenter';
|
||||
|
||||
let tabsClient: TabsClient = new TabsClientImpl();
|
||||
|
||||
const TARGET_SELECTOR = [
|
||||
'a', 'button', 'input', 'textarea', 'area',
|
||||
'[contenteditable=true]', '[contenteditable=""]', '[tabindex]',
|
||||
'[role="button"]', 'summary'
|
||||
].join(',');
|
||||
let followMasterClient: FollowMasterClient =
|
||||
new FollowMasterClientImpl(window.top);
|
||||
let followPresenter: FollowPresenter =
|
||||
new FollowPresenterImpl();
|
||||
|
||||
interface Size {
|
||||
width: number;
|
||||
|
@ -23,118 +24,46 @@ interface Point {
|
|||
y: number;
|
||||
}
|
||||
|
||||
const inViewport = (
|
||||
win: Window,
|
||||
element: Element,
|
||||
viewSize: Size,
|
||||
framePosition: Point,
|
||||
): boolean => {
|
||||
let {
|
||||
top, left, bottom, right
|
||||
} = dom.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 class Follow {
|
||||
private win: Window;
|
||||
private enabled: boolean;
|
||||
|
||||
private hints: {[key: string]: Hint };
|
||||
|
||||
private targets: HTMLElement[] = [];
|
||||
|
||||
constructor(win: Window) {
|
||||
this.win = win;
|
||||
this.hints = {};
|
||||
this.targets = [];
|
||||
constructor() {
|
||||
this.enabled = false;
|
||||
|
||||
new MessageListener().onWebMessage(this.onMessage.bind(this));
|
||||
}
|
||||
|
||||
key(key: keyUtils.Key): boolean {
|
||||
if (Object.keys(this.hints).length === 0) {
|
||||
key(key: Key): boolean {
|
||||
if (!this.enabled) {
|
||||
return false;
|
||||
}
|
||||
this.win.parent.postMessage(JSON.stringify({
|
||||
type: messages.FOLLOW_KEY_PRESS,
|
||||
key: key.key,
|
||||
ctrlKey: key.ctrlKey,
|
||||
}), '*');
|
||||
followMasterClient.sendKey(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
countHints(sender: any, viewSize: Size, framePosition: Point) {
|
||||
this.targets = Follow.getTargetElements(this.win, viewSize, framePosition);
|
||||
sender.postMessage(JSON.stringify({
|
||||
type: messages.FOLLOW_RESPONSE_COUNT_TARGETS,
|
||||
count: this.targets.length,
|
||||
}), '*');
|
||||
countHints(viewSize: Size, framePosition: Point) {
|
||||
let count = followPresenter.getTargetCount(viewSize, framePosition);
|
||||
followMasterClient.responseHintCount(count);
|
||||
}
|
||||
|
||||
createHints(keysArray: string[]) {
|
||||
if (keysArray.length !== this.targets.length) {
|
||||
throw new Error('illegal hint count');
|
||||
createHints(viewSize: Size, framePosition: Point, tags: string[]) {
|
||||
this.enabled = true;
|
||||
followPresenter.createHints(viewSize, framePosition, tags);
|
||||
}
|
||||
|
||||
this.hints = {};
|
||||
for (let i = 0; i < keysArray.length; ++i) {
|
||||
let keys = keysArray[i];
|
||||
let target = this.targets[i];
|
||||
if (target instanceof HTMLAnchorElement ||
|
||||
target instanceof HTMLAreaElement) {
|
||||
this.hints[keys] = new LinkHint(target, keys);
|
||||
} else {
|
||||
this.hints[keys] = new InputHint(target, keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showHints(keys: string) {
|
||||
Object.keys(this.hints).filter(key => key.startsWith(keys))
|
||||
.forEach(key => this.hints[key].show());
|
||||
Object.keys(this.hints).filter(key => !key.startsWith(keys))
|
||||
.forEach(key => this.hints[key].hide());
|
||||
showHints(prefix: string) {
|
||||
followPresenter.filterHints(prefix);
|
||||
}
|
||||
|
||||
removeHints() {
|
||||
Object.keys(this.hints).forEach((key) => {
|
||||
this.hints[key].remove();
|
||||
});
|
||||
this.hints = {};
|
||||
this.targets = [];
|
||||
followPresenter.clearHints();
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
async activateHints(keys: string, newTab: boolean, background: boolean): Promise<void> {
|
||||
let hint = this.hints[keys];
|
||||
async activateHints(
|
||||
tag: string, newTab: boolean, background: boolean,
|
||||
): Promise<void> {
|
||||
let hint = followPresenter.getHint(tag);
|
||||
if (!hint) {
|
||||
return;
|
||||
}
|
||||
|
@ -156,38 +85,20 @@ export default class Follow {
|
|||
}
|
||||
}
|
||||
|
||||
onMessage(message: messages.Message, sender: any) {
|
||||
onMessage(message: messages.Message, _sender: Window) {
|
||||
switch (message.type) {
|
||||
case messages.FOLLOW_REQUEST_COUNT_TARGETS:
|
||||
return this.countHints(sender, message.viewSize, message.framePosition);
|
||||
return this.countHints(message.viewSize, message.framePosition);
|
||||
case messages.FOLLOW_CREATE_HINTS:
|
||||
return this.createHints(message.keysArray);
|
||||
return this.createHints(
|
||||
message.viewSize, message.framePosition, message.tags);
|
||||
case messages.FOLLOW_SHOW_HINTS:
|
||||
return this.showHints(message.keys);
|
||||
return this.showHints(message.prefix);
|
||||
case messages.FOLLOW_ACTIVATE:
|
||||
return this.activateHints(message.keys, message.newTab, message.background);
|
||||
return this.activateHints(
|
||||
message.tag, message.newTab, message.background);
|
||||
case messages.FOLLOW_REMOVE_HINTS:
|
||||
return this.removeHints();
|
||||
}
|
||||
}
|
||||
|
||||
static getTargetElements(
|
||||
win: Window,
|
||||
viewSize:
|
||||
Size, framePosition: Point,
|
||||
): HTMLElement[] {
|
||||
let all = win.document.querySelectorAll(TARGET_SELECTOR);
|
||||
let filtered = Array.prototype.filter.call(all, (element: HTMLElement) => {
|
||||
let style = win.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(win, element) &&
|
||||
inViewport(win, element, viewSize, framePosition);
|
||||
});
|
||||
return filtered;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ let settingUseCase = new SettingUseCase();
|
|||
export default class Common {
|
||||
constructor(win: Window, store: any) {
|
||||
const input = new InputComponent(win.document.body);
|
||||
const follow = new FollowComponent(win);
|
||||
const follow = new FollowComponent();
|
||||
const mark = new MarkComponent(store);
|
||||
const keymapper = new KeymapperComponent(store);
|
||||
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
import * as followControllerActions from '../../actions/follow-controller';
|
||||
import * as messages from '../../../shared/messages';
|
||||
import MessageListener, { WebMessageSender } from '../../MessageListener';
|
||||
import MessageListener from '../../MessageListener';
|
||||
import HintKeyProducer from '../../hint-key-producer';
|
||||
|
||||
import { SettingRepositoryImpl } from '../../repositories/SettingRepository';
|
||||
import FollowSlaveClient, { FollowSlaveClientImpl }
|
||||
from '../../client/FollowSlaveClient';
|
||||
|
||||
let settingRepository = new SettingRepositoryImpl();
|
||||
|
||||
const broadcastMessage = (win: Window, message: messages.Message): void => {
|
||||
let json = JSON.stringify(message);
|
||||
let frames = [win.self].concat(Array.from(win.frames as any));
|
||||
frames.forEach(frame => frame.postMessage(json, '*'));
|
||||
};
|
||||
|
||||
export default class FollowController {
|
||||
private win: Window;
|
||||
|
||||
|
@ -43,7 +39,7 @@ export default class FollowController {
|
|||
});
|
||||
}
|
||||
|
||||
onMessage(message: messages.Message, sender: WebMessageSender) {
|
||||
onMessage(message: messages.Message, sender: Window) {
|
||||
switch (message.type) {
|
||||
case messages.FOLLOW_START:
|
||||
return this.store.dispatch(
|
||||
|
@ -77,18 +73,17 @@ export default class FollowController {
|
|||
this.store.dispatch(followControllerActions.disable());
|
||||
}
|
||||
|
||||
broadcastMessage(this.win, {
|
||||
type: messages.FOLLOW_SHOW_HINTS,
|
||||
keys: this.state.keys as string,
|
||||
this.broadcastMessage((c: FollowSlaveClient) => {
|
||||
c.filterHints(this.state.keys!!);
|
||||
});
|
||||
}
|
||||
|
||||
activate(): void {
|
||||
broadcastMessage(this.win, {
|
||||
type: messages.FOLLOW_ACTIVATE,
|
||||
keys: this.state.keys as string,
|
||||
newTab: this.state.newTab!!,
|
||||
background: this.state.background!!,
|
||||
this.broadcastMessage((c: FollowSlaveClient) => {
|
||||
c.activateIfExists(
|
||||
this.state.keys!!,
|
||||
this.state.newTab!!,
|
||||
this.state.background!!);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -123,50 +118,64 @@ export default class FollowController {
|
|||
let doc = this.win.document;
|
||||
let viewWidth = this.win.innerWidth || doc.documentElement.clientWidth;
|
||||
let viewHeight = this.win.innerHeight || doc.documentElement.clientHeight;
|
||||
let frameElements = this.win.document.querySelectorAll('frame,iframe');
|
||||
let frameElements = this.win.document.querySelectorAll('iframe');
|
||||
|
||||
this.win.postMessage(JSON.stringify({
|
||||
type: messages.FOLLOW_REQUEST_COUNT_TARGETS,
|
||||
viewSize: { width: viewWidth, height: viewHeight },
|
||||
framePosition: { x: 0, y: 0 },
|
||||
}), '*');
|
||||
frameElements.forEach((ele) => {
|
||||
new FollowSlaveClientImpl(this.win).requestHintCount(
|
||||
{ width: viewWidth, height: viewHeight },
|
||||
{ x: 0, y: 0 });
|
||||
|
||||
for (let ele of Array.from(frameElements)) {
|
||||
let { left: frameX, top: frameY } = ele.getBoundingClientRect();
|
||||
let message = JSON.stringify({
|
||||
type: messages.FOLLOW_REQUEST_COUNT_TARGETS,
|
||||
viewSize: { width: viewWidth, height: viewHeight },
|
||||
framePosition: { x: frameX, y: frameY },
|
||||
});
|
||||
if (ele instanceof HTMLFrameElement && ele.contentWindow ||
|
||||
ele instanceof HTMLIFrameElement && ele.contentWindow) {
|
||||
ele.contentWindow.postMessage(message, '*');
|
||||
new FollowSlaveClientImpl(ele.contentWindow!!).requestHintCount(
|
||||
{ width: viewWidth, height: viewHeight },
|
||||
{ x: frameX, y: frameY },
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
create(count: number, sender: WebMessageSender) {
|
||||
create(count: number, sender: Window) {
|
||||
let produced = [];
|
||||
for (let i = 0; i < count; ++i) {
|
||||
produced.push((this.producer as HintKeyProducer).produce());
|
||||
}
|
||||
this.keys = this.keys.concat(produced);
|
||||
|
||||
(sender as Window).postMessage(JSON.stringify({
|
||||
type: messages.FOLLOW_CREATE_HINTS,
|
||||
keysArray: produced,
|
||||
newTab: this.state.newTab,
|
||||
background: this.state.background,
|
||||
}), '*');
|
||||
let doc = this.win.document;
|
||||
let viewWidth = this.win.innerWidth || doc.documentElement.clientWidth;
|
||||
let viewHeight = this.win.innerHeight || doc.documentElement.clientHeight;
|
||||
let pos = { x: 0, y: 0 };
|
||||
if (sender !== window) {
|
||||
let frameElements = this.win.document.querySelectorAll('iframe');
|
||||
let ele = Array.from(frameElements).find(e => e.contentWindow === sender);
|
||||
if (!ele) {
|
||||
// elements of the sender is gone
|
||||
return;
|
||||
}
|
||||
let { left: frameX, top: frameY } = ele.getBoundingClientRect();
|
||||
pos = { x: frameX, y: frameY };
|
||||
}
|
||||
new FollowSlaveClientImpl(sender).createHints(
|
||||
{ width: viewWidth, height: viewHeight },
|
||||
pos,
|
||||
produced,
|
||||
);
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.keys = [];
|
||||
broadcastMessage(this.win, {
|
||||
type: messages.FOLLOW_REMOVE_HINTS,
|
||||
this.broadcastMessage((c: FollowSlaveClient) => {
|
||||
c.clearHints();
|
||||
});
|
||||
}
|
||||
|
||||
private hintchars() {
|
||||
return settingRepository.get().properties.hintchars;
|
||||
}
|
||||
|
||||
private broadcastMessage(f: (clinet: FollowSlaveClient) => void) {
|
||||
let windows = [window.self].concat(Array.from(window.frames as any));
|
||||
windows
|
||||
.map(w => new FollowSlaveClientImpl(w))
|
||||
.forEach(c => f(c));
|
||||
}
|
||||
}
|
||||
|
|
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;
|
||||
}
|
||||
}
|
|
@ -108,12 +108,14 @@ export interface FollowResponseCountTargetsMessage {
|
|||
|
||||
export interface FollowCreateHintsMessage {
|
||||
type: typeof FOLLOW_CREATE_HINTS;
|
||||
keysArray: string[];
|
||||
tags: string[];
|
||||
viewSize: { width: number, height: number };
|
||||
framePosition: { x: number, y: number };
|
||||
}
|
||||
|
||||
export interface FollowShowHintsMessage {
|
||||
type: typeof FOLLOW_SHOW_HINTS;
|
||||
keys: string;
|
||||
prefix: string;
|
||||
}
|
||||
|
||||
export interface FollowRemoveHintsMessage {
|
||||
|
@ -122,7 +124,7 @@ export interface FollowRemoveHintsMessage {
|
|||
|
||||
export interface FollowActivateMessage {
|
||||
type: typeof FOLLOW_ACTIVATE;
|
||||
keys: string;
|
||||
tag: string;
|
||||
newTab: boolean;
|
||||
background: boolean;
|
||||
}
|
||||
|
|
Reference in a new issue