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