Follow as a clean architecture

jh-changes
Shin'ya Ueoka 6 years ago
parent 5b7f7f5dbd
commit e0c4182f14
  1. 21
      src/content/controllers/FollowKeyController.ts
  2. 31
      src/content/controllers/FollowMasterController.ts
  3. 32
      src/content/controllers/FollowSlaveController.ts
  4. 16
      src/content/controllers/KeymapController.ts
  5. 35
      src/content/index.ts
  6. 35
      src/content/repositories/FollowKeyRepository.ts
  7. 59
      src/content/repositories/FollowMasterRepository.ts
  8. 31
      src/content/repositories/FollowSlaveRepository.ts
  9. 150
      src/content/usecases/FollowMasterUseCase.ts
  10. 91
      src/content/usecases/FollowSlaveUseCase.ts
  11. 2
      test/content/InputDriver.test.ts

@ -0,0 +1,21 @@
import FollowSlaveUseCase from '../usecases/FollowSlaveUseCase';
import Key from '../domains/Key';
export default class FollowKeyController {
private followSlaveUseCase: FollowSlaveUseCase;
constructor({
followSlaveUseCase = new FollowSlaveUseCase(),
} = {}) {
this.followSlaveUseCase = followSlaveUseCase;
}
press(key: Key): boolean {
if (!this.followSlaveUseCase.isFollowMode()) {
return false;
}
this.followSlaveUseCase.sendKey(key);
return true;
}
}

@ -0,0 +1,31 @@
import FollowMasterUseCase from '../usecases/FollowMasterUseCase';
import * as messages from '../../shared/messages';
export default class FollowMasterController {
private followMasterUseCase: FollowMasterUseCase;
constructor({
followMasterUseCase = new FollowMasterUseCase(),
} = {}) {
this.followMasterUseCase = followMasterUseCase;
}
followStart(m: messages.FollowStartMessage): void {
this.followMasterUseCase.startFollow(m.newTab, m.background);
}
responseCountTargets(
m: messages.FollowResponseCountTargetsMessage, sender: Window,
): void {
this.followMasterUseCase.createSlaveHints(m.count, sender);
}
keyPress(message: messages.FollowKeyPressMessage): void {
if (message.key === '[' && message.ctrlKey) {
this.followMasterUseCase.cancelFollow();
} else {
this.followMasterUseCase.enqueue(message.key);
}
}
}

@ -0,0 +1,32 @@
import * as messages from '../../shared/messages';
import FollowSlaveUseCase from '../usecases/FollowSlaveUseCase';
export default class FollowSlaveController {
private usecase: FollowSlaveUseCase;
constructor({
usecase = new FollowSlaveUseCase(),
} = {}) {
this.usecase = usecase;
}
countTargets(m: messages.FollowRequestCountTargetsMessage): void {
this.usecase.countTargets(m.viewSize, m.framePosition);
}
createHints(m: messages.FollowCreateHintsMessage): void {
this.usecase.createHints(m.viewSize, m.framePosition, m.tags);
}
showHints(m: messages.FollowShowHintsMessage): void {
this.usecase.showHints(m.prefix);
}
activate(m: messages.FollowActivateMessage): void {
this.usecase.activate(m.tag, m.newTab, m.background);
}
clear(_m: messages.FollowRemoveHintsMessage) {
this.usecase.clear();
}
}

@ -8,6 +8,8 @@ import FocusUseCase from '../usecases/FocusUseCase';
import ClipboardUseCase from '../usecases/ClipboardUseCase'; import ClipboardUseCase from '../usecases/ClipboardUseCase';
import BackgroundClient from '../client/BackgroundClient'; import BackgroundClient from '../client/BackgroundClient';
import MarkKeyyUseCase from '../usecases/MarkKeyUseCase'; import MarkKeyyUseCase from '../usecases/MarkKeyUseCase';
import FollowMasterClient, { FollowMasterClientImpl }
from '../client/FollowMasterClient';
import Key from '../domains/Key'; import Key from '../domains/Key';
export default class KeymapController { export default class KeymapController {
@ -29,6 +31,8 @@ export default class KeymapController {
private markKeyUseCase: MarkKeyyUseCase; private markKeyUseCase: MarkKeyyUseCase;
private followMasterClient: FollowMasterClient;
constructor({ constructor({
keymapUseCase = new KeymapUseCase(), keymapUseCase = new KeymapUseCase(),
addonEnabledUseCase = new AddonEnabledUseCase(), addonEnabledUseCase = new AddonEnabledUseCase(),
@ -39,6 +43,7 @@ export default class KeymapController {
clipbaordUseCase = new ClipboardUseCase(), clipbaordUseCase = new ClipboardUseCase(),
backgroundClient = new BackgroundClient(), backgroundClient = new BackgroundClient(),
markKeyUseCase = new MarkKeyyUseCase(), markKeyUseCase = new MarkKeyyUseCase(),
followMasterClient = new FollowMasterClientImpl(window.top),
} = {}) { } = {}) {
this.keymapUseCase = keymapUseCase; this.keymapUseCase = keymapUseCase;
this.addonEnabledUseCase = addonEnabledUseCase; this.addonEnabledUseCase = addonEnabledUseCase;
@ -49,6 +54,7 @@ export default class KeymapController {
this.clipbaordUseCase = clipbaordUseCase; this.clipbaordUseCase = clipbaordUseCase;
this.backgroundClient = backgroundClient; this.backgroundClient = backgroundClient;
this.markKeyUseCase = markKeyUseCase; this.markKeyUseCase = markKeyUseCase;
this.followMasterClient = followMasterClient;
} }
// eslint-disable-next-line complexity, max-lines-per-function // eslint-disable-next-line complexity, max-lines-per-function
@ -96,13 +102,9 @@ export default class KeymapController {
case operations.SCROLL_END: case operations.SCROLL_END:
this.scrollUseCase.scrollToEnd(); this.scrollUseCase.scrollToEnd();
break; break;
// case operations.FOLLOW_START: case operations.FOLLOW_START:
// window.top.postMessage(JSON.stringify({ this.followMasterClient.startFollow(op.newTab, op.background);
// type: messages.FOLLOW_START, break;
// newTab: operation.newTab,
// background: operation.background,
// }), '*');
// break;
case operations.MARK_SET_PREFIX: case operations.MARK_SET_PREFIX:
this.markKeyUseCase.enableSetMode(); this.markKeyUseCase.enableSetMode();
break; break;

@ -6,6 +6,9 @@ import consoleFrameStyle from './site-style';
import MessageListener from './MessageListener'; import MessageListener from './MessageListener';
import FindController from './controllers/FindController'; import FindController from './controllers/FindController';
import MarkController from './controllers/MarkController'; import MarkController from './controllers/MarkController';
import FollowMasterController from './controllers/FollowMasterController';
import FollowSlaveController from './controllers/FollowSlaveController';
import FollowKeyController from './controllers/FollowKeyController';
import * as messages from '../shared/messages'; import * as messages from '../shared/messages';
import InputDriver from './InputDriver'; import InputDriver from './InputDriver';
import KeymapController from './controllers/KeymapController'; import KeymapController from './controllers/KeymapController';
@ -17,11 +20,14 @@ import AddonEnabledController from './controllers/AddonEnabledController';
// const store = newStore(); // const store = newStore();
let listener = new MessageListener();
if (window.self === window.top) { if (window.self === window.top) {
// new TopContentComponent(window, store); // eslint-disable-line no-new // new TopContentComponent(window, store); // eslint-disable-line no-new
let findController = new FindController(); let findController = new FindController();
new MessageListener().onWebMessage((message: messages.Message) => {
let followMasterController = new FollowMasterController();
listener.onWebMessage((message: messages.Message, sender: Window) => {
switch (message.type) { switch (message.type) {
case messages.CONSOLE_ENTER_FIND: case messages.CONSOLE_ENTER_FIND:
return findController.start(message); return findController.start(message);
@ -32,6 +38,13 @@ if (window.self === window.top) {
case messages.CONSOLE_UNFOCUS: case messages.CONSOLE_UNFOCUS:
window.focus(); window.focus();
consoleFrames.blur(window.document); consoleFrames.blur(window.document);
break;
case messages.FOLLOW_START:
return followMasterController.followStart(message);
case messages.FOLLOW_RESPONSE_COUNT_TARGETS:
return followMasterController.responseCountTargets(message, sender);
case messages.FOLLOW_KEY_PRESS:
return followMasterController.keyPress(message);
} }
return undefined; return undefined;
}); });
@ -54,10 +67,28 @@ if (window.self === window.top) {
// new FrameContentComponent(window, store); // eslint-disable-line no-new // new FrameContentComponent(window, store); // eslint-disable-line no-new
} }
let followSlaveController = new FollowSlaveController();
listener.onWebMessage((message: messages.Message) => {
switch (message.type) {
case messages.FOLLOW_REQUEST_COUNT_TARGETS:
return followSlaveController.countTargets(message);
case messages.FOLLOW_CREATE_HINTS:
return followSlaveController.createHints(message);
case messages.FOLLOW_SHOW_HINTS:
return followSlaveController.showHints(message);
case messages.FOLLOW_ACTIVATE:
return followSlaveController.activate(message);
case messages.FOLLOW_REMOVE_HINTS:
return followSlaveController.clear(message);
}
return undefined;
});
let keymapController = new KeymapController(); let keymapController = new KeymapController();
let markKeyController = new MarkKeyController(); let markKeyController = new MarkKeyController();
let followKeyController = new FollowKeyController();
let inputDriver = new InputDriver(document.body); let inputDriver = new InputDriver(document.body);
// inputDriver.onKey(key => followSlaveController.pressKey(key)); inputDriver.onKey(key => followKeyController.press(key));
inputDriver.onKey(key => markKeyController.press(key)); inputDriver.onKey(key => markKeyController.press(key));
inputDriver.onKey(key => keymapController.press(key)); inputDriver.onKey(key => keymapController.press(key));

@ -0,0 +1,35 @@
export default interface FollowKeyRepository {
getKeys(): string[];
pushKey(key: string): void;
popKey(): void;
clearKeys(): void;
// eslint-disable-next-line semi
}
const current: {
keys: string[];
} = {
keys: [],
};
export class FollowKeyRepositoryImpl implements FollowKeyRepository {
getKeys(): string[] {
return current.keys;
}
pushKey(key: string): void {
current.keys.push(key);
}
popKey(): void {
current.keys.pop();
}
clearKeys(): void {
current.keys = [];
}
}

@ -0,0 +1,59 @@
export default interface FollowMasterRepository {
setCurrentFollowMode(newTab: boolean, background: boolean): void;
getTags(): string[];
getTagsByPrefix(prefix: string): string[];
addTag(tag: string): void;
clearTags(): void;
getCurrentNewTabMode(): boolean;
getCurrentBackgroundMode(): boolean;
// eslint-disable-next-line semi
}
const current: {
newTab: boolean;
background: boolean;
tags: string[];
} = {
newTab: false,
background: false,
tags: [],
};
export class FollowMasterRepositoryImpl implements FollowMasterRepository {
setCurrentFollowMode(newTab: boolean, background: boolean): void {
current.newTab = newTab;
current.background = background;
}
getTags(): string[] {
return current.tags;
}
getTagsByPrefix(prefix: string): string[] {
return current.tags.filter(t => t.startsWith(prefix));
}
addTag(tag: string): void {
current.tags.push(tag);
}
clearTags(): void {
current.tags = [];
}
getCurrentNewTabMode(): boolean {
return current.newTab;
}
getCurrentBackgroundMode(): boolean {
return current.background;
}
}

@ -0,0 +1,31 @@
export default interface FollowSlaveRepository {
enableFollowMode(): void;
disableFollowMode(): void;
isFollowMode(): boolean;
// eslint-disable-next-line semi
}
const current: {
enabled: boolean;
} = {
enabled: false,
};
export class FollowSlaveRepositoryImpl implements FollowSlaveRepository {
enableFollowMode(): void {
current.enabled = true;
}
disableFollowMode(): void {
current.enabled = false;
}
isFollowMode(): boolean {
return current.enabled;
}
}

@ -0,0 +1,150 @@
import FollowKeyRepository, { FollowKeyRepositoryImpl }
from '../repositories/FollowKeyRepository';
import FollowMasterRepository, { FollowMasterRepositoryImpl }
from '../repositories/FollowMasterRepository';
import FollowSlaveClient, { FollowSlaveClientImpl }
from '../client/FollowSlaveClient';
import HintKeyProducer from '../hint-key-producer';
import SettingRepository, { SettingRepositoryImpl }
from '../repositories/SettingRepository';
export default class FollowMasterUseCase {
private followKeyRepository: FollowKeyRepository;
private followMasterRepository: FollowMasterRepository;
private settingRepository: SettingRepository;
// TODO Make repository
private producer: HintKeyProducer | null;
constructor({
followKeyRepository = new FollowKeyRepositoryImpl(),
followMasterRepository = new FollowMasterRepositoryImpl(),
settingRepository = new SettingRepositoryImpl(),
} = {}) {
this.followKeyRepository = followKeyRepository;
this.followMasterRepository = followMasterRepository;
this.settingRepository = settingRepository;
this.producer = null;
}
startFollow(newTab: boolean, background: boolean): void {
let hintchars = this.settingRepository.get().properties.hintchars;
this.producer = new HintKeyProducer(hintchars);
this.followKeyRepository.clearKeys();
this.followMasterRepository.setCurrentFollowMode(newTab, background);
let viewWidth = window.top.innerWidth;
let viewHeight = window.top.innerHeight;
new FollowSlaveClientImpl(window.top).requestHintCount(
{ width: viewWidth, height: viewHeight },
{ x: 0, y: 0 },
);
let frameElements = window.document.querySelectorAll('iframe');
for (let i = 0; i < frameElements.length; ++i) {
let ele = frameElements[i] as HTMLFrameElement | HTMLIFrameElement;
let { left: frameX, top: frameY } = ele.getBoundingClientRect();
new FollowSlaveClientImpl(ele.contentWindow!!).requestHintCount(
{ width: viewWidth, height: viewHeight },
{ x: frameX, y: frameY },
);
}
}
// eslint-disable-next-line max-statements
createSlaveHints(count: number, sender: Window): void {
let produced = [];
for (let i = 0; i < count; ++i) {
let tag = this.producer!!.produce();
produced.push(tag);
this.followMasterRepository.addTag(tag);
}
let doc = window.document;
let viewWidth = window.innerWidth || doc.documentElement.clientWidth;
let viewHeight = window.innerHeight || doc.documentElement.clientHeight;
let pos = { x: 0, y: 0 };
if (sender !== window) {
let frameElements = window.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,
);
}
cancelFollow(): void {
this.followMasterRepository.clearTags();
this.broadcastToSlaves((client) => {
client.clearHints();
});
}
filter(prefix: string): void {
this.broadcastToSlaves((client) => {
client.filterHints(prefix);
});
}
activate(tag: string): void {
this.followMasterRepository.clearTags();
let newTab = this.followMasterRepository.getCurrentNewTabMode();
let background = this.followMasterRepository.getCurrentBackgroundMode();
this.broadcastToSlaves((client) => {
client.activateIfExists(tag, newTab, background);
client.clearHints();
});
}
enqueue(key: string): void {
switch (key) {
case 'Enter':
this.activate(this.getCurrentTag());
return;
case 'Esc':
this.cancelFollow();
return;
case 'Backspace':
case 'Delete':
this.followKeyRepository.popKey();
this.filter(this.getCurrentTag());
return;
}
this.followKeyRepository.pushKey(key);
let tag = this.getCurrentTag();
let matched = this.followMasterRepository.getTagsByPrefix(tag);
if (matched.length === 0) {
this.cancelFollow();
} else if (matched.length === 1) {
this.activate(tag);
} else {
this.filter(tag);
}
}
private broadcastToSlaves(handler: (client: FollowSlaveClient) => void) {
let allFrames = [window.self].concat(Array.from(window.frames as any));
let clients = allFrames.map(frame => new FollowSlaveClientImpl(frame));
for (let client of clients) {
handler(client);
}
}
private getCurrentTag(): string {
return this.followKeyRepository.getKeys().join('');
}
}

@ -0,0 +1,91 @@
import FollowSlaveRepository, { FollowSlaveRepositoryImpl }
from '../repositories/FollowSlaveRepository';
import FollowPresenter, { FollowPresenterImpl }
from '../presenters/FollowPresenter';
import TabsClient, { TabsClientImpl } from '../client/TabsClient';
import { LinkHint, InputHint } from '../presenters/Hint';
import FollowMasterClient, { FollowMasterClientImpl }
from '../client/FollowMasterClient';
import Key from '../domains/Key';
interface Size {
width: number;
height: number;
}
interface Point {
x: number;
y: number;
}
export default class FollowSlaveUseCase {
private presenter: FollowPresenter;
private tabsClient: TabsClient;
private followMasterClient: FollowMasterClient;
private followSlaveRepository: FollowSlaveRepository;
constructor({
presenter = new FollowPresenterImpl(),
tabsClient = new TabsClientImpl(),
followMasterClient = new FollowMasterClientImpl(window.top),
followSlaveRepository = new FollowSlaveRepositoryImpl(),
} = {}) {
this.presenter = presenter;
this.tabsClient = tabsClient;
this.followMasterClient = followMasterClient;
this.followSlaveRepository = followSlaveRepository;
}
countTargets(viewSize: Size, framePosition: Point): void {
let count = this.presenter.getTargetCount(viewSize, framePosition);
this.followMasterClient.responseHintCount(count);
}
createHints(viewSize: Size, framePosition: Point, tags: string[]): void {
this.followSlaveRepository.enableFollowMode();
this.presenter.createHints(viewSize, framePosition, tags);
}
showHints(prefix: string) {
this.presenter.filterHints(prefix);
}
sendKey(key: Key): void {
this.followMasterClient.sendKey(key);
}
isFollowMode(): boolean {
return this.followSlaveRepository.isFollowMode();
}
async activate(tag: string, newTab: boolean, background: boolean) {
let hint = this.presenter.getHint(tag);
if (!hint) {
return;
}
if (hint instanceof LinkHint) {
let url = hint.getLink();
// ignore taget='_blank'
if (!newTab && hint.getLinkTarget() === '_blank') {
hint.click();
return;
}
// eslint-disable-next-line no-script-url
if (!url || url === '#' || url.toLowerCase().startsWith('javascript:')) {
return;
}
await this.tabsClient.openUrl(url, newTab, background);
} else if (hint instanceof InputHint) {
hint.activate();
}
}
clear(): void {
this.followSlaveRepository.disableFollowMode();
this.presenter.clearHints();
}
}

@ -1,6 +1,6 @@
import InputDriver from '../../src/content/InputDriver'; import InputDriver from '../../src/content/InputDriver';
import { expect } from 'chai'; import { expect } from 'chai';
import { Key } from '../../src/shared/utils/keys'; import Key from '../../src/content/domains/Key';
describe('InputDriver', () => { describe('InputDriver', () => {
let target: HTMLElement; let target: HTMLElement;