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