diff options
author | Shin'ya Ueoka <ueokande@i-beam.org> | 2019-05-19 09:26:52 +0900 |
---|---|---|
committer | Shin'ya Ueoka <ueokande@i-beam.org> | 2019-05-19 09:26:52 +0900 |
commit | e0c4182f14f908d13c8c814c7bc2b48a1791f881 (patch) | |
tree | a277179d51013c4a2384f076197a1dbcc442d2d0 /src/content/usecases | |
parent | 5b7f7f5dbd94b5bce7aee4667add187ffb9944f2 (diff) |
Follow as a clean architecture
Diffstat (limited to 'src/content/usecases')
-rw-r--r-- | src/content/usecases/FollowMasterUseCase.ts | 150 | ||||
-rw-r--r-- | src/content/usecases/FollowSlaveUseCase.ts | 91 |
2 files changed, 241 insertions, 0 deletions
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(); + } +} |