aboutsummaryrefslogtreecommitdiff
path: root/src/content
diff options
context:
space:
mode:
Diffstat (limited to 'src/content')
-rw-r--r--src/content/MessageListener.ts6
-rw-r--r--src/content/actions/operation.ts8
-rw-r--r--src/content/client/FollowMasterClient.ts47
-rw-r--r--src/content/client/FollowSlaveClient.ts76
-rw-r--r--src/content/components/common/follow.ts163
-rw-r--r--src/content/components/common/index.ts2
-rw-r--r--src/content/components/top-content/follow-controller.ts95
-rw-r--r--src/content/presenters/FollowPresenter.ts134
8 files changed, 354 insertions, 177 deletions
diff --git a/src/content/MessageListener.ts b/src/content/MessageListener.ts
index 1d7a479..e545cab 100644
--- a/src/content/MessageListener.ts
+++ b/src/content/MessageListener.ts
@@ -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);
diff --git a/src/content/actions/operation.ts b/src/content/actions/operation.ts
index 28192d7..657cf47 100644
--- a/src/content/actions/operation.ts
+++ b/src/content/actions/operation.ts
@@ -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();
diff --git a/src/content/client/FollowMasterClient.ts b/src/content/client/FollowMasterClient.ts
new file mode 100644
index 0000000..464b52f
--- /dev/null
+++ b/src/content/client/FollowMasterClient.ts
@@ -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), '*');
+ }
+}
diff --git a/src/content/client/FollowSlaveClient.ts b/src/content/client/FollowSlaveClient.ts
new file mode 100644
index 0000000..0905cd9
--- /dev/null
+++ b/src/content/client/FollowSlaveClient.ts
@@ -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), '*');
+ }
+}
diff --git a/src/content/components/common/follow.ts b/src/content/components/common/follow.ts
index 9a62613..e0003e3 100644
--- a/src/content/components/common/follow.ts
+++ b/src/content/components/common/follow.ts
@@ -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');
- }
-
- 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);
- }
- }
+ createHints(viewSize: Size, framePosition: Point, tags: string[]) {
+ this.enabled = true;
+ followPresenter.createHints(viewSize, framePosition, tags);
}
- 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;
- }
}
diff --git a/src/content/components/common/index.ts b/src/content/components/common/index.ts
index 899953d..b2f48a3 100644
--- a/src/content/components/common/index.ts
+++ b/src/content/components/common/index.ts
@@ -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);
diff --git a/src/content/components/top-content/follow-controller.ts b/src/content/components/top-content/follow-controller.ts
index 2a242c2..43c917e 100644
--- a/src/content/components/top-content/follow-controller.ts
+++ b/src/content/components/top-content/follow-controller.ts
@@ -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');
-
- this.win.postMessage(JSON.stringify({
- type: messages.FOLLOW_REQUEST_COUNT_TARGETS,
- viewSize: { width: viewWidth, height: viewHeight },
- framePosition: { x: 0, y: 0 },
- }), '*');
- frameElements.forEach((ele) => {
+ let frameElements = this.win.document.querySelectorAll('iframe');
+
+ 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));
+ }
}
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;
+ }
+}