parent
5b7f7f5dbd
commit
e0c4182f14
11 changed files with 493 additions and 10 deletions
@ -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(); |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
Reference in new issue