parent
e0c4182f14
commit
4be04628e1
27 changed files with 1 additions and 1083 deletions
@ -1,32 +0,0 @@ |
|||||||
import * as actions from './index'; |
|
||||||
|
|
||||||
const enable = ( |
|
||||||
newTab: boolean, background: boolean, |
|
||||||
): actions.FollowAction => { |
|
||||||
return { |
|
||||||
type: actions.FOLLOW_CONTROLLER_ENABLE, |
|
||||||
newTab, |
|
||||||
background, |
|
||||||
}; |
|
||||||
}; |
|
||||||
|
|
||||||
const disable = (): actions.FollowAction => { |
|
||||||
return { |
|
||||||
type: actions.FOLLOW_CONTROLLER_DISABLE, |
|
||||||
}; |
|
||||||
}; |
|
||||||
|
|
||||||
const keyPress = (key: string): actions.FollowAction => { |
|
||||||
return { |
|
||||||
type: actions.FOLLOW_CONTROLLER_KEY_PRESS, |
|
||||||
key: key |
|
||||||
}; |
|
||||||
}; |
|
||||||
|
|
||||||
const backspace = (): actions.FollowAction => { |
|
||||||
return { |
|
||||||
type: actions.FOLLOW_CONTROLLER_BACKSPACE, |
|
||||||
}; |
|
||||||
}; |
|
||||||
|
|
||||||
export { enable, disable, keyPress, backspace }; |
|
@ -1,81 +0,0 @@ |
|||||||
import Redux from 'redux'; |
|
||||||
import Key from '../domains/Key'; |
|
||||||
|
|
||||||
// User input
|
|
||||||
export const INPUT_KEY_PRESS = 'input.key.press'; |
|
||||||
export const INPUT_CLEAR_KEYS = 'input.clear.keys'; |
|
||||||
|
|
||||||
// Completion
|
|
||||||
export const COMPLETION_SET_ITEMS = 'completion.set.items'; |
|
||||||
export const COMPLETION_SELECT_NEXT = 'completions.select.next'; |
|
||||||
export const COMPLETION_SELECT_PREV = 'completions.select.prev'; |
|
||||||
|
|
||||||
// Follow
|
|
||||||
export const FOLLOW_CONTROLLER_ENABLE = 'follow.controller.enable'; |
|
||||||
export const FOLLOW_CONTROLLER_DISABLE = 'follow.controller.disable'; |
|
||||||
export const FOLLOW_CONTROLLER_KEY_PRESS = 'follow.controller.key.press'; |
|
||||||
export const FOLLOW_CONTROLLER_BACKSPACE = 'follow.controller.backspace'; |
|
||||||
|
|
||||||
// Mark
|
|
||||||
export const MARK_START_SET = 'mark.start.set'; |
|
||||||
export const MARK_START_JUMP = 'mark.start.jump'; |
|
||||||
export const MARK_CANCEL = 'mark.cancel'; |
|
||||||
|
|
||||||
export const NOOP = 'noop'; |
|
||||||
|
|
||||||
export interface InputKeyPressAction extends Redux.Action { |
|
||||||
type: typeof INPUT_KEY_PRESS; |
|
||||||
key: Key; |
|
||||||
} |
|
||||||
|
|
||||||
export interface InputClearKeysAction extends Redux.Action { |
|
||||||
type: typeof INPUT_CLEAR_KEYS; |
|
||||||
} |
|
||||||
|
|
||||||
export interface FollowControllerEnableAction extends Redux.Action { |
|
||||||
type: typeof FOLLOW_CONTROLLER_ENABLE; |
|
||||||
newTab: boolean; |
|
||||||
background: boolean; |
|
||||||
} |
|
||||||
|
|
||||||
export interface FollowControllerDisableAction extends Redux.Action { |
|
||||||
type: typeof FOLLOW_CONTROLLER_DISABLE; |
|
||||||
} |
|
||||||
|
|
||||||
export interface FollowControllerKeyPressAction extends Redux.Action { |
|
||||||
type: typeof FOLLOW_CONTROLLER_KEY_PRESS; |
|
||||||
key: string; |
|
||||||
} |
|
||||||
|
|
||||||
export interface FollowControllerBackspaceAction extends Redux.Action { |
|
||||||
type: typeof FOLLOW_CONTROLLER_BACKSPACE; |
|
||||||
} |
|
||||||
|
|
||||||
export interface MarkStartSetAction extends Redux.Action { |
|
||||||
type: typeof MARK_START_SET; |
|
||||||
} |
|
||||||
|
|
||||||
export interface MarkStartJumpAction extends Redux.Action { |
|
||||||
type: typeof MARK_START_JUMP; |
|
||||||
} |
|
||||||
|
|
||||||
export interface MarkCancelAction extends Redux.Action { |
|
||||||
type: typeof MARK_CANCEL; |
|
||||||
} |
|
||||||
|
|
||||||
export interface NoopAction extends Redux.Action { |
|
||||||
type: typeof NOOP; |
|
||||||
} |
|
||||||
|
|
||||||
export type InputAction = InputKeyPressAction | InputClearKeysAction; |
|
||||||
export type FollowAction = |
|
||||||
FollowControllerEnableAction | FollowControllerDisableAction | |
|
||||||
FollowControllerKeyPressAction | FollowControllerBackspaceAction; |
|
||||||
export type MarkAction = |
|
||||||
MarkStartSetAction | MarkStartJumpAction | MarkCancelAction | NoopAction; |
|
||||||
|
|
||||||
export type Action = |
|
||||||
InputAction | |
|
||||||
FollowAction | |
|
||||||
MarkAction | |
|
||||||
NoopAction; |
|
@ -1,17 +0,0 @@ |
|||||||
import * as actions from './index'; |
|
||||||
import Key from '../domains/Key'; |
|
||||||
|
|
||||||
const keyPress = (key: Key): actions.InputAction => { |
|
||||||
return { |
|
||||||
type: actions.INPUT_KEY_PRESS, |
|
||||||
key, |
|
||||||
}; |
|
||||||
}; |
|
||||||
|
|
||||||
const clearKeys = (): actions.InputAction => { |
|
||||||
return { |
|
||||||
type: actions.INPUT_CLEAR_KEYS |
|
||||||
}; |
|
||||||
}; |
|
||||||
|
|
||||||
export { keyPress, clearKeys }; |
|
@ -1,17 +0,0 @@ |
|||||||
import * as actions from './index'; |
|
||||||
|
|
||||||
const startSet = (): actions.MarkAction => { |
|
||||||
return { type: actions.MARK_START_SET }; |
|
||||||
}; |
|
||||||
|
|
||||||
const startJump = (): actions.MarkAction => { |
|
||||||
return { type: actions.MARK_START_JUMP }; |
|
||||||
}; |
|
||||||
|
|
||||||
const cancel = (): actions.MarkAction => { |
|
||||||
return { type: actions.MARK_CANCEL }; |
|
||||||
}; |
|
||||||
|
|
||||||
export { |
|
||||||
startSet, startJump, cancel, |
|
||||||
}; |
|
@ -1,112 +0,0 @@ |
|||||||
import * as operations from '../../shared/operations'; |
|
||||||
import * as actions from './index'; |
|
||||||
import * as messages from '../../shared/messages'; |
|
||||||
import * as navigates from '../navigates'; |
|
||||||
import * as focuses from '../focuses'; |
|
||||||
import * as markActions from './mark'; |
|
||||||
|
|
||||||
import AddonEnabledUseCase from '../usecases/AddonEnabledUseCase'; |
|
||||||
import ClipboardUseCase from '../usecases/ClipboardUseCase'; |
|
||||||
import { SettingRepositoryImpl } from '../repositories/SettingRepository'; |
|
||||||
import { ScrollPresenterImpl } from '../presenters/ScrollPresenter'; |
|
||||||
import { FollowMasterClientImpl } from '../client/FollowMasterClient'; |
|
||||||
|
|
||||||
let addonEnabledUseCase = new AddonEnabledUseCase(); |
|
||||||
let clipbaordUseCase = new ClipboardUseCase(); |
|
||||||
let settingRepository = new SettingRepositoryImpl(); |
|
||||||
let scrollPresenter = new ScrollPresenterImpl(); |
|
||||||
let followMasterClient = new FollowMasterClientImpl(window.top); |
|
||||||
|
|
||||||
// eslint-disable-next-line complexity, max-lines-per-function
|
|
||||||
const exec = async( |
|
||||||
operation: operations.Operation, |
|
||||||
): Promise<actions.Action> => { |
|
||||||
let settings = settingRepository.get(); |
|
||||||
let smoothscroll = settings.properties.smoothscroll; |
|
||||||
switch (operation.type) { |
|
||||||
case operations.ADDON_ENABLE: |
|
||||||
await addonEnabledUseCase.enable(); |
|
||||||
return { type: actions.NOOP }; |
|
||||||
case operations.ADDON_DISABLE: |
|
||||||
await addonEnabledUseCase.disable(); |
|
||||||
return { type: actions.NOOP }; |
|
||||||
case operations.ADDON_TOGGLE_ENABLED: |
|
||||||
await addonEnabledUseCase.toggle(); |
|
||||||
return { type: actions.NOOP }; |
|
||||||
case operations.FIND_NEXT: |
|
||||||
window.top.postMessage(JSON.stringify({ |
|
||||||
type: messages.FIND_NEXT, |
|
||||||
}), '*'); |
|
||||||
break; |
|
||||||
case operations.FIND_PREV: |
|
||||||
window.top.postMessage(JSON.stringify({ |
|
||||||
type: messages.FIND_PREV, |
|
||||||
}), '*'); |
|
||||||
break; |
|
||||||
case operations.SCROLL_VERTICALLY: |
|
||||||
scrollPresenter.scrollVertically(operation.count, smoothscroll); |
|
||||||
break; |
|
||||||
case operations.SCROLL_HORIZONALLY: |
|
||||||
scrollPresenter.scrollHorizonally(operation.count, smoothscroll); |
|
||||||
break; |
|
||||||
case operations.SCROLL_PAGES: |
|
||||||
scrollPresenter.scrollPages(operation.count, smoothscroll); |
|
||||||
break; |
|
||||||
case operations.SCROLL_TOP: |
|
||||||
scrollPresenter.scrollToTop(smoothscroll); |
|
||||||
break; |
|
||||||
case operations.SCROLL_BOTTOM: |
|
||||||
scrollPresenter.scrollToBottom(smoothscroll); |
|
||||||
break; |
|
||||||
case operations.SCROLL_HOME: |
|
||||||
scrollPresenter.scrollToHome(smoothscroll); |
|
||||||
break; |
|
||||||
case operations.SCROLL_END: |
|
||||||
scrollPresenter.scrollToEnd(smoothscroll); |
|
||||||
break; |
|
||||||
case operations.FOLLOW_START: |
|
||||||
followMasterClient.startFollow(operation.newTab, operation.background); |
|
||||||
break; |
|
||||||
case operations.MARK_SET_PREFIX: |
|
||||||
return markActions.startSet(); |
|
||||||
case operations.MARK_JUMP_PREFIX: |
|
||||||
return markActions.startJump(); |
|
||||||
case operations.NAVIGATE_HISTORY_PREV: |
|
||||||
navigates.historyPrev(window); |
|
||||||
break; |
|
||||||
case operations.NAVIGATE_HISTORY_NEXT: |
|
||||||
navigates.historyNext(window); |
|
||||||
break; |
|
||||||
case operations.NAVIGATE_LINK_PREV: |
|
||||||
navigates.linkPrev(window); |
|
||||||
break; |
|
||||||
case operations.NAVIGATE_LINK_NEXT: |
|
||||||
navigates.linkNext(window); |
|
||||||
break; |
|
||||||
case operations.NAVIGATE_PARENT: |
|
||||||
navigates.parent(window); |
|
||||||
break; |
|
||||||
case operations.NAVIGATE_ROOT: |
|
||||||
navigates.root(window); |
|
||||||
break; |
|
||||||
case operations.FOCUS_INPUT: |
|
||||||
focuses.focusInput(); |
|
||||||
break; |
|
||||||
case operations.URLS_YANK: |
|
||||||
await clipbaordUseCase.yankCurrentURL(); |
|
||||||
break; |
|
||||||
case operations.URLS_PASTE: |
|
||||||
await clipbaordUseCase.openOrSearch( |
|
||||||
operation.newTab ? operation.newTab : false, |
|
||||||
); |
|
||||||
break; |
|
||||||
default: |
|
||||||
browser.runtime.sendMessage({ |
|
||||||
type: messages.BACKGROUND_OPERATION, |
|
||||||
operation, |
|
||||||
}); |
|
||||||
} |
|
||||||
return { type: actions.NOOP }; |
|
||||||
}; |
|
||||||
|
|
||||||
export { exec }; |
|
@ -1,104 +0,0 @@ |
|||||||
import MessageListener from '../../MessageListener'; |
|
||||||
import { LinkHint, InputHint } from '../../presenters/Hint'; |
|
||||||
import * as messages from '../../../shared/messages'; |
|
||||||
import Key from '../../domains/Key'; |
|
||||||
import TabsClient, { TabsClientImpl } from '../../client/TabsClient'; |
|
||||||
import FollowMasterClient, { FollowMasterClientImpl } |
|
||||||
from '../../client/FollowMasterClient'; |
|
||||||
import FollowPresenter, { FollowPresenterImpl } |
|
||||||
from '../../presenters/FollowPresenter'; |
|
||||||
|
|
||||||
let tabsClient: TabsClient = new TabsClientImpl(); |
|
||||||
let followMasterClient: FollowMasterClient = |
|
||||||
new FollowMasterClientImpl(window.top); |
|
||||||
let followPresenter: FollowPresenter = |
|
||||||
new FollowPresenterImpl(); |
|
||||||
|
|
||||||
interface Size { |
|
||||||
width: number; |
|
||||||
height: number; |
|
||||||
} |
|
||||||
|
|
||||||
interface Point { |
|
||||||
x: number; |
|
||||||
y: number; |
|
||||||
} |
|
||||||
|
|
||||||
export default class Follow { |
|
||||||
private enabled: boolean; |
|
||||||
|
|
||||||
constructor() { |
|
||||||
this.enabled = false; |
|
||||||
|
|
||||||
new MessageListener().onWebMessage(this.onMessage.bind(this)); |
|
||||||
} |
|
||||||
|
|
||||||
key(key: Key): boolean { |
|
||||||
if (!this.enabled) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
followMasterClient.sendKey(key); |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
countHints(viewSize: Size, framePosition: Point) { |
|
||||||
let count = followPresenter.getTargetCount(viewSize, framePosition); |
|
||||||
followMasterClient.responseHintCount(count); |
|
||||||
} |
|
||||||
|
|
||||||
createHints(viewSize: Size, framePosition: Point, tags: string[]) { |
|
||||||
this.enabled = true; |
|
||||||
followPresenter.createHints(viewSize, framePosition, tags); |
|
||||||
} |
|
||||||
|
|
||||||
showHints(prefix: string) { |
|
||||||
followPresenter.filterHints(prefix); |
|
||||||
} |
|
||||||
|
|
||||||
removeHints() { |
|
||||||
followPresenter.clearHints(); |
|
||||||
this.enabled = false; |
|
||||||
} |
|
||||||
|
|
||||||
async activateHints( |
|
||||||
tag: string, newTab: boolean, background: boolean, |
|
||||||
): Promise<void> { |
|
||||||
let hint = followPresenter.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 tabsClient.openUrl(url, newTab, background); |
|
||||||
} else if (hint instanceof InputHint) { |
|
||||||
hint.activate(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
onMessage(message: messages.Message, _sender: Window) { |
|
||||||
switch (message.type) { |
|
||||||
case messages.FOLLOW_REQUEST_COUNT_TARGETS: |
|
||||||
return this.countHints(message.viewSize, message.framePosition); |
|
||||||
case messages.FOLLOW_CREATE_HINTS: |
|
||||||
return this.createHints( |
|
||||||
message.viewSize, message.framePosition, message.tags); |
|
||||||
case messages.FOLLOW_SHOW_HINTS: |
|
||||||
return this.showHints(message.prefix); |
|
||||||
case messages.FOLLOW_ACTIVATE: |
|
||||||
return this.activateHints( |
|
||||||
message.tag, message.newTab, message.background); |
|
||||||
case messages.FOLLOW_REMOVE_HINTS: |
|
||||||
return this.removeHints(); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,59 +0,0 @@ |
|||||||
import InputDriver from './../../InputDriver'; |
|
||||||
import FollowComponent from './follow'; |
|
||||||
import MarkComponent from './mark'; |
|
||||||
// import KeymapperComponent from './keymapper';
|
|
||||||
import * as messages from '../../../shared/messages'; |
|
||||||
import MessageListener from '../../MessageListener'; |
|
||||||
import * as blacklists from '../../../shared/blacklists'; |
|
||||||
import Key from '../../domains/Key'; |
|
||||||
|
|
||||||
import AddonEnabledUseCase from '../../usecases/AddonEnabledUseCase'; |
|
||||||
import SettingUseCase from '../../usecases/SettingUseCase'; |
|
||||||
|
|
||||||
let addonEnabledUseCase = new AddonEnabledUseCase(); |
|
||||||
let settingUseCase = new SettingUseCase(); |
|
||||||
|
|
||||||
export default class Common { |
|
||||||
constructor(win: Window, store: any) { |
|
||||||
const input = new InputDriver(win.document.body); |
|
||||||
const follow = new FollowComponent(); |
|
||||||
const mark = new MarkComponent(store); |
|
||||||
// const keymapper = new KeymapperComponent(store);
|
|
||||||
|
|
||||||
input.onKey((key: Key) => follow.key(key)); |
|
||||||
input.onKey((key: Key) => mark.key(key)); |
|
||||||
// input.onKey((key: Key) => keymapper.key(key));
|
|
||||||
|
|
||||||
this.reloadSettings(); |
|
||||||
|
|
||||||
new MessageListener().onBackgroundMessage(this.onMessage.bind(this)); |
|
||||||
} |
|
||||||
|
|
||||||
onMessage(message: messages.Message) { |
|
||||||
switch (message.type) { |
|
||||||
case messages.SETTINGS_CHANGED: |
|
||||||
return this.reloadSettings(); |
|
||||||
case messages.ADDON_TOGGLE_ENABLED: |
|
||||||
return addonEnabledUseCase.toggle(); |
|
||||||
} |
|
||||||
return undefined; |
|
||||||
} |
|
||||||
|
|
||||||
async reloadSettings() { |
|
||||||
try { |
|
||||||
let current = await settingUseCase.reload(); |
|
||||||
let disabled = blacklists.includes( |
|
||||||
current.blacklist, window.location.href, |
|
||||||
); |
|
||||||
if (disabled) { |
|
||||||
addonEnabledUseCase.disable(); |
|
||||||
} else { |
|
||||||
addonEnabledUseCase.enable(); |
|
||||||
} |
|
||||||
} catch (e) { |
|
||||||
// Sometime sendMessage fails when background script is not ready.
|
|
||||||
console.warn(e); |
|
||||||
setTimeout(() => this.reloadSettings(), 500); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,44 +0,0 @@ |
|||||||
import * as markActions from '../../actions/mark'; |
|
||||||
import * as consoleFrames from '../..//console-frames'; |
|
||||||
import Key from '../../domains/Key'; |
|
||||||
|
|
||||||
import MarkUseCase from '../../usecases/MarkUseCase'; |
|
||||||
|
|
||||||
let markUseCase = new MarkUseCase(); |
|
||||||
|
|
||||||
const cancelKey = (key: Key): boolean => { |
|
||||||
return key.key === 'Esc' || key.key === '[' && Boolean(key.ctrlKey); |
|
||||||
}; |
|
||||||
|
|
||||||
export default class MarkComponent { |
|
||||||
private store: any; |
|
||||||
|
|
||||||
constructor(store: any) { |
|
||||||
this.store = store; |
|
||||||
} |
|
||||||
|
|
||||||
// eslint-disable-next-line max-statements
|
|
||||||
key(key: Key) { |
|
||||||
let { mark: markState } = this.store.getState(); |
|
||||||
|
|
||||||
if (!markState.setMode && !markState.jumpMode) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
if (cancelKey(key)) { |
|
||||||
this.store.dispatch(markActions.cancel()); |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
if (key.ctrlKey || key.metaKey || key.altKey) { |
|
||||||
consoleFrames.postError('Unknown mark'); |
|
||||||
} else if (markState.setMode) { |
|
||||||
markUseCase.set(key.key); |
|
||||||
} else if (markState.jumpMode) { |
|
||||||
markUseCase.jump(key.key); |
|
||||||
} |
|
||||||
|
|
||||||
this.store.dispatch(markActions.cancel()); |
|
||||||
return true; |
|
||||||
} |
|
||||||
} |
|
@ -1,3 +0,0 @@ |
|||||||
import CommonComponent from './common'; |
|
||||||
|
|
||||||
export default CommonComponent; |
|
@ -1,181 +0,0 @@ |
|||||||
import * as followControllerActions from '../../actions/follow-controller'; |
|
||||||
import * as messages from '../../../shared/messages'; |
|
||||||
import MessageListener from '../../MessageListener'; |
|
||||||
import HintKeyProducer from '../../hint-key-producer'; |
|
||||||
|
|
||||||
import { SettingRepositoryImpl } from '../../repositories/SettingRepository'; |
|
||||||
import FollowSlaveClient, { FollowSlaveClientImpl } |
|
||||||
from '../../client/FollowSlaveClient'; |
|
||||||
|
|
||||||
let settingRepository = new SettingRepositoryImpl(); |
|
||||||
|
|
||||||
export default class FollowController { |
|
||||||
private win: Window; |
|
||||||
|
|
||||||
private store: any; |
|
||||||
|
|
||||||
private state: { |
|
||||||
enabled?: boolean; |
|
||||||
newTab?: boolean; |
|
||||||
background?: boolean; |
|
||||||
keys?: string, |
|
||||||
}; |
|
||||||
|
|
||||||
private keys: string[]; |
|
||||||
|
|
||||||
private producer: HintKeyProducer | null; |
|
||||||
|
|
||||||
constructor(win: Window, store: any) { |
|
||||||
this.win = win; |
|
||||||
this.store = store; |
|
||||||
this.state = {}; |
|
||||||
this.keys = []; |
|
||||||
this.producer = null; |
|
||||||
|
|
||||||
new MessageListener().onWebMessage(this.onMessage.bind(this)); |
|
||||||
|
|
||||||
store.subscribe(() => { |
|
||||||
this.update(); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
onMessage(message: messages.Message, sender: Window) { |
|
||||||
switch (message.type) { |
|
||||||
case messages.FOLLOW_START: |
|
||||||
return this.store.dispatch( |
|
||||||
followControllerActions.enable(message.newTab, message.background)); |
|
||||||
case messages.FOLLOW_RESPONSE_COUNT_TARGETS: |
|
||||||
return this.create(message.count, sender); |
|
||||||
case messages.FOLLOW_KEY_PRESS: |
|
||||||
return this.keyPress(message.key, message.ctrlKey); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
update(): void { |
|
||||||
let prevState = this.state; |
|
||||||
this.state = this.store.getState().followController; |
|
||||||
|
|
||||||
if (!prevState.enabled && this.state.enabled) { |
|
||||||
this.count(); |
|
||||||
} else if (prevState.enabled && !this.state.enabled) { |
|
||||||
this.remove(); |
|
||||||
} else if (prevState.keys !== this.state.keys) { |
|
||||||
this.updateHints(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
updateHints(): void { |
|
||||||
let shown = this.keys.filter((key) => { |
|
||||||
return key.startsWith(this.state.keys as string); |
|
||||||
}); |
|
||||||
if (shown.length === 1) { |
|
||||||
this.activate(); |
|
||||||
this.store.dispatch(followControllerActions.disable()); |
|
||||||
} |
|
||||||
|
|
||||||
this.broadcastMessage((c: FollowSlaveClient) => { |
|
||||||
c.filterHints(this.state.keys!!); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
activate(): void { |
|
||||||
this.broadcastMessage((c: FollowSlaveClient) => { |
|
||||||
c.activateIfExists( |
|
||||||
this.state.keys!!, |
|
||||||
this.state.newTab!!, |
|
||||||
this.state.background!!); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
keyPress(key: string, ctrlKey: boolean): boolean { |
|
||||||
if (key === '[' && ctrlKey) { |
|
||||||
this.store.dispatch(followControllerActions.disable()); |
|
||||||
return true; |
|
||||||
} |
|
||||||
switch (key) { |
|
||||||
case 'Enter': |
|
||||||
this.activate(); |
|
||||||
this.store.dispatch(followControllerActions.disable()); |
|
||||||
break; |
|
||||||
case 'Esc': |
|
||||||
this.store.dispatch(followControllerActions.disable()); |
|
||||||
break; |
|
||||||
case 'Backspace': |
|
||||||
case 'Delete': |
|
||||||
this.store.dispatch(followControllerActions.backspace()); |
|
||||||
break; |
|
||||||
default: |
|
||||||
if (this.hintchars().includes(key)) { |
|
||||||
this.store.dispatch(followControllerActions.keyPress(key)); |
|
||||||
} |
|
||||||
break; |
|
||||||
} |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
count() { |
|
||||||
this.producer = new HintKeyProducer(this.hintchars()); |
|
||||||
let doc = this.win.document; |
|
||||||
let viewWidth = this.win.innerWidth || doc.documentElement.clientWidth; |
|
||||||
let viewHeight = this.win.innerHeight || doc.documentElement.clientHeight; |
|
||||||
let frameElements = this.win.document.querySelectorAll('iframe'); |
|
||||||
|
|
||||||
new FollowSlaveClientImpl(this.win).requestHintCount( |
|
||||||
{ width: viewWidth, height: viewHeight }, |
|
||||||
{ x: 0, y: 0 }); |
|
||||||
|
|
||||||
for (let ele of Array.from(frameElements)) { |
|
||||||
let { left: frameX, top: frameY } = ele.getBoundingClientRect(); |
|
||||||
new FollowSlaveClientImpl(ele.contentWindow!!).requestHintCount( |
|
||||||
{ width: viewWidth, height: viewHeight }, |
|
||||||
{ x: frameX, y: frameY }, |
|
||||||
); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
create(count: number, sender: Window) { |
|
||||||
let produced = []; |
|
||||||
for (let i = 0; i < count; ++i) { |
|
||||||
produced.push((this.producer as HintKeyProducer).produce()); |
|
||||||
} |
|
||||||
this.keys = this.keys.concat(produced); |
|
||||||
|
|
||||||
let doc = this.win.document; |
|
||||||
let viewWidth = this.win.innerWidth || doc.documentElement.clientWidth; |
|
||||||
let viewHeight = this.win.innerHeight || doc.documentElement.clientHeight; |
|
||||||
let pos = { x: 0, y: 0 }; |
|
||||||
if (sender !== window) { |
|
||||||
let frameElements = this.win.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, |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
remove() { |
|
||||||
this.keys = []; |
|
||||||
this.broadcastMessage((c: FollowSlaveClient) => { |
|
||||||
c.clearHints(); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
private hintchars() { |
|
||||||
return settingRepository.get().properties.hintchars; |
|
||||||
} |
|
||||||
|
|
||||||
private broadcastMessage(f: (clinet: FollowSlaveClient) => void) { |
|
||||||
let windows = [window.self].concat(Array.from(window.frames as any)); |
|
||||||
windows |
|
||||||
.map(w => new FollowSlaveClientImpl(w)) |
|
||||||
.forEach(c => f(c)); |
|
||||||
} |
|
||||||
} |
|
@ -1,47 +0,0 @@ |
|||||||
import CommonComponent from '../common'; |
|
||||||
import FollowController from './follow-controller'; |
|
||||||
import * as consoleFrames from '../../console-frames'; |
|
||||||
import * as messages from '../../../shared/messages'; |
|
||||||
import MessageListener from '../../MessageListener'; |
|
||||||
import AddonEnabledUseCase from '../../usecases/AddonEnabledUseCase'; |
|
||||||
import { ScrollPresenterImpl } from '../../presenters/ScrollPresenter'; |
|
||||||
|
|
||||||
let addonEnabledUseCase = new AddonEnabledUseCase(); |
|
||||||
let scrollPresenter = new ScrollPresenterImpl(); |
|
||||||
|
|
||||||
export default class TopContent { |
|
||||||
private win: Window; |
|
||||||
|
|
||||||
constructor(win: Window, store: any) { |
|
||||||
this.win = win; |
|
||||||
|
|
||||||
new CommonComponent(win, store); // eslint-disable-line no-new
|
|
||||||
new FollowController(win, store); // eslint-disable-line no-new
|
|
||||||
|
|
||||||
// TODO make component
|
|
||||||
consoleFrames.initialize(this.win.document); |
|
||||||
|
|
||||||
new MessageListener().onWebMessage(this.onWebMessage.bind(this)); |
|
||||||
new MessageListener().onBackgroundMessage( |
|
||||||
this.onBackgroundMessage.bind(this)); |
|
||||||
} |
|
||||||
|
|
||||||
onWebMessage(message: messages.Message) { |
|
||||||
switch (message.type) { |
|
||||||
case messages.CONSOLE_UNFOCUS: |
|
||||||
this.win.focus(); |
|
||||||
consoleFrames.blur(window.document); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
onBackgroundMessage(message: messages.Message) { |
|
||||||
let addonEnabled = addonEnabledUseCase.getEnabled(); |
|
||||||
|
|
||||||
switch (message.type) { |
|
||||||
case messages.ADDON_ENABLED_QUERY: |
|
||||||
return Promise.resolve(addonEnabled); |
|
||||||
case messages.TAB_SCROLL_TO: |
|
||||||
return scrollPresenter.scrollTo(message.x, message.y, false); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,15 +0,0 @@ |
|||||||
import * as doms from '../shared/utils/dom'; |
|
||||||
|
|
||||||
const focusInput = (): void => { |
|
||||||
let inputTypes = ['email', 'number', 'search', 'tel', 'text', 'url']; |
|
||||||
let inputSelector = inputTypes.map(type => `input[type=${type}]`).join(','); |
|
||||||
let targets = window.document.querySelectorAll(inputSelector + ',textarea'); |
|
||||||
let target = Array.from(targets).find(doms.isVisible); |
|
||||||
if (target instanceof HTMLInputElement) { |
|
||||||
target.focus(); |
|
||||||
} else if (target instanceof HTMLTextAreaElement) { |
|
||||||
target.focus(); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
export { focusInput }; |
|
@ -1,40 +0,0 @@ |
|||||||
import * as actions from '../actions'; |
|
||||||
|
|
||||||
export interface State { |
|
||||||
enabled: boolean; |
|
||||||
newTab: boolean; |
|
||||||
background: boolean; |
|
||||||
keys: string, |
|
||||||
} |
|
||||||
|
|
||||||
const defaultState: State = { |
|
||||||
enabled: false, |
|
||||||
newTab: false, |
|
||||||
background: false, |
|
||||||
keys: '', |
|
||||||
}; |
|
||||||
|
|
||||||
export default function reducer( |
|
||||||
state: State = defaultState, |
|
||||||
action: actions.FollowAction, |
|
||||||
): State { |
|
||||||
switch (action.type) { |
|
||||||
case actions.FOLLOW_CONTROLLER_ENABLE: |
|
||||||
return { ...state, |
|
||||||
enabled: true, |
|
||||||
newTab: action.newTab, |
|
||||||
background: action.background, |
|
||||||
keys: '', }; |
|
||||||
case actions.FOLLOW_CONTROLLER_DISABLE: |
|
||||||
return { ...state, |
|
||||||
enabled: false, }; |
|
||||||
case actions.FOLLOW_CONTROLLER_KEY_PRESS: |
|
||||||
return { ...state, |
|
||||||
keys: state.keys + action.key, }; |
|
||||||
case actions.FOLLOW_CONTROLLER_BACKSPACE: |
|
||||||
return { ...state, |
|
||||||
keys: state.keys.slice(0, -1), }; |
|
||||||
default: |
|
||||||
return state; |
|
||||||
} |
|
||||||
} |
|
@ -1,15 +0,0 @@ |
|||||||
import { combineReducers } from 'redux'; |
|
||||||
import input, { State as InputState } from './input'; |
|
||||||
import followController, { State as FollowControllerState } |
|
||||||
from './follow-controller'; |
|
||||||
import mark, { State as MarkState } from './mark'; |
|
||||||
|
|
||||||
export interface State { |
|
||||||
input: InputState; |
|
||||||
followController: FollowControllerState; |
|
||||||
mark: MarkState; |
|
||||||
} |
|
||||||
|
|
||||||
export default combineReducers({ |
|
||||||
input, followController, mark, |
|
||||||
}); |
|
@ -1,26 +0,0 @@ |
|||||||
import * as actions from '../actions'; |
|
||||||
import Key from '../domains/Key'; |
|
||||||
|
|
||||||
export interface State { |
|
||||||
keys: Key[], |
|
||||||
} |
|
||||||
|
|
||||||
const defaultState: State = { |
|
||||||
keys: [] |
|
||||||
}; |
|
||||||
|
|
||||||
export default function reducer( |
|
||||||
state: State = defaultState, |
|
||||||
action: actions.InputAction, |
|
||||||
): State { |
|
||||||
switch (action.type) { |
|
||||||
case actions.INPUT_KEY_PRESS: |
|
||||||
return { ...state, |
|
||||||
keys: state.keys.concat([action.key]), }; |
|
||||||
case actions.INPUT_CLEAR_KEYS: |
|
||||||
return { ...state, |
|
||||||
keys: [], }; |
|
||||||
default: |
|
||||||
return state; |
|
||||||
} |
|
||||||
} |
|
@ -1,27 +0,0 @@ |
|||||||
import * as actions from '../actions'; |
|
||||||
|
|
||||||
export interface State { |
|
||||||
setMode: boolean; |
|
||||||
jumpMode: boolean; |
|
||||||
} |
|
||||||
|
|
||||||
const defaultState: State = { |
|
||||||
setMode: false, |
|
||||||
jumpMode: false, |
|
||||||
}; |
|
||||||
|
|
||||||
export default function reducer( |
|
||||||
state: State = defaultState, |
|
||||||
action: actions.MarkAction, |
|
||||||
): State { |
|
||||||
switch (action.type) { |
|
||||||
case actions.MARK_START_SET: |
|
||||||
return { ...state, setMode: true }; |
|
||||||
case actions.MARK_START_JUMP: |
|
||||||
return { ...state, jumpMode: true }; |
|
||||||
case actions.MARK_CANCEL: |
|
||||||
return { ...state, setMode: false, jumpMode: false }; |
|
||||||
default: |
|
||||||
return state; |
|
||||||
} |
|
||||||
} |
|
@ -1,8 +0,0 @@ |
|||||||
import promise from 'redux-promise'; |
|
||||||
import reducers from '../reducers'; |
|
||||||
import { createStore, applyMiddleware } from 'redux'; |
|
||||||
|
|
||||||
export const newStore = () => createStore( |
|
||||||
reducers, |
|
||||||
applyMiddleware(promise), |
|
||||||
); |
|
@ -1,34 +0,0 @@ |
|||||||
import * as actions from 'content/actions'; |
|
||||||
import * as followControllerActions from 'content/actions/follow-controller'; |
|
||||||
|
|
||||||
describe('follow-controller actions', () => { |
|
||||||
describe('enable', () => { |
|
||||||
it('creates FOLLOW_CONTROLLER_ENABLE action', () => { |
|
||||||
let action = followControllerActions.enable(true); |
|
||||||
expect(action.type).to.equal(actions.FOLLOW_CONTROLLER_ENABLE); |
|
||||||
expect(action.newTab).to.equal(true); |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('disable', () => { |
|
||||||
it('creates FOLLOW_CONTROLLER_DISABLE action', () => { |
|
||||||
let action = followControllerActions.disable(true); |
|
||||||
expect(action.type).to.equal(actions.FOLLOW_CONTROLLER_DISABLE); |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('keyPress', () => { |
|
||||||
it('creates FOLLOW_CONTROLLER_KEY_PRESS action', () => { |
|
||||||
let action = followControllerActions.keyPress(100); |
|
||||||
expect(action.type).to.equal(actions.FOLLOW_CONTROLLER_KEY_PRESS); |
|
||||||
expect(action.key).to.equal(100); |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('backspace', () => { |
|
||||||
it('creates FOLLOW_CONTROLLER_BACKSPACE action', () => { |
|
||||||
let action = followControllerActions.backspace(100); |
|
||||||
expect(action.type).to.equal(actions.FOLLOW_CONTROLLER_BACKSPACE); |
|
||||||
}); |
|
||||||
}); |
|
||||||
}); |
|
@ -1,19 +0,0 @@ |
|||||||
import * as actions from 'content/actions'; |
|
||||||
import * as inputActions from 'content/actions/input'; |
|
||||||
|
|
||||||
describe("input actions", () => { |
|
||||||
describe("keyPress", () => { |
|
||||||
it('create INPUT_KEY_PRESS action', () => { |
|
||||||
let action = inputActions.keyPress('a'); |
|
||||||
expect(action.type).to.equal(actions.INPUT_KEY_PRESS); |
|
||||||
expect(action.key).to.equal('a'); |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe("clearKeys", () => { |
|
||||||
it('create INPUT_CLEAR_KEYSaction', () => { |
|
||||||
let action = inputActions.clearKeys(); |
|
||||||
expect(action.type).to.equal(actions.INPUT_CLEAR_KEYS); |
|
||||||
}); |
|
||||||
}); |
|
||||||
}); |
|
@ -1,25 +0,0 @@ |
|||||||
import * as actions from 'content/actions'; |
|
||||||
import * as markActions from 'content/actions/mark'; |
|
||||||
|
|
||||||
describe('mark actions', () => { |
|
||||||
describe('startSet', () => { |
|
||||||
it('create MARK_START_SET action', () => { |
|
||||||
let action = markActions.startSet(); |
|
||||||
expect(action.type).to.equal(actions.MARK_START_SET); |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('startJump', () => { |
|
||||||
it('create MARK_START_JUMP action', () => { |
|
||||||
let action = markActions.startJump(); |
|
||||||
expect(action.type).to.equal(actions.MARK_START_JUMP); |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('cancel', () => { |
|
||||||
it('create MARK_CANCEL action', () => { |
|
||||||
let action = markActions.cancel(); |
|
||||||
expect(action.type).to.equal(actions.MARK_CANCEL); |
|
||||||
}); |
|
||||||
}); |
|
||||||
}); |
|
@ -1,17 +0,0 @@ |
|||||||
<!DOCTYPE html> |
|
||||||
<html> |
|
||||||
<body> |
|
||||||
<a id='visible_a' href='#' >link</a> |
|
||||||
<a href='#' style='display:none'>invisible 1</a> |
|
||||||
<a href='#' style='visibility:hidden'>invisible 2</a> |
|
||||||
<i>not link<i> |
|
||||||
<div id='editable_div_1' contenteditable>link</div> |
|
||||||
<div id='editable_div_2' contenteditable='true'>link</div> |
|
||||||
<div id='x' contenteditable='false'>link</div> |
|
||||||
<details> |
|
||||||
<summary id='summary_1'>summary link</summary> |
|
||||||
Some details |
|
||||||
<a href='#'>not visible</a> |
|
||||||
</details> |
|
||||||
</body> |
|
||||||
</html> |
|
@ -1,25 +0,0 @@ |
|||||||
import FollowComponent from 'content/components/common/follow'; |
|
||||||
|
|
||||||
describe('FollowComponent', () => { |
|
||||||
describe('#getTargetElements', () => { |
|
||||||
beforeEach(() => { |
|
||||||
document.body.innerHTML = __html__['test/content/components/common/follow.html']; |
|
||||||
}); |
|
||||||
|
|
||||||
it('returns visible links', () => { |
|
||||||
let targets = FollowComponent.getTargetElements( |
|
||||||
window, |
|
||||||
{ width: window.innerWidth, height: window.innerHeight }, |
|
||||||
{ x: 0, y: 0 }); |
|
||||||
expect(targets).to.have.lengthOf(4); |
|
||||||
|
|
||||||
let ids = Array.prototype.map.call(targets, (e) => e.id); |
|
||||||
expect(ids).to.include.members([ |
|
||||||
'visible_a', |
|
||||||
'editable_div_1', |
|
||||||
'editable_div_2', |
|
||||||
'summary_1', |
|
||||||
]); |
|
||||||
}); |
|
||||||
}); |
|
||||||
}); |
|
@ -1,47 +0,0 @@ |
|||||||
import * as actions from 'content/actions'; |
|
||||||
import followControllerReducer from 'content/reducers/follow-controller'; |
|
||||||
|
|
||||||
describe('follow-controller reducer', () => { |
|
||||||
it ('returns the initial state', () => { |
|
||||||
let state = followControllerReducer(undefined, {}); |
|
||||||
expect(state).to.have.property('enabled', false); |
|
||||||
expect(state).to.have.property('newTab'); |
|
||||||
expect(state).to.have.deep.property('keys', ''); |
|
||||||
}); |
|
||||||
|
|
||||||
it ('returns next state for FOLLOW_CONTROLLER_ENABLE', () => { |
|
||||||
let action = { type: actions.FOLLOW_CONTROLLER_ENABLE, newTab: true }; |
|
||||||
let state = followControllerReducer({ enabled: false, newTab: false }, action); |
|
||||||
expect(state).to.have.property('enabled', true); |
|
||||||
expect(state).to.have.property('newTab', true); |
|
||||||
expect(state).to.have.property('keys', ''); |
|
||||||
}); |
|
||||||
|
|
||||||
it ('returns next state for FOLLOW_CONTROLLER_DISABLE', () => { |
|
||||||
let action = { type: actions.FOLLOW_CONTROLLER_DISABLE }; |
|
||||||
let state = followControllerReducer({ enabled: true }, action); |
|
||||||
expect(state).to.have.property('enabled', false); |
|
||||||
}); |
|
||||||
|
|
||||||
it ('returns next state for FOLLOW_CONTROLLER_KEY_PRESS', () => { |
|
||||||
let action = { type: actions.FOLLOW_CONTROLLER_KEY_PRESS, key: 'a'}; |
|
||||||
let state = followControllerReducer({ keys: '' }, action); |
|
||||||
expect(state).to.have.deep.property('keys', 'a'); |
|
||||||
|
|
||||||
action = { type: actions.FOLLOW_CONTROLLER_KEY_PRESS, key: 'b'}; |
|
||||||
state = followControllerReducer(state, action); |
|
||||||
expect(state).to.have.deep.property('keys', 'ab'); |
|
||||||
}); |
|
||||||
|
|
||||||
it ('returns next state for FOLLOW_CONTROLLER_BACKSPACE', () => { |
|
||||||
let action = { type: actions.FOLLOW_CONTROLLER_BACKSPACE }; |
|
||||||
let state = followControllerReducer({ keys: 'ab' }, action); |
|
||||||
expect(state).to.have.deep.property('keys', 'a'); |
|
||||||
|
|
||||||
state = followControllerReducer(state, action); |
|
||||||
expect(state).to.have.deep.property('keys', ''); |
|
||||||
|
|
||||||
state = followControllerReducer(state, action); |
|
||||||
expect(state).to.have.deep.property('keys', ''); |
|
||||||
}); |
|
||||||
}); |
|
@ -1,25 +0,0 @@ |
|||||||
import * as actions from 'content/actions'; |
|
||||||
import inputReducer from 'content/reducers/input'; |
|
||||||
|
|
||||||
describe("input reducer", () => { |
|
||||||
it('return the initial state', () => { |
|
||||||
let state = inputReducer(undefined, {}); |
|
||||||
expect(state).to.have.deep.property('keys', []); |
|
||||||
}); |
|
||||||
|
|
||||||
it('return next state for INPUT_KEY_PRESS', () => { |
|
||||||
let action = { type: actions.INPUT_KEY_PRESS, key: 'a' }; |
|
||||||
let state = inputReducer(undefined, action); |
|
||||||
expect(state).to.have.deep.property('keys', ['a']); |
|
||||||
|
|
||||||
action = { type: actions.INPUT_KEY_PRESS, key: 'b' }; |
|
||||||
state = inputReducer(state, action); |
|
||||||
expect(state).to.have.deep.property('keys', ['a', 'b']); |
|
||||||
}); |
|
||||||
|
|
||||||
it('return next state for INPUT_CLEAR_KEYS', () => { |
|
||||||
let action = { type: actions.INPUT_CLEAR_KEYS }; |
|
||||||
let state = inputReducer({ keys: [1, 2, 3] }, action); |
|
||||||
expect(state).to.have.deep.property('keys', []); |
|
||||||
}); |
|
||||||
}); |
|
@ -1,31 +0,0 @@ |
|||||||
import * as actions from 'content/actions'; |
|
||||||
import reducer from 'content/reducers/mark'; |
|
||||||
|
|
||||||
describe("mark reducer", () => { |
|
||||||
it('return the initial state', () => { |
|
||||||
let state = reducer(undefined, {}); |
|
||||||
expect(state.setMode).to.be.false; |
|
||||||
expect(state.jumpMode).to.be.false; |
|
||||||
}); |
|
||||||
|
|
||||||
it('starts set mode', () => { |
|
||||||
let action = { type: actions.MARK_START_SET }; |
|
||||||
let state = reducer(undefined, action); |
|
||||||
expect(state.setMode).to.be.true; |
|
||||||
}); |
|
||||||
|
|
||||||
it('starts jump mode', () => { |
|
||||||
let action = { type: actions.MARK_START_JUMP }; |
|
||||||
let state = reducer(undefined, action); |
|
||||||
expect(state.jumpMode).to.be.true; |
|
||||||
}); |
|
||||||
|
|
||||||
it('cancels set and jump mode', () => { |
|
||||||
let action = { type: actions.MARK_CANCEL }; |
|
||||||
let state = reducer({ setMode: true }, action); |
|
||||||
expect(state.setMode).to.be.false; |
|
||||||
|
|
||||||
state = reducer({ jumpMode: true }, action); |
|
||||||
expect(state.jumpMode).to.be.false; |
|
||||||
}); |
|
||||||
}); |
|
Reference in new issue