aboutsummaryrefslogtreecommitdiff
path: root/src/content
diff options
context:
space:
mode:
authorShin'ya Ueoka <ueokande@i-beam.org>2019-05-19 09:26:52 +0900
committerShin'ya Ueoka <ueokande@i-beam.org>2019-05-19 09:26:52 +0900
commite0c4182f14f908d13c8c814c7bc2b48a1791f881 (patch)
treea277179d51013c4a2384f076197a1dbcc442d2d0 /src/content
parent5b7f7f5dbd94b5bce7aee4667add187ffb9944f2 (diff)
Follow as a clean architecture
Diffstat (limited to 'src/content')
-rw-r--r--src/content/controllers/FollowKeyController.ts21
-rw-r--r--src/content/controllers/FollowMasterController.ts31
-rw-r--r--src/content/controllers/FollowSlaveController.ts32
-rw-r--r--src/content/controllers/KeymapController.ts16
-rw-r--r--src/content/index.ts35
-rw-r--r--src/content/repositories/FollowKeyRepository.ts35
-rw-r--r--src/content/repositories/FollowMasterRepository.ts59
-rw-r--r--src/content/repositories/FollowSlaveRepository.ts31
-rw-r--r--src/content/usecases/FollowMasterUseCase.ts150
-rw-r--r--src/content/usecases/FollowSlaveUseCase.ts91
10 files changed, 492 insertions, 9 deletions
diff --git a/src/content/controllers/FollowKeyController.ts b/src/content/controllers/FollowKeyController.ts
new file mode 100644
index 0000000..eb45e01
--- /dev/null
+++ b/src/content/controllers/FollowKeyController.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;
+ }
+}
diff --git a/src/content/controllers/FollowMasterController.ts b/src/content/controllers/FollowMasterController.ts
new file mode 100644
index 0000000..89294ff
--- /dev/null
+++ b/src/content/controllers/FollowMasterController.ts
@@ -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);
+ }
+ }
+}
+
diff --git a/src/content/controllers/FollowSlaveController.ts b/src/content/controllers/FollowSlaveController.ts
new file mode 100644
index 0000000..88dccf3
--- /dev/null
+++ b/src/content/controllers/FollowSlaveController.ts
@@ -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();
+ }
+}
diff --git a/src/content/controllers/KeymapController.ts b/src/content/controllers/KeymapController.ts
index 424292c..20c24c0 100644
--- a/src/content/controllers/KeymapController.ts
+++ b/src/content/controllers/KeymapController.ts
@@ -8,6 +8,8 @@ import FocusUseCase from '../usecases/FocusUseCase';
import ClipboardUseCase from '../usecases/ClipboardUseCase';
import BackgroundClient from '../client/BackgroundClient';
import MarkKeyyUseCase from '../usecases/MarkKeyUseCase';
+import FollowMasterClient, { FollowMasterClientImpl }
+ from '../client/FollowMasterClient';
import Key from '../domains/Key';
export default class KeymapController {
@@ -29,6 +31,8 @@ export default class KeymapController {
private markKeyUseCase: MarkKeyyUseCase;
+ private followMasterClient: FollowMasterClient;
+
constructor({
keymapUseCase = new KeymapUseCase(),
addonEnabledUseCase = new AddonEnabledUseCase(),
@@ -39,6 +43,7 @@ export default class KeymapController {
clipbaordUseCase = new ClipboardUseCase(),
backgroundClient = new BackgroundClient(),
markKeyUseCase = new MarkKeyyUseCase(),
+ followMasterClient = new FollowMasterClientImpl(window.top),
} = {}) {
this.keymapUseCase = keymapUseCase;
this.addonEnabledUseCase = addonEnabledUseCase;
@@ -49,6 +54,7 @@ export default class KeymapController {
this.clipbaordUseCase = clipbaordUseCase;
this.backgroundClient = backgroundClient;
this.markKeyUseCase = markKeyUseCase;
+ this.followMasterClient = followMasterClient;
}
// eslint-disable-next-line complexity, max-lines-per-function
@@ -96,13 +102,9 @@ export default class KeymapController {
case operations.SCROLL_END:
this.scrollUseCase.scrollToEnd();
break;
- // case operations.FOLLOW_START:
- // window.top.postMessage(JSON.stringify({
- // type: messages.FOLLOW_START,
- // newTab: operation.newTab,
- // background: operation.background,
- // }), '*');
- // break;
+ case operations.FOLLOW_START:
+ this.followMasterClient.startFollow(op.newTab, op.background);
+ break;
case operations.MARK_SET_PREFIX:
this.markKeyUseCase.enableSetMode();
break;
diff --git a/src/content/index.ts b/src/content/index.ts
index d644095..08bdf6b 100644
--- a/src/content/index.ts
+++ b/src/content/index.ts
@@ -6,6 +6,9 @@ import consoleFrameStyle from './site-style';
import MessageListener from './MessageListener';
import FindController from './controllers/FindController';
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 InputDriver from './InputDriver';
import KeymapController from './controllers/KeymapController';
@@ -17,11 +20,14 @@ import AddonEnabledController from './controllers/AddonEnabledController';
// const store = newStore();
+let listener = new MessageListener();
if (window.self === window.top) {
// new TopContentComponent(window, store); // eslint-disable-line no-new
let findController = new FindController();
- new MessageListener().onWebMessage((message: messages.Message) => {
+
+ let followMasterController = new FollowMasterController();
+ listener.onWebMessage((message: messages.Message, sender: Window) => {
switch (message.type) {
case messages.CONSOLE_ENTER_FIND:
return findController.start(message);
@@ -32,6 +38,13 @@ if (window.self === window.top) {
case messages.CONSOLE_UNFOCUS:
window.focus();
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;
});
@@ -54,10 +67,28 @@ if (window.self === window.top) {
// 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 markKeyController = new MarkKeyController();
+let followKeyController = new FollowKeyController();
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 => keymapController.press(key));
diff --git a/src/content/repositories/FollowKeyRepository.ts b/src/content/repositories/FollowKeyRepository.ts
new file mode 100644
index 0000000..a671b5c
--- /dev/null
+++ b/src/content/repositories/FollowKeyRepository.ts
@@ -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 = [];
+ }
+}
diff --git a/src/content/repositories/FollowMasterRepository.ts b/src/content/repositories/FollowMasterRepository.ts
new file mode 100644
index 0000000..a964953
--- /dev/null
+++ b/src/content/repositories/FollowMasterRepository.ts
@@ -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;
+ }
+}
+
diff --git a/src/content/repositories/FollowSlaveRepository.ts b/src/content/repositories/FollowSlaveRepository.ts
new file mode 100644
index 0000000..4c2de72
--- /dev/null
+++ b/src/content/repositories/FollowSlaveRepository.ts
@@ -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;
+ }
+}
+
+
diff --git a/src/content/usecases/FollowMasterUseCase.ts b/src/content/usecases/FollowMasterUseCase.ts
new file mode 100644
index 0000000..c2c6835
--- /dev/null
+++ b/src/content/usecases/FollowMasterUseCase.ts
@@ -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('');
+ }
+}
diff --git a/src/content/usecases/FollowSlaveUseCase.ts b/src/content/usecases/FollowSlaveUseCase.ts
new file mode 100644
index 0000000..eb011de
--- /dev/null
+++ b/src/content/usecases/FollowSlaveUseCase.ts
@@ -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();
+ }
+}