commit
3f4bc62ed5
131 changed files with 3825 additions and 2350 deletions
@ -1,19 +0,0 @@ |
|||||||
import * as messages from '../../shared/messages'; |
|
||||||
import * as actions from './index'; |
|
||||||
|
|
||||||
const enable = (): Promise<actions.AddonAction> => setEnabled(true); |
|
||||||
|
|
||||||
const disable = (): Promise<actions.AddonAction> => setEnabled(false); |
|
||||||
|
|
||||||
const setEnabled = async(enabled: boolean): Promise<actions.AddonAction> => { |
|
||||||
await browser.runtime.sendMessage({ |
|
||||||
type: messages.ADDON_ENABLED_RESPONSE, |
|
||||||
enabled, |
|
||||||
}); |
|
||||||
return { |
|
||||||
type: actions.ADDON_SET_ENABLED, |
|
||||||
enabled, |
|
||||||
}; |
|
||||||
}; |
|
||||||
|
|
||||||
export { enable, disable, setEnabled }; |
|
@ -1,100 +0,0 @@ |
|||||||
//
|
|
||||||
// window.find(aString, aCaseSensitive, aBackwards, aWrapAround,
|
|
||||||
// aWholeWord, aSearchInFrames);
|
|
||||||
//
|
|
||||||
// NOTE: window.find is not standard API
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Window/find
|
|
||||||
|
|
||||||
import * as messages from '../../shared/messages'; |
|
||||||
import * as actions from './index'; |
|
||||||
import * as consoleFrames from '../console-frames'; |
|
||||||
|
|
||||||
interface MyWindow extends Window { |
|
||||||
find( |
|
||||||
aString: string, |
|
||||||
aCaseSensitive?: boolean, |
|
||||||
aBackwards?: boolean, |
|
||||||
aWrapAround?: boolean, |
|
||||||
aWholeWord?: boolean, |
|
||||||
aSearchInFrames?: boolean, |
|
||||||
aShowDialog?: boolean): boolean; |
|
||||||
} |
|
||||||
|
|
||||||
// eslint-disable-next-line no-var, vars-on-top, init-declarations
|
|
||||||
declare var window: MyWindow; |
|
||||||
|
|
||||||
const find = (str: string, backwards: boolean): boolean => { |
|
||||||
let caseSensitive = false; |
|
||||||
let wrapScan = true; |
|
||||||
|
|
||||||
|
|
||||||
// NOTE: aWholeWord dows not implemented, and aSearchInFrames does not work
|
|
||||||
// because of same origin policy
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-extra-parens
|
|
||||||
let found = window.find(str, caseSensitive, backwards, wrapScan); |
|
||||||
if (found) { |
|
||||||
return found; |
|
||||||
} |
|
||||||
let sel = window.getSelection(); |
|
||||||
if (sel) { |
|
||||||
sel.removeAllRanges(); |
|
||||||
} |
|
||||||
|
|
||||||
// eslint-disable-next-line no-extra-parens
|
|
||||||
return window.find(str, caseSensitive, backwards, wrapScan); |
|
||||||
}; |
|
||||||
|
|
||||||
// eslint-disable-next-line max-statements
|
|
||||||
const findNext = async( |
|
||||||
currentKeyword: string, reset: boolean, backwards: boolean, |
|
||||||
): Promise<actions.FindAction> => { |
|
||||||
if (reset) { |
|
||||||
let sel = window.getSelection(); |
|
||||||
if (sel) { |
|
||||||
sel.removeAllRanges(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
let keyword = currentKeyword; |
|
||||||
if (currentKeyword) { |
|
||||||
browser.runtime.sendMessage({ |
|
||||||
type: messages.FIND_SET_KEYWORD, |
|
||||||
keyword: currentKeyword, |
|
||||||
}); |
|
||||||
} else { |
|
||||||
keyword = await browser.runtime.sendMessage({ |
|
||||||
type: messages.FIND_GET_KEYWORD, |
|
||||||
}); |
|
||||||
} |
|
||||||
if (!keyword) { |
|
||||||
await consoleFrames.postError('No previous search keywords'); |
|
||||||
return { type: actions.NOOP }; |
|
||||||
} |
|
||||||
let found = find(keyword, backwards); |
|
||||||
if (found) { |
|
||||||
consoleFrames.postInfo('Pattern found: ' + keyword); |
|
||||||
} else { |
|
||||||
consoleFrames.postError('Pattern not found: ' + keyword); |
|
||||||
} |
|
||||||
|
|
||||||
return { |
|
||||||
type: actions.FIND_SET_KEYWORD, |
|
||||||
keyword, |
|
||||||
found, |
|
||||||
}; |
|
||||||
}; |
|
||||||
|
|
||||||
const next = ( |
|
||||||
currentKeyword: string, reset: boolean, |
|
||||||
): Promise<actions.FindAction> => { |
|
||||||
return findNext(currentKeyword, reset, false); |
|
||||||
}; |
|
||||||
|
|
||||||
const prev = ( |
|
||||||
currentKeyword: string, reset: boolean, |
|
||||||
): Promise<actions.FindAction> => { |
|
||||||
return findNext(currentKeyword, reset, true); |
|
||||||
}; |
|
||||||
|
|
||||||
export { next, prev }; |
|
@ -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,122 +0,0 @@ |
|||||||
import Redux from 'redux'; |
|
||||||
import Settings from '../../shared/Settings'; |
|
||||||
import * as keyUtils from '../../shared/utils/keys'; |
|
||||||
|
|
||||||
// Enable/disable
|
|
||||||
export const ADDON_SET_ENABLED = 'addon.set.enabled'; |
|
||||||
|
|
||||||
// Find
|
|
||||||
export const FIND_SET_KEYWORD = 'find.set.keyword'; |
|
||||||
|
|
||||||
// Settings
|
|
||||||
export const SETTING_SET = 'setting.set'; |
|
||||||
|
|
||||||
// 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 MARK_SET_LOCAL = 'mark.set.local'; |
|
||||||
|
|
||||||
export const NOOP = 'noop'; |
|
||||||
|
|
||||||
export interface AddonSetEnabledAction extends Redux.Action { |
|
||||||
type: typeof ADDON_SET_ENABLED; |
|
||||||
enabled: boolean; |
|
||||||
} |
|
||||||
|
|
||||||
export interface FindSetKeywordAction extends Redux.Action { |
|
||||||
type: typeof FIND_SET_KEYWORD; |
|
||||||
keyword: string; |
|
||||||
found: boolean; |
|
||||||
} |
|
||||||
|
|
||||||
export interface SettingSetAction extends Redux.Action { |
|
||||||
type: typeof SETTING_SET; |
|
||||||
settings: Settings, |
|
||||||
} |
|
||||||
|
|
||||||
export interface InputKeyPressAction extends Redux.Action { |
|
||||||
type: typeof INPUT_KEY_PRESS; |
|
||||||
key: keyUtils.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 MarkSetLocalAction extends Redux.Action { |
|
||||||
type: typeof MARK_SET_LOCAL; |
|
||||||
key: string; |
|
||||||
x: number; |
|
||||||
y: number; |
|
||||||
} |
|
||||||
|
|
||||||
export interface NoopAction extends Redux.Action { |
|
||||||
type: typeof NOOP; |
|
||||||
} |
|
||||||
|
|
||||||
export type AddonAction = AddonSetEnabledAction; |
|
||||||
export type FindAction = FindSetKeywordAction | NoopAction; |
|
||||||
export type SettingAction = SettingSetAction; |
|
||||||
export type InputAction = InputKeyPressAction | InputClearKeysAction; |
|
||||||
export type FollowAction = |
|
||||||
FollowControllerEnableAction | FollowControllerDisableAction | |
|
||||||
FollowControllerKeyPressAction | FollowControllerBackspaceAction; |
|
||||||
export type MarkAction = |
|
||||||
MarkStartSetAction | MarkStartJumpAction | |
|
||||||
MarkCancelAction | MarkSetLocalAction | NoopAction; |
|
||||||
|
|
||||||
export type Action = |
|
||||||
AddonAction | |
|
||||||
FindAction | |
|
||||||
SettingAction | |
|
||||||
InputAction | |
|
||||||
FollowAction | |
|
||||||
MarkAction | |
|
||||||
NoopAction; |
|
@ -1,17 +0,0 @@ |
|||||||
import * as actions from './index'; |
|
||||||
import * as keyUtils from '../../shared/utils/keys'; |
|
||||||
|
|
||||||
const keyPress = (key: keyUtils.Key): actions.InputAction => { |
|
||||||
return { |
|
||||||
type: actions.INPUT_KEY_PRESS, |
|
||||||
key, |
|
||||||
}; |
|
||||||
}; |
|
||||||
|
|
||||||
const clearKeys = (): actions.InputAction => { |
|
||||||
return { |
|
||||||
type: actions.INPUT_CLEAR_KEYS |
|
||||||
}; |
|
||||||
}; |
|
||||||
|
|
||||||
export { keyPress, clearKeys }; |
|
@ -1,46 +0,0 @@ |
|||||||
import * as actions from './index'; |
|
||||||
import * as messages from '../../shared/messages'; |
|
||||||
|
|
||||||
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 }; |
|
||||||
}; |
|
||||||
|
|
||||||
const setLocal = (key: string, x: number, y: number): actions.MarkAction => { |
|
||||||
return { |
|
||||||
type: actions.MARK_SET_LOCAL, |
|
||||||
key, |
|
||||||
x, |
|
||||||
y, |
|
||||||
}; |
|
||||||
}; |
|
||||||
|
|
||||||
const setGlobal = (key: string, x: number, y: number): actions.MarkAction => { |
|
||||||
browser.runtime.sendMessage({ |
|
||||||
type: messages.MARK_SET_GLOBAL, |
|
||||||
key, |
|
||||||
x, |
|
||||||
y, |
|
||||||
}); |
|
||||||
return { type: actions.NOOP }; |
|
||||||
}; |
|
||||||
|
|
||||||
const jumpGlobal = (key: string): actions.MarkAction => { |
|
||||||
browser.runtime.sendMessage({ |
|
||||||
type: messages.MARK_JUMP_GLOBAL, |
|
||||||
key, |
|
||||||
}); |
|
||||||
return { type: actions.NOOP }; |
|
||||||
}; |
|
||||||
|
|
||||||
export { |
|
||||||
startSet, startJump, cancel, setLocal, |
|
||||||
setGlobal, jumpGlobal, |
|
||||||
}; |
|
@ -1,107 +0,0 @@ |
|||||||
import * as operations from '../../shared/operations'; |
|
||||||
import * as actions from './index'; |
|
||||||
import * as messages from '../../shared/messages'; |
|
||||||
import * as scrolls from '../scrolls'; |
|
||||||
import * as navigates from '../navigates'; |
|
||||||
import * as focuses from '../focuses'; |
|
||||||
import * as urls from '../urls'; |
|
||||||
import * as consoleFrames from '../console-frames'; |
|
||||||
import * as addonActions from './addon'; |
|
||||||
import * as markActions from './mark'; |
|
||||||
|
|
||||||
// eslint-disable-next-line complexity, max-lines-per-function
|
|
||||||
const exec = ( |
|
||||||
operation: operations.Operation, |
|
||||||
settings: any, |
|
||||||
addonEnabled: boolean, |
|
||||||
): Promise<actions.Action> | actions.Action => { |
|
||||||
let smoothscroll = settings.properties.smoothscroll; |
|
||||||
switch (operation.type) { |
|
||||||
case operations.ADDON_ENABLE: |
|
||||||
return addonActions.enable(); |
|
||||||
case operations.ADDON_DISABLE: |
|
||||||
return addonActions.disable(); |
|
||||||
case operations.ADDON_TOGGLE_ENABLED: |
|
||||||
return addonActions.setEnabled(!addonEnabled); |
|
||||||
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: |
|
||||||
scrolls.scrollVertically(operation.count, smoothscroll); |
|
||||||
break; |
|
||||||
case operations.SCROLL_HORIZONALLY: |
|
||||||
scrolls.scrollHorizonally(operation.count, smoothscroll); |
|
||||||
break; |
|
||||||
case operations.SCROLL_PAGES: |
|
||||||
scrolls.scrollPages(operation.count, smoothscroll); |
|
||||||
break; |
|
||||||
case operations.SCROLL_TOP: |
|
||||||
scrolls.scrollToTop(smoothscroll); |
|
||||||
break; |
|
||||||
case operations.SCROLL_BOTTOM: |
|
||||||
scrolls.scrollToBottom(smoothscroll); |
|
||||||
break; |
|
||||||
case operations.SCROLL_HOME: |
|
||||||
scrolls.scrollToHome(smoothscroll); |
|
||||||
break; |
|
||||||
case operations.SCROLL_END: |
|
||||||
scrolls.scrollToEnd(smoothscroll); |
|
||||||
break; |
|
||||||
case operations.FOLLOW_START: |
|
||||||
window.top.postMessage(JSON.stringify({ |
|
||||||
type: messages.FOLLOW_START, |
|
||||||
newTab: operation.newTab, |
|
||||||
background: 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: |
|
||||||
urls.yank(window); |
|
||||||
consoleFrames.postInfo('Yanked ' + window.location.href); |
|
||||||
break; |
|
||||||
case operations.URLS_PASTE: |
|
||||||
urls.paste( |
|
||||||
window, operation.newTab ? operation.newTab : false, settings.search |
|
||||||
); |
|
||||||
break; |
|
||||||
default: |
|
||||||
browser.runtime.sendMessage({ |
|
||||||
type: messages.BACKGROUND_OPERATION, |
|
||||||
operation, |
|
||||||
}); |
|
||||||
} |
|
||||||
return { type: actions.NOOP }; |
|
||||||
}; |
|
||||||
|
|
||||||
export { exec }; |
|
@ -1,28 +0,0 @@ |
|||||||
import * as actions from './index'; |
|
||||||
import * as operations from '../../shared/operations'; |
|
||||||
import * as messages from '../../shared/messages'; |
|
||||||
import Settings, { Keymaps } from '../../shared/Settings'; |
|
||||||
|
|
||||||
const reservedKeymaps: Keymaps = { |
|
||||||
'<Esc>': { type: operations.CANCEL }, |
|
||||||
'<C-[>': { type: operations.CANCEL }, |
|
||||||
}; |
|
||||||
|
|
||||||
const set = (settings: Settings): actions.SettingAction => { |
|
||||||
return { |
|
||||||
type: actions.SETTING_SET, |
|
||||||
settings: { |
|
||||||
...settings, |
|
||||||
keymaps: { ...settings.keymaps, ...reservedKeymaps }, |
|
||||||
} |
|
||||||
}; |
|
||||||
}; |
|
||||||
|
|
||||||
const load = async(): Promise<actions.SettingAction> => { |
|
||||||
let settings = await browser.runtime.sendMessage({ |
|
||||||
type: messages.SETTINGS_QUERY, |
|
||||||
}); |
|
||||||
return set(settings); |
|
||||||
}; |
|
||||||
|
|
||||||
export { set, load }; |
|
@ -0,0 +1,16 @@ |
|||||||
|
import * as messages from '../../shared/messages'; |
||||||
|
|
||||||
|
export default interface AddonIndicatorClient { |
||||||
|
setEnabled(enabled: boolean): Promise<void>; |
||||||
|
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
} |
||||||
|
|
||||||
|
export class AddonIndicatorClientImpl implements AddonIndicatorClient { |
||||||
|
setEnabled(enabled: boolean): Promise<void> { |
||||||
|
return browser.runtime.sendMessage({ |
||||||
|
type: messages.ADDON_ENABLED_RESPONSE, |
||||||
|
enabled, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
import * as operations from '../../shared/operations'; |
||||||
|
import * as messages from '../../shared/messages'; |
||||||
|
|
||||||
|
export default class BackgroundClient { |
||||||
|
execBackgroundOp(op: operations.Operation): Promise<void> { |
||||||
|
return browser.runtime.sendMessage({ |
||||||
|
type: messages.BACKGROUND_OPERATION, |
||||||
|
operation: op, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
import * as messages from '../../shared/messages'; |
||||||
|
|
||||||
|
export default interface ConsoleClient { |
||||||
|
info(text: string): Promise<void>; |
||||||
|
error(text: string): Promise<void>; |
||||||
|
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
} |
||||||
|
|
||||||
|
export class ConsoleClientImpl implements ConsoleClient { |
||||||
|
async info(text: string): Promise<void> { |
||||||
|
await browser.runtime.sendMessage({ |
||||||
|
type: messages.CONSOLE_FRAME_MESSAGE, |
||||||
|
message: { |
||||||
|
type: messages.CONSOLE_SHOW_INFO, |
||||||
|
text, |
||||||
|
}, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
async error(text: string): Promise<void> { |
||||||
|
await browser.runtime.sendMessage({ |
||||||
|
type: messages.CONSOLE_FRAME_MESSAGE, |
||||||
|
message: { |
||||||
|
type: messages.CONSOLE_SHOW_ERROR, |
||||||
|
text, |
||||||
|
}, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,25 @@ |
|||||||
|
import * as messages from '../../shared/messages'; |
||||||
|
|
||||||
|
export default interface FindClient { |
||||||
|
getGlobalLastKeyword(): Promise<string | null>; |
||||||
|
|
||||||
|
setGlobalLastKeyword(keyword: string): Promise<void>; |
||||||
|
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
} |
||||||
|
|
||||||
|
export class FindClientImpl implements FindClient { |
||||||
|
async getGlobalLastKeyword(): Promise<string | null> { |
||||||
|
let keyword = await browser.runtime.sendMessage({ |
||||||
|
type: messages.FIND_GET_KEYWORD, |
||||||
|
}); |
||||||
|
return keyword as string; |
||||||
|
} |
||||||
|
|
||||||
|
async setGlobalLastKeyword(keyword: string): Promise<void> { |
||||||
|
await browser.runtime.sendMessage({ |
||||||
|
type: messages.FIND_SET_KEYWORD, |
||||||
|
keyword: keyword, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
import * as messages from '../../shared/messages'; |
||||||
|
|
||||||
|
export default interface FindMasterClient { |
||||||
|
findNext(): void; |
||||||
|
|
||||||
|
findPrev(): void; |
||||||
|
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
} |
||||||
|
|
||||||
|
export class FindMasterClientImpl implements FindMasterClient { |
||||||
|
findNext(): void { |
||||||
|
window.top.postMessage(JSON.stringify({ |
||||||
|
type: messages.FIND_NEXT, |
||||||
|
}), '*'); |
||||||
|
} |
||||||
|
|
||||||
|
findPrev(): void { |
||||||
|
window.top.postMessage(JSON.stringify({ |
||||||
|
type: messages.FIND_PREV, |
||||||
|
}), '*'); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,47 @@ |
|||||||
|
import * as messages from '../../shared/messages'; |
||||||
|
import Key from '../domains/Key'; |
||||||
|
|
||||||
|
export default interface FollowMasterClient { |
||||||
|
startFollow(newTab: boolean, background: boolean): void; |
||||||
|
|
||||||
|
responseHintCount(count: number): void; |
||||||
|
|
||||||
|
sendKey(key: Key): void; |
||||||
|
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
} |
||||||
|
|
||||||
|
export class FollowMasterClientImpl implements FollowMasterClient { |
||||||
|
private window: Window; |
||||||
|
|
||||||
|
constructor(window: Window) { |
||||||
|
this.window = window; |
||||||
|
} |
||||||
|
|
||||||
|
startFollow(newTab: boolean, background: boolean): void { |
||||||
|
this.postMessage({ |
||||||
|
type: messages.FOLLOW_START, |
||||||
|
newTab, |
||||||
|
background, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
responseHintCount(count: number): void { |
||||||
|
this.postMessage({ |
||||||
|
type: messages.FOLLOW_RESPONSE_COUNT_TARGETS, |
||||||
|
count, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
sendKey(key: Key): void { |
||||||
|
this.postMessage({ |
||||||
|
type: messages.FOLLOW_KEY_PRESS, |
||||||
|
key: key.key, |
||||||
|
ctrlKey: key.ctrlKey || false, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private postMessage(msg: messages.Message): void { |
||||||
|
this.window.postMessage(JSON.stringify(msg), '*'); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,76 @@ |
|||||||
|
import * as messages from '../../shared/messages'; |
||||||
|
|
||||||
|
interface Size { |
||||||
|
width: number; |
||||||
|
height: number; |
||||||
|
} |
||||||
|
|
||||||
|
interface Point { |
||||||
|
x: number; |
||||||
|
y: number; |
||||||
|
} |
||||||
|
|
||||||
|
export default interface FollowSlaveClient { |
||||||
|
filterHints(prefix: string): void; |
||||||
|
|
||||||
|
requestHintCount(viewSize: Size, framePosition: Point): void; |
||||||
|
|
||||||
|
createHints(viewSize: Size, framePosition: Point, tags: string[]): void; |
||||||
|
|
||||||
|
clearHints(): void; |
||||||
|
|
||||||
|
activateIfExists(tag: string, newTab: boolean, background: boolean): void; |
||||||
|
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
} |
||||||
|
|
||||||
|
export class FollowSlaveClientImpl implements FollowSlaveClient { |
||||||
|
private target: Window; |
||||||
|
|
||||||
|
constructor(target: Window) { |
||||||
|
this.target = target; |
||||||
|
} |
||||||
|
|
||||||
|
filterHints(prefix: string): void { |
||||||
|
this.postMessage({ |
||||||
|
type: messages.FOLLOW_SHOW_HINTS, |
||||||
|
prefix, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
requestHintCount(viewSize: Size, framePosition: Point): void { |
||||||
|
this.postMessage({ |
||||||
|
type: messages.FOLLOW_REQUEST_COUNT_TARGETS, |
||||||
|
viewSize, |
||||||
|
framePosition, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
createHints(viewSize: Size, framePosition: Point, tags: string[]): void { |
||||||
|
this.postMessage({ |
||||||
|
type: messages.FOLLOW_CREATE_HINTS, |
||||||
|
viewSize, |
||||||
|
framePosition, |
||||||
|
tags, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
clearHints(): void { |
||||||
|
this.postMessage({ |
||||||
|
type: messages.FOLLOW_REMOVE_HINTS, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
activateIfExists(tag: string, newTab: boolean, background: boolean): void { |
||||||
|
this.postMessage({ |
||||||
|
type: messages.FOLLOW_ACTIVATE, |
||||||
|
tag, |
||||||
|
newTab, |
||||||
|
background, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private postMessage(msg: messages.Message): void { |
||||||
|
this.target.postMessage(JSON.stringify(msg), '*'); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,28 @@ |
|||||||
|
import Mark from '../domains/Mark'; |
||||||
|
import * as messages from '../../shared/messages'; |
||||||
|
|
||||||
|
export default interface MarkClient { |
||||||
|
setGloablMark(key: string, mark: Mark): Promise<void>; |
||||||
|
|
||||||
|
jumpGlobalMark(key: string): Promise<void>; |
||||||
|
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
} |
||||||
|
|
||||||
|
export class MarkClientImpl implements MarkClient { |
||||||
|
async setGloablMark(key: string, mark: Mark): Promise<void> { |
||||||
|
await browser.runtime.sendMessage({ |
||||||
|
type: messages.MARK_SET_GLOBAL, |
||||||
|
key, |
||||||
|
x: mark.x, |
||||||
|
y: mark.y, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
async jumpGlobalMark(key: string): Promise<void> { |
||||||
|
await browser.runtime.sendMessage({ |
||||||
|
type: messages.MARK_JUMP_GLOBAL, |
||||||
|
key, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
import Settings from '../../shared/Settings'; |
||||||
|
import * as messages from '../../shared/messages'; |
||||||
|
|
||||||
|
export default interface SettingClient { |
||||||
|
load(): Promise<Settings>; |
||||||
|
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
} |
||||||
|
|
||||||
|
export class SettingClientImpl { |
||||||
|
async load(): Promise<Settings> { |
||||||
|
let settings = await browser.runtime.sendMessage({ |
||||||
|
type: messages.SETTINGS_QUERY, |
||||||
|
}); |
||||||
|
return settings as Settings; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
import * as messages from '../../shared/messages'; |
||||||
|
|
||||||
|
export default interface TabsClient { |
||||||
|
openUrl(url: string, newTab: boolean, background?: boolean): Promise<void>; |
||||||
|
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
} |
||||||
|
|
||||||
|
export class TabsClientImpl implements TabsClient { |
||||||
|
async openUrl( |
||||||
|
url: string, |
||||||
|
newTab: boolean, |
||||||
|
background?: boolean, |
||||||
|
): Promise<void> { |
||||||
|
await browser.runtime.sendMessage({ |
||||||
|
type: messages.OPEN_URL, |
||||||
|
url, |
||||||
|
newTab, |
||||||
|
background, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -1,231 +0,0 @@ |
|||||||
import MessageListener from '../../MessageListener'; |
|
||||||
import Hint from './hint'; |
|
||||||
import * as dom from '../../../shared/utils/dom'; |
|
||||||
import * as messages from '../../../shared/messages'; |
|
||||||
import * as keyUtils from '../../../shared/utils/keys'; |
|
||||||
|
|
||||||
const TARGET_SELECTOR = [ |
|
||||||
'a', 'button', 'input', 'textarea', 'area', |
|
||||||
'[contenteditable=true]', '[contenteditable=""]', '[tabindex]', |
|
||||||
'[role="button"]', 'summary' |
|
||||||
].join(','); |
|
||||||
|
|
||||||
interface Size { |
|
||||||
width: number; |
|
||||||
height: number; |
|
||||||
} |
|
||||||
|
|
||||||
interface Point { |
|
||||||
x: number; |
|
||||||
y: number; |
|
||||||
} |
|
||||||
|
|
||||||
const inViewport = ( |
|
||||||
win: Window, |
|
||||||
element: Element, |
|
||||||
viewSize: Size, |
|
||||||
framePosition: Point, |
|
||||||
): boolean => { |
|
||||||
let { |
|
||||||
top, left, bottom, right |
|
||||||
} = dom.viewportRect(element); |
|
||||||
let doc = win.document; |
|
||||||
let frameWidth = doc.documentElement.clientWidth; |
|
||||||
let frameHeight = doc.documentElement.clientHeight; |
|
||||||
|
|
||||||
if (right < 0 || bottom < 0 || top > frameHeight || left > frameWidth) { |
|
||||||
// out of frame
|
|
||||||
return false; |
|
||||||
} |
|
||||||
if (right + framePosition.x < 0 || bottom + framePosition.y < 0 || |
|
||||||
left + framePosition.x > viewSize.width || |
|
||||||
top + framePosition.y > viewSize.height) { |
|
||||||
// out of viewport
|
|
||||||
return false; |
|
||||||
} |
|
||||||
return true; |
|
||||||
}; |
|
||||||
|
|
||||||
const isAriaHiddenOrAriaDisabled = (win: Window, element: Element): boolean => { |
|
||||||
if (!element || win.document.documentElement === element) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
for (let attr of ['aria-hidden', 'aria-disabled']) { |
|
||||||
let value = element.getAttribute(attr); |
|
||||||
if (value !== null) { |
|
||||||
let hidden = value.toLowerCase(); |
|
||||||
if (hidden === '' || hidden === 'true') { |
|
||||||
return true; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
return isAriaHiddenOrAriaDisabled(win, element.parentElement as Element); |
|
||||||
}; |
|
||||||
|
|
||||||
export default class Follow { |
|
||||||
private win: Window; |
|
||||||
|
|
||||||
private newTab: boolean; |
|
||||||
|
|
||||||
private background: boolean; |
|
||||||
|
|
||||||
private hints: {[key: string]: Hint }; |
|
||||||
|
|
||||||
private targets: HTMLElement[] = []; |
|
||||||
|
|
||||||
constructor(win: Window) { |
|
||||||
this.win = win; |
|
||||||
this.newTab = false; |
|
||||||
this.background = false; |
|
||||||
this.hints = {}; |
|
||||||
this.targets = []; |
|
||||||
|
|
||||||
new MessageListener().onWebMessage(this.onMessage.bind(this)); |
|
||||||
} |
|
||||||
|
|
||||||
key(key: keyUtils.Key): boolean { |
|
||||||
if (Object.keys(this.hints).length === 0) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
this.win.parent.postMessage(JSON.stringify({ |
|
||||||
type: messages.FOLLOW_KEY_PRESS, |
|
||||||
key: key.key, |
|
||||||
ctrlKey: key.ctrlKey, |
|
||||||
}), '*'); |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
openLink(element: HTMLAreaElement|HTMLAnchorElement) { |
|
||||||
// Browser prevent new tab by link with target='_blank'
|
|
||||||
if (!this.newTab && element.getAttribute('target') !== '_blank') { |
|
||||||
element.click(); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
let href = element.getAttribute('href'); |
|
||||||
|
|
||||||
// eslint-disable-next-line no-script-url
|
|
||||||
if (!href || href === '#' || href.toLowerCase().startsWith('javascript:')) { |
|
||||||
return; |
|
||||||
} |
|
||||||
return browser.runtime.sendMessage({ |
|
||||||
type: messages.OPEN_URL, |
|
||||||
url: element.href, |
|
||||||
newTab: true, |
|
||||||
background: this.background, |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
countHints(sender: any, viewSize: Size, framePosition: Point) { |
|
||||||
this.targets = Follow.getTargetElements(this.win, viewSize, framePosition); |
|
||||||
sender.postMessage(JSON.stringify({ |
|
||||||
type: messages.FOLLOW_RESPONSE_COUNT_TARGETS, |
|
||||||
count: this.targets.length, |
|
||||||
}), '*'); |
|
||||||
} |
|
||||||
|
|
||||||
createHints(keysArray: string[], newTab: boolean, background: boolean) { |
|
||||||
if (keysArray.length !== this.targets.length) { |
|
||||||
throw new Error('illegal hint count'); |
|
||||||
} |
|
||||||
|
|
||||||
this.newTab = newTab; |
|
||||||
this.background = background; |
|
||||||
this.hints = {}; |
|
||||||
for (let i = 0; i < keysArray.length; ++i) { |
|
||||||
let keys = keysArray[i]; |
|
||||||
let hint = new Hint(this.targets[i], keys); |
|
||||||
this.hints[keys] = hint; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
showHints(keys: string) { |
|
||||||
Object.keys(this.hints).filter(key => key.startsWith(keys)) |
|
||||||
.forEach(key => this.hints[key].show()); |
|
||||||
Object.keys(this.hints).filter(key => !key.startsWith(keys)) |
|
||||||
.forEach(key => this.hints[key].hide()); |
|
||||||
} |
|
||||||
|
|
||||||
removeHints() { |
|
||||||
Object.keys(this.hints).forEach((key) => { |
|
||||||
this.hints[key].remove(); |
|
||||||
}); |
|
||||||
this.hints = {}; |
|
||||||
this.targets = []; |
|
||||||
} |
|
||||||
|
|
||||||
activateHints(keys: string) { |
|
||||||
let hint = this.hints[keys]; |
|
||||||
if (!hint) { |
|
||||||
return; |
|
||||||
} |
|
||||||
let element = hint.getTarget(); |
|
||||||
switch (element.tagName.toLowerCase()) { |
|
||||||
case 'a': |
|
||||||
return this.openLink(element as HTMLAnchorElement); |
|
||||||
case 'area': |
|
||||||
return this.openLink(element as HTMLAreaElement); |
|
||||||
case 'input': |
|
||||||
switch ((element as HTMLInputElement).type) { |
|
||||||
case 'file': |
|
||||||
case 'checkbox': |
|
||||||
case 'radio': |
|
||||||
case 'submit': |
|
||||||
case 'reset': |
|
||||||
case 'button': |
|
||||||
case 'image': |
|
||||||
case 'color': |
|
||||||
return element.click(); |
|
||||||
default: |
|
||||||
return element.focus(); |
|
||||||
} |
|
||||||
case 'textarea': |
|
||||||
return element.focus(); |
|
||||||
case 'button': |
|
||||||
case 'summary': |
|
||||||
return element.click(); |
|
||||||
default: |
|
||||||
if (dom.isContentEditable(element)) { |
|
||||||
return element.focus(); |
|
||||||
} else if (element.hasAttribute('tabindex')) { |
|
||||||
return element.click(); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
onMessage(message: messages.Message, sender: any) { |
|
||||||
switch (message.type) { |
|
||||||
case messages.FOLLOW_REQUEST_COUNT_TARGETS: |
|
||||||
return this.countHints(sender, message.viewSize, message.framePosition); |
|
||||||
case messages.FOLLOW_CREATE_HINTS: |
|
||||||
return this.createHints( |
|
||||||
message.keysArray, message.newTab, message.background); |
|
||||||
case messages.FOLLOW_SHOW_HINTS: |
|
||||||
return this.showHints(message.keys); |
|
||||||
case messages.FOLLOW_ACTIVATE: |
|
||||||
return this.activateHints(message.keys); |
|
||||||
case messages.FOLLOW_REMOVE_HINTS: |
|
||||||
return this.removeHints(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
static getTargetElements( |
|
||||||
win: Window, |
|
||||||
viewSize: |
|
||||||
Size, framePosition: Point, |
|
||||||
): HTMLElement[] { |
|
||||||
let all = win.document.querySelectorAll(TARGET_SELECTOR); |
|
||||||
let filtered = Array.prototype.filter.call(all, (element: HTMLElement) => { |
|
||||||
let style = win.getComputedStyle(element); |
|
||||||
|
|
||||||
// AREA's 'display' in Browser style is 'none'
|
|
||||||
return (element.tagName === 'AREA' || style.display !== 'none') && |
|
||||||
style.visibility !== 'hidden' && |
|
||||||
(element as HTMLInputElement).type !== 'hidden' && |
|
||||||
element.offsetHeight > 0 && |
|
||||||
!isAriaHiddenOrAriaDisabled(win, element) && |
|
||||||
inViewport(win, element, viewSize, framePosition); |
|
||||||
}); |
|
||||||
return filtered; |
|
||||||
} |
|
||||||
} |
|
@ -1,62 +0,0 @@ |
|||||||
import * as dom from '../../../shared/utils/dom'; |
|
||||||
|
|
||||||
interface Point { |
|
||||||
x: number; |
|
||||||
y: number; |
|
||||||
} |
|
||||||
|
|
||||||
const hintPosition = (element: Element): Point => { |
|
||||||
let { left, top, right, bottom } = dom.viewportRect(element); |
|
||||||
|
|
||||||
if (element.tagName !== 'AREA') { |
|
||||||
return { x: left, y: top }; |
|
||||||
} |
|
||||||
|
|
||||||
return { |
|
||||||
x: (left + right) / 2, |
|
||||||
y: (top + bottom) / 2, |
|
||||||
}; |
|
||||||
}; |
|
||||||
|
|
||||||
export default class Hint { |
|
||||||
private target: HTMLElement; |
|
||||||
|
|
||||||
private element: HTMLElement; |
|
||||||
|
|
||||||
constructor(target: HTMLElement, tag: string) { |
|
||||||
let doc = target.ownerDocument; |
|
||||||
if (doc === null) { |
|
||||||
throw new TypeError('ownerDocument is null'); |
|
||||||
} |
|
||||||
|
|
||||||
let { x, y } = hintPosition(target); |
|
||||||
let { scrollX, scrollY } = window; |
|
||||||
|
|
||||||
this.target = target; |
|
||||||
|
|
||||||
this.element = doc.createElement('span'); |
|
||||||
this.element.className = 'vimvixen-hint'; |
|
||||||
this.element.textContent = tag; |
|
||||||
this.element.style.left = x + scrollX + 'px'; |
|
||||||
this.element.style.top = y + scrollY + 'px'; |
|
||||||
|
|
||||||
this.show(); |
|
||||||
doc.body.append(this.element); |
|
||||||
} |
|
||||||
|
|
||||||
show(): void { |
|
||||||
this.element.style.display = 'inline'; |
|
||||||
} |
|
||||||
|
|
||||||
hide(): void { |
|
||||||
this.element.style.display = 'none'; |
|
||||||
} |
|
||||||
|
|
||||||
remove(): void { |
|
||||||
this.element.remove(); |
|
||||||
} |
|
||||||
|
|
||||||
getTarget(): HTMLElement { |
|
||||||
return this.target; |
|
||||||
} |
|
||||||
} |
|
@ -1,61 +0,0 @@ |
|||||||
import InputComponent from './input'; |
|
||||||
import FollowComponent from './follow'; |
|
||||||
import MarkComponent from './mark'; |
|
||||||
import KeymapperComponent from './keymapper'; |
|
||||||
import * as settingActions from '../../actions/setting'; |
|
||||||
import * as messages from '../../../shared/messages'; |
|
||||||
import MessageListener from '../../MessageListener'; |
|
||||||
import * as addonActions from '../../actions/addon'; |
|
||||||
import * as blacklists from '../../../shared/blacklists'; |
|
||||||
import * as keys from '../../../shared/utils/keys'; |
|
||||||
import * as actions from '../../actions'; |
|
||||||
|
|
||||||
export default class Common { |
|
||||||
private win: Window; |
|
||||||
|
|
||||||
private store: any; |
|
||||||
|
|
||||||
constructor(win: Window, store: any) { |
|
||||||
const input = new InputComponent(win.document.body); |
|
||||||
const follow = new FollowComponent(win); |
|
||||||
const mark = new MarkComponent(store); |
|
||||||
const keymapper = new KeymapperComponent(store); |
|
||||||
|
|
||||||
input.onKey((key: keys.Key) => follow.key(key)); |
|
||||||
input.onKey((key: keys.Key) => mark.key(key)); |
|
||||||
input.onKey((key: keys.Key) => keymapper.key(key)); |
|
||||||
|
|
||||||
this.win = win; |
|
||||||
this.store = store; |
|
||||||
|
|
||||||
this.reloadSettings(); |
|
||||||
|
|
||||||
new MessageListener().onBackgroundMessage(this.onMessage.bind(this)); |
|
||||||
} |
|
||||||
|
|
||||||
onMessage(message: messages.Message) { |
|
||||||
let { enabled } = this.store.getState().addon; |
|
||||||
switch (message.type) { |
|
||||||
case messages.SETTINGS_CHANGED: |
|
||||||
return this.reloadSettings(); |
|
||||||
case messages.ADDON_TOGGLE_ENABLED: |
|
||||||
this.store.dispatch(addonActions.setEnabled(!enabled)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
reloadSettings() { |
|
||||||
try { |
|
||||||
this.store.dispatch(settingActions.load()) |
|
||||||
.then((action: actions.SettingAction) => { |
|
||||||
let enabled = !blacklists.includes( |
|
||||||
action.settings.blacklist, this.win.location.href |
|
||||||
); |
|
||||||
this.store.dispatch(addonActions.setEnabled(enabled)); |
|
||||||
}); |
|
||||||
} catch (e) { |
|
||||||
// Sometime sendMessage fails when background script is not ready.
|
|
||||||
console.warn(e); |
|
||||||
setTimeout(() => this.reloadSettings(), 500); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,68 +0,0 @@ |
|||||||
import * as inputActions from '../../actions/input'; |
|
||||||
import * as operationActions from '../../actions/operation'; |
|
||||||
import * as operations from '../../../shared/operations'; |
|
||||||
import * as keyUtils from '../../../shared/utils/keys'; |
|
||||||
|
|
||||||
const mapStartsWith = ( |
|
||||||
mapping: keyUtils.Key[], |
|
||||||
keys: keyUtils.Key[], |
|
||||||
): boolean => { |
|
||||||
if (mapping.length < keys.length) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
for (let i = 0; i < keys.length; ++i) { |
|
||||||
if (!keyUtils.equals(mapping[i], keys[i])) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
} |
|
||||||
return true; |
|
||||||
}; |
|
||||||
|
|
||||||
export default class KeymapperComponent { |
|
||||||
private store: any; |
|
||||||
|
|
||||||
constructor(store: any) { |
|
||||||
this.store = store; |
|
||||||
} |
|
||||||
|
|
||||||
// eslint-disable-next-line max-statements
|
|
||||||
key(key: keyUtils.Key): boolean { |
|
||||||
this.store.dispatch(inputActions.keyPress(key)); |
|
||||||
|
|
||||||
let state = this.store.getState(); |
|
||||||
let input = state.input; |
|
||||||
let keymaps = new Map<keyUtils.Key[], operations.Operation>( |
|
||||||
state.setting.keymaps.map( |
|
||||||
(e: {key: keyUtils.Key[], op: operations.Operation}) => [e.key, e.op], |
|
||||||
) |
|
||||||
); |
|
||||||
|
|
||||||
let matched = Array.from(keymaps.keys()).filter( |
|
||||||
(mapping: keyUtils.Key[]) => { |
|
||||||
return mapStartsWith(mapping, input.keys); |
|
||||||
}); |
|
||||||
if (!state.addon.enabled) { |
|
||||||
// available keymaps are only ADDON_ENABLE and ADDON_TOGGLE_ENABLED if
|
|
||||||
// the addon disabled
|
|
||||||
matched = matched.filter((keys) => { |
|
||||||
let type = (keymaps.get(keys) as operations.Operation).type; |
|
||||||
return type === operations.ADDON_ENABLE || |
|
||||||
type === operations.ADDON_TOGGLE_ENABLED; |
|
||||||
}); |
|
||||||
} |
|
||||||
if (matched.length === 0) { |
|
||||||
this.store.dispatch(inputActions.clearKeys()); |
|
||||||
return false; |
|
||||||
} else if (matched.length > 1 || |
|
||||||
matched.length === 1 && input.keys.length < matched[0].length) { |
|
||||||
return true; |
|
||||||
} |
|
||||||
let operation = keymaps.get(matched[0]) as operations.Operation; |
|
||||||
let act = operationActions.exec( |
|
||||||
operation, state.setting, state.addon.enabled |
|
||||||
); |
|
||||||
this.store.dispatch(act); |
|
||||||
this.store.dispatch(inputActions.clearKeys()); |
|
||||||
return true; |
|
||||||
} |
|
||||||
} |
|
@ -1,79 +0,0 @@ |
|||||||
import * as markActions from '../../actions/mark'; |
|
||||||
import * as scrolls from '../..//scrolls'; |
|
||||||
import * as consoleFrames from '../..//console-frames'; |
|
||||||
import * as keyUtils from '../../../shared/utils/keys'; |
|
||||||
import Mark from '../../Mark'; |
|
||||||
|
|
||||||
const cancelKey = (key: keyUtils.Key): boolean => { |
|
||||||
return key.key === 'Esc' || key.key === '[' && Boolean(key.ctrlKey); |
|
||||||
}; |
|
||||||
|
|
||||||
const globalKey = (key: string): boolean => { |
|
||||||
return (/^[A-Z0-9]$/).test(key); |
|
||||||
}; |
|
||||||
|
|
||||||
export default class MarkComponent { |
|
||||||
private store: any; |
|
||||||
|
|
||||||
constructor(store: any) { |
|
||||||
this.store = store; |
|
||||||
} |
|
||||||
|
|
||||||
// eslint-disable-next-line max-statements
|
|
||||||
key(key: keyUtils.Key) { |
|
||||||
let { mark: markState, setting } = this.store.getState(); |
|
||||||
let smoothscroll = setting.properties.smoothscroll; |
|
||||||
|
|
||||||
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 (globalKey(key.key) && markState.setMode) { |
|
||||||
this.doSetGlobal(key); |
|
||||||
} else if (globalKey(key.key) && markState.jumpMode) { |
|
||||||
this.doJumpGlobal(key); |
|
||||||
} else if (markState.setMode) { |
|
||||||
this.doSet(key); |
|
||||||
} else if (markState.jumpMode) { |
|
||||||
this.doJump(markState.marks, key, smoothscroll); |
|
||||||
} |
|
||||||
|
|
||||||
this.store.dispatch(markActions.cancel()); |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
doSet(key: keyUtils.Key) { |
|
||||||
let { x, y } = scrolls.getScroll(); |
|
||||||
this.store.dispatch(markActions.setLocal(key.key, x, y)); |
|
||||||
} |
|
||||||
|
|
||||||
doJump( |
|
||||||
marks: { [key: string]: Mark }, |
|
||||||
key: keyUtils.Key, |
|
||||||
smoothscroll: boolean, |
|
||||||
) { |
|
||||||
if (!marks[key.key]) { |
|
||||||
consoleFrames.postError('Mark is not set'); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
let { x, y } = marks[key.key]; |
|
||||||
scrolls.scrollTo(x, y, smoothscroll); |
|
||||||
} |
|
||||||
|
|
||||||
doSetGlobal(key: keyUtils.Key) { |
|
||||||
let { x, y } = scrolls.getScroll(); |
|
||||||
this.store.dispatch(markActions.setGlobal(key.key, x, y)); |
|
||||||
} |
|
||||||
|
|
||||||
doJumpGlobal(key: keyUtils.Key) { |
|
||||||
this.store.dispatch(markActions.jumpGlobal(key.key)); |
|
||||||
} |
|
||||||
} |
|
@ -1,3 +0,0 @@ |
|||||||
import CommonComponent from './common'; |
|
||||||
|
|
||||||
export default CommonComponent; |
|
@ -1,46 +0,0 @@ |
|||||||
import * as findActions from '../../actions/find'; |
|
||||||
import * as messages from '../../../shared/messages'; |
|
||||||
import MessageListener from '../../MessageListener'; |
|
||||||
|
|
||||||
export default class FindComponent { |
|
||||||
private store: any; |
|
||||||
|
|
||||||
constructor(store: any) { |
|
||||||
this.store = store; |
|
||||||
|
|
||||||
new MessageListener().onWebMessage(this.onMessage.bind(this)); |
|
||||||
} |
|
||||||
|
|
||||||
onMessage(message: messages.Message) { |
|
||||||
switch (message.type) { |
|
||||||
case messages.CONSOLE_ENTER_FIND: |
|
||||||
return this.start(message.text); |
|
||||||
case messages.FIND_NEXT: |
|
||||||
return this.next(); |
|
||||||
case messages.FIND_PREV: |
|
||||||
return this.prev(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
start(text: string) { |
|
||||||
let state = this.store.getState().find; |
|
||||||
|
|
||||||
if (text.length === 0) { |
|
||||||
return this.store.dispatch( |
|
||||||
findActions.next(state.keyword as string, true)); |
|
||||||
} |
|
||||||
return this.store.dispatch(findActions.next(text, true)); |
|
||||||
} |
|
||||||
|
|
||||||
next() { |
|
||||||
let state = this.store.getState().find; |
|
||||||
return this.store.dispatch( |
|
||||||
findActions.next(state.keyword as string, false)); |
|
||||||
} |
|
||||||
|
|
||||||
prev() { |
|
||||||
let state = this.store.getState().find; |
|
||||||
return this.store.dispatch( |
|
||||||
findActions.prev(state.keyword as string, false)); |
|
||||||
} |
|
||||||
} |
|
@ -1,166 +0,0 @@ |
|||||||
import * as followControllerActions from '../../actions/follow-controller'; |
|
||||||
import * as messages from '../../../shared/messages'; |
|
||||||
import MessageListener, { WebMessageSender } from '../../MessageListener'; |
|
||||||
import HintKeyProducer from '../../hint-key-producer'; |
|
||||||
|
|
||||||
const broadcastMessage = (win: Window, message: messages.Message): void => { |
|
||||||
let json = JSON.stringify(message); |
|
||||||
let frames = [win.self].concat(Array.from(win.frames as any)); |
|
||||||
frames.forEach(frame => frame.postMessage(json, '*')); |
|
||||||
}; |
|
||||||
|
|
||||||
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: WebMessageSender) { |
|
||||||
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()); |
|
||||||
} |
|
||||||
|
|
||||||
broadcastMessage(this.win, { |
|
||||||
type: messages.FOLLOW_SHOW_HINTS, |
|
||||||
keys: this.state.keys as string, |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
activate(): void { |
|
||||||
broadcastMessage(this.win, { |
|
||||||
type: messages.FOLLOW_ACTIVATE, |
|
||||||
keys: this.state.keys as string, |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
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('frame,iframe'); |
|
||||||
|
|
||||||
this.win.postMessage(JSON.stringify({ |
|
||||||
type: messages.FOLLOW_REQUEST_COUNT_TARGETS, |
|
||||||
viewSize: { width: viewWidth, height: viewHeight }, |
|
||||||
framePosition: { x: 0, y: 0 }, |
|
||||||
}), '*'); |
|
||||||
frameElements.forEach((ele) => { |
|
||||||
let { left: frameX, top: frameY } = ele.getBoundingClientRect(); |
|
||||||
let message = JSON.stringify({ |
|
||||||
type: messages.FOLLOW_REQUEST_COUNT_TARGETS, |
|
||||||
viewSize: { width: viewWidth, height: viewHeight }, |
|
||||||
framePosition: { x: frameX, y: frameY }, |
|
||||||
}); |
|
||||||
if (ele instanceof HTMLFrameElement && ele.contentWindow || |
|
||||||
ele instanceof HTMLIFrameElement && ele.contentWindow) { |
|
||||||
ele.contentWindow.postMessage(message, '*'); |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
create(count: number, sender: WebMessageSender) { |
|
||||||
let produced = []; |
|
||||||
for (let i = 0; i < count; ++i) { |
|
||||||
produced.push((this.producer as HintKeyProducer).produce()); |
|
||||||
} |
|
||||||
this.keys = this.keys.concat(produced); |
|
||||||
|
|
||||||
(sender as Window).postMessage(JSON.stringify({ |
|
||||||
type: messages.FOLLOW_CREATE_HINTS, |
|
||||||
keysArray: produced, |
|
||||||
newTab: this.state.newTab, |
|
||||||
background: this.state.background, |
|
||||||
}), '*'); |
|
||||||
} |
|
||||||
|
|
||||||
remove() { |
|
||||||
this.keys = []; |
|
||||||
broadcastMessage(this.win, { |
|
||||||
type: messages.FOLLOW_REMOVE_HINTS, |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
hintchars() { |
|
||||||
return this.store.getState().setting.properties.hintchars; |
|
||||||
} |
|
||||||
} |
|
@ -1,51 +0,0 @@ |
|||||||
import CommonComponent from '../common'; |
|
||||||
import FollowController from './follow-controller'; |
|
||||||
import FindComponent from './find'; |
|
||||||
import * as consoleFrames from '../../console-frames'; |
|
||||||
import * as messages from '../../../shared/messages'; |
|
||||||
import MessageListener from '../../MessageListener'; |
|
||||||
import * as scrolls from '../../scrolls'; |
|
||||||
|
|
||||||
export default class TopContent { |
|
||||||
private win: Window; |
|
||||||
|
|
||||||
private store: any; |
|
||||||
|
|
||||||
constructor(win: Window, store: any) { |
|
||||||
this.win = win; |
|
||||||
this.store = store; |
|
||||||
|
|
||||||
new CommonComponent(win, store); // eslint-disable-line no-new
|
|
||||||
new FollowController(win, store); // eslint-disable-line no-new
|
|
||||||
new FindComponent(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 addonState = this.store.getState().addon; |
|
||||||
|
|
||||||
switch (message.type) { |
|
||||||
case messages.ADDON_ENABLED_QUERY: |
|
||||||
return Promise.resolve({ |
|
||||||
type: messages.ADDON_ENABLED_RESPONSE, |
|
||||||
enabled: addonState.enabled, |
|
||||||
}); |
|
||||||
case messages.TAB_SCROLL_TO: |
|
||||||
return scrolls.scrollTo(message.x, message.y, false); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,38 +0,0 @@ |
|||||||
import * as messages from '../shared/messages'; |
|
||||||
|
|
||||||
const initialize = (doc: Document): HTMLIFrameElement => { |
|
||||||
let iframe = doc.createElement('iframe'); |
|
||||||
iframe.src = browser.runtime.getURL('build/console.html'); |
|
||||||
iframe.id = 'vimvixen-console-frame'; |
|
||||||
iframe.className = 'vimvixen-console-frame'; |
|
||||||
doc.body.append(iframe); |
|
||||||
|
|
||||||
return iframe; |
|
||||||
}; |
|
||||||
|
|
||||||
const blur = (doc: Document) => { |
|
||||||
let ele = doc.getElementById('vimvixen-console-frame') as HTMLIFrameElement; |
|
||||||
ele.blur(); |
|
||||||
}; |
|
||||||
|
|
||||||
const postError = (text: string): Promise<any> => { |
|
||||||
return browser.runtime.sendMessage({ |
|
||||||
type: messages.CONSOLE_FRAME_MESSAGE, |
|
||||||
message: { |
|
||||||
type: messages.CONSOLE_SHOW_ERROR, |
|
||||||
text, |
|
||||||
}, |
|
||||||
}); |
|
||||||
}; |
|
||||||
|
|
||||||
const postInfo = (text: string): Promise<any> => { |
|
||||||
return browser.runtime.sendMessage({ |
|
||||||
type: messages.CONSOLE_FRAME_MESSAGE, |
|
||||||
message: { |
|
||||||
type: messages.CONSOLE_SHOW_INFO, |
|
||||||
text, |
|
||||||
}, |
|
||||||
}); |
|
||||||
}; |
|
||||||
|
|
||||||
export { initialize, blur, postError, postInfo }; |
|
@ -0,0 +1,19 @@ |
|||||||
|
import * as messages from '../../shared/messages'; |
||||||
|
import AddonEnabledUseCase from '../usecases/AddonEnabledUseCase'; |
||||||
|
|
||||||
|
export default class AddonEnabledController { |
||||||
|
private addonEnabledUseCase: AddonEnabledUseCase; |
||||||
|
|
||||||
|
constructor({ |
||||||
|
addonEnabledUseCase = new AddonEnabledUseCase(), |
||||||
|
} = {}) { |
||||||
|
this.addonEnabledUseCase = addonEnabledUseCase; |
||||||
|
} |
||||||
|
|
||||||
|
getAddonEnabled( |
||||||
|
_message: messages.AddonEnabledQueryMessage, |
||||||
|
): Promise<boolean> { |
||||||
|
let enabled = this.addonEnabledUseCase.getEnabled(); |
||||||
|
return Promise.resolve(enabled); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
import ConsoleFrameUseCase from '../usecases/ConsoleFrameUseCase'; |
||||||
|
import * as messages from '../../shared/messages'; |
||||||
|
|
||||||
|
export default class ConsoleFrameController { |
||||||
|
private consoleFrameUseCase: ConsoleFrameUseCase; |
||||||
|
|
||||||
|
constructor({ |
||||||
|
consoleFrameUseCase = new ConsoleFrameUseCase(), |
||||||
|
} = {}) { |
||||||
|
this.consoleFrameUseCase = consoleFrameUseCase; |
||||||
|
} |
||||||
|
|
||||||
|
unfocus(_message: messages.Message) { |
||||||
|
this.consoleFrameUseCase.unfocus(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,24 @@ |
|||||||
|
import * as messages from '../../shared/messages'; |
||||||
|
import FindUseCase from '../usecases/FindUseCase'; |
||||||
|
|
||||||
|
export default class FindController { |
||||||
|
private findUseCase: FindUseCase; |
||||||
|
|
||||||
|
constructor({ |
||||||
|
findUseCase = new FindUseCase(), |
||||||
|
} = {}) { |
||||||
|
this.findUseCase = findUseCase; |
||||||
|
} |
||||||
|
|
||||||
|
async start(m: messages.ConsoleEnterFindMessage): Promise<void> { |
||||||
|
await this.findUseCase.startFind(m.text); |
||||||
|
} |
||||||
|
|
||||||
|
async next(_: messages.FindNextMessage): Promise<void> { |
||||||
|
await this.findUseCase.findNext(); |
||||||
|
} |
||||||
|
|
||||||
|
async prev(_: messages.FindPrevMessage): Promise<void> { |
||||||
|
await this.findUseCase.findPrev(); |
||||||
|
} |
||||||
|
} |
@ -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,148 @@ |
|||||||
|
import * as operations from '../../shared/operations'; |
||||||
|
import KeymapUseCase from '../usecases/KeymapUseCase'; |
||||||
|
import AddonEnabledUseCase from '../usecases/AddonEnabledUseCase'; |
||||||
|
import FindSlaveUseCase from '../usecases/FindSlaveUseCase'; |
||||||
|
import ScrollUseCase from '../usecases/ScrollUseCase'; |
||||||
|
import NavigateUseCase from '../usecases/NavigateUseCase'; |
||||||
|
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 { |
||||||
|
private keymapUseCase: KeymapUseCase; |
||||||
|
|
||||||
|
private addonEnabledUseCase: AddonEnabledUseCase; |
||||||
|
|
||||||
|
private findSlaveUseCase: FindSlaveUseCase; |
||||||
|
|
||||||
|
private scrollUseCase: ScrollUseCase; |
||||||
|
|
||||||
|
private navigateUseCase: NavigateUseCase; |
||||||
|
|
||||||
|
private focusUseCase: FocusUseCase; |
||||||
|
|
||||||
|
private clipbaordUseCase: ClipboardUseCase; |
||||||
|
|
||||||
|
private backgroundClient: BackgroundClient; |
||||||
|
|
||||||
|
private markKeyUseCase: MarkKeyyUseCase; |
||||||
|
|
||||||
|
private followMasterClient: FollowMasterClient; |
||||||
|
|
||||||
|
constructor({ |
||||||
|
keymapUseCase = new KeymapUseCase(), |
||||||
|
addonEnabledUseCase = new AddonEnabledUseCase(), |
||||||
|
findSlaveUseCase = new FindSlaveUseCase(), |
||||||
|
scrollUseCase = new ScrollUseCase(), |
||||||
|
navigateUseCase = new NavigateUseCase(), |
||||||
|
focusUseCase = new FocusUseCase(), |
||||||
|
clipbaordUseCase = new ClipboardUseCase(), |
||||||
|
backgroundClient = new BackgroundClient(), |
||||||
|
markKeyUseCase = new MarkKeyyUseCase(), |
||||||
|
followMasterClient = new FollowMasterClientImpl(window.top), |
||||||
|
} = {}) { |
||||||
|
this.keymapUseCase = keymapUseCase; |
||||||
|
this.addonEnabledUseCase = addonEnabledUseCase; |
||||||
|
this.findSlaveUseCase = findSlaveUseCase; |
||||||
|
this.scrollUseCase = scrollUseCase; |
||||||
|
this.navigateUseCase = navigateUseCase; |
||||||
|
this.focusUseCase = focusUseCase; |
||||||
|
this.clipbaordUseCase = clipbaordUseCase; |
||||||
|
this.backgroundClient = backgroundClient; |
||||||
|
this.markKeyUseCase = markKeyUseCase; |
||||||
|
this.followMasterClient = followMasterClient; |
||||||
|
} |
||||||
|
|
||||||
|
// eslint-disable-next-line complexity, max-lines-per-function
|
||||||
|
press(key: Key): boolean { |
||||||
|
let op = this.keymapUseCase.nextOp(key); |
||||||
|
if (op === null) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
// do not await due to return a boolean immediately
|
||||||
|
switch (op.type) { |
||||||
|
case operations.ADDON_ENABLE: |
||||||
|
this.addonEnabledUseCase.enable(); |
||||||
|
break; |
||||||
|
case operations.ADDON_DISABLE: |
||||||
|
this.addonEnabledUseCase.disable(); |
||||||
|
break; |
||||||
|
case operations.ADDON_TOGGLE_ENABLED: |
||||||
|
this.addonEnabledUseCase.toggle(); |
||||||
|
break; |
||||||
|
case operations.FIND_NEXT: |
||||||
|
this.findSlaveUseCase.findNext(); |
||||||
|
break; |
||||||
|
case operations.FIND_PREV: |
||||||
|
this.findSlaveUseCase.findPrev(); |
||||||
|
break; |
||||||
|
case operations.SCROLL_VERTICALLY: |
||||||
|
this.scrollUseCase.scrollVertically(op.count); |
||||||
|
break; |
||||||
|
case operations.SCROLL_HORIZONALLY: |
||||||
|
this.scrollUseCase.scrollHorizonally(op.count); |
||||||
|
break; |
||||||
|
case operations.SCROLL_PAGES: |
||||||
|
this.scrollUseCase.scrollPages(op.count); |
||||||
|
break; |
||||||
|
case operations.SCROLL_TOP: |
||||||
|
this.scrollUseCase.scrollToTop(); |
||||||
|
break; |
||||||
|
case operations.SCROLL_BOTTOM: |
||||||
|
this.scrollUseCase.scrollToBottom(); |
||||||
|
break; |
||||||
|
case operations.SCROLL_HOME: |
||||||
|
this.scrollUseCase.scrollToHome(); |
||||||
|
break; |
||||||
|
case operations.SCROLL_END: |
||||||
|
this.scrollUseCase.scrollToEnd(); |
||||||
|
break; |
||||||
|
case operations.FOLLOW_START: |
||||||
|
this.followMasterClient.startFollow(op.newTab, op.background); |
||||||
|
break; |
||||||
|
case operations.MARK_SET_PREFIX: |
||||||
|
this.markKeyUseCase.enableSetMode(); |
||||||
|
break; |
||||||
|
case operations.MARK_JUMP_PREFIX: |
||||||
|
this.markKeyUseCase.enableJumpMode(); |
||||||
|
break; |
||||||
|
case operations.NAVIGATE_HISTORY_PREV: |
||||||
|
this.navigateUseCase.openHistoryPrev(); |
||||||
|
break; |
||||||
|
case operations.NAVIGATE_HISTORY_NEXT: |
||||||
|
this.navigateUseCase.openHistoryNext(); |
||||||
|
break; |
||||||
|
case operations.NAVIGATE_LINK_PREV: |
||||||
|
this.navigateUseCase.openLinkPrev(); |
||||||
|
break; |
||||||
|
case operations.NAVIGATE_LINK_NEXT: |
||||||
|
this.navigateUseCase.openLinkNext(); |
||||||
|
break; |
||||||
|
case operations.NAVIGATE_PARENT: |
||||||
|
this.navigateUseCase.openParent(); |
||||||
|
break; |
||||||
|
case operations.NAVIGATE_ROOT: |
||||||
|
this.navigateUseCase.openRoot(); |
||||||
|
break; |
||||||
|
case operations.FOCUS_INPUT: |
||||||
|
this.focusUseCase.focusFirstInput(); |
||||||
|
break; |
||||||
|
case operations.URLS_YANK: |
||||||
|
this.clipbaordUseCase.yankCurrentURL(); |
||||||
|
break; |
||||||
|
case operations.URLS_PASTE: |
||||||
|
this.clipbaordUseCase.openOrSearch( |
||||||
|
op.newTab ? op.newTab : false, |
||||||
|
); |
||||||
|
break; |
||||||
|
default: |
||||||
|
this.backgroundClient.execBackgroundOp(op); |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
import * as messages from '../../shared/messages'; |
||||||
|
import MarkUseCase from '../usecases/MarkUseCase'; |
||||||
|
|
||||||
|
export default class MarkController { |
||||||
|
private markUseCase: MarkUseCase; |
||||||
|
|
||||||
|
constructor({ |
||||||
|
markUseCase = new MarkUseCase(), |
||||||
|
} = {}) { |
||||||
|
this.markUseCase = markUseCase; |
||||||
|
} |
||||||
|
|
||||||
|
scrollTo(message: messages.TabScrollToMessage) { |
||||||
|
this.markUseCase.scroll(message.x, message.y); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,31 @@ |
|||||||
|
import MarkUseCase from '../usecases/MarkUseCase'; |
||||||
|
import MarkKeyyUseCase from '../usecases/MarkKeyUseCase'; |
||||||
|
import Key from '../domains/Key'; |
||||||
|
|
||||||
|
export default class MarkKeyController { |
||||||
|
private markUseCase: MarkUseCase; |
||||||
|
|
||||||
|
private markKeyUseCase: MarkKeyyUseCase; |
||||||
|
|
||||||
|
constructor({ |
||||||
|
markUseCase = new MarkUseCase(), |
||||||
|
markKeyUseCase = new MarkKeyyUseCase(), |
||||||
|
} = {}) { |
||||||
|
this.markUseCase = markUseCase; |
||||||
|
this.markKeyUseCase = markKeyUseCase; |
||||||
|
} |
||||||
|
|
||||||
|
press(key: Key): boolean { |
||||||
|
if (this.markKeyUseCase.isSetMode()) { |
||||||
|
this.markUseCase.set(key.key); |
||||||
|
this.markKeyUseCase.disableSetMode(); |
||||||
|
return true; |
||||||
|
} |
||||||
|
if (this.markKeyUseCase.isJumpMode()) { |
||||||
|
this.markUseCase.jump(key.key); |
||||||
|
this.markKeyUseCase.disableJumpMode(); |
||||||
|
return true; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
import AddonEnabledUseCase from '../usecases/AddonEnabledUseCase'; |
||||||
|
import SettingUseCase from '../usecases/SettingUseCase'; |
||||||
|
import * as blacklists from '../../shared/blacklists'; |
||||||
|
|
||||||
|
import * as messages from '../../shared/messages'; |
||||||
|
|
||||||
|
export default class SettingController { |
||||||
|
private addonEnabledUseCase: AddonEnabledUseCase; |
||||||
|
|
||||||
|
private settingUseCase: SettingUseCase; |
||||||
|
|
||||||
|
constructor({ |
||||||
|
addonEnabledUseCase = new AddonEnabledUseCase(), |
||||||
|
settingUseCase = new SettingUseCase(), |
||||||
|
} = {}) { |
||||||
|
this.addonEnabledUseCase = addonEnabledUseCase; |
||||||
|
this.settingUseCase = settingUseCase; |
||||||
|
} |
||||||
|
|
||||||
|
async initSettings(): Promise<void> { |
||||||
|
try { |
||||||
|
let current = await this.settingUseCase.reload(); |
||||||
|
let disabled = blacklists.includes( |
||||||
|
current.blacklist, window.location.href, |
||||||
|
); |
||||||
|
if (disabled) { |
||||||
|
this.addonEnabledUseCase.disable(); |
||||||
|
} else { |
||||||
|
this.addonEnabledUseCase.enable(); |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
// Sometime sendMessage fails when background script is not ready.
|
||||||
|
console.warn(e); |
||||||
|
setTimeout(() => this.initSettings(), 500); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async reloadSettings(_message: messages.Message): Promise<void> { |
||||||
|
await this.settingUseCase.reload(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,64 @@ |
|||||||
|
import Key, * as keyUtils from './Key'; |
||||||
|
|
||||||
|
export default class KeySequence { |
||||||
|
private keys: Key[]; |
||||||
|
|
||||||
|
private constructor(keys: Key[]) { |
||||||
|
this.keys = keys; |
||||||
|
} |
||||||
|
|
||||||
|
static from(keys: Key[]): KeySequence { |
||||||
|
return new KeySequence(keys); |
||||||
|
} |
||||||
|
|
||||||
|
push(key: Key): number { |
||||||
|
return this.keys.push(key); |
||||||
|
} |
||||||
|
|
||||||
|
length(): number { |
||||||
|
return this.keys.length; |
||||||
|
} |
||||||
|
|
||||||
|
startsWith(o: KeySequence): boolean { |
||||||
|
if (this.keys.length < o.keys.length) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
for (let i = 0; i < o.keys.length; ++i) { |
||||||
|
if (!keyUtils.equals(this.keys[i], o.keys[i])) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
getKeyArray(): Key[] { |
||||||
|
return this.keys; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const fromMapKeys = (keys: string): KeySequence => { |
||||||
|
const fromMapKeysRecursive = ( |
||||||
|
remainings: string, mappedKeys: Key[], |
||||||
|
): Key[] => { |
||||||
|
if (remainings.length === 0) { |
||||||
|
return mappedKeys; |
||||||
|
} |
||||||
|
|
||||||
|
let nextPos = 1; |
||||||
|
if (remainings.startsWith('<')) { |
||||||
|
let ltPos = remainings.indexOf('>'); |
||||||
|
if (ltPos > 0) { |
||||||
|
nextPos = ltPos + 1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return fromMapKeysRecursive( |
||||||
|
remainings.slice(nextPos), |
||||||
|
mappedKeys.concat([keyUtils.fromMapKey(remainings.slice(0, nextPos))]) |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
let data = fromMapKeysRecursive(keys, []); |
||||||
|
return KeySequence.from(data); |
||||||
|
}; |
||||||
|
|
@ -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,16 +1,16 @@ |
|||||||
import TopContentComponent from './components/top-content'; |
import { ConsoleFramePresenterImpl } from './presenters/ConsoleFramePresenter'; |
||||||
import FrameContentComponent from './components/frame-content'; |
|
||||||
import consoleFrameStyle from './site-style'; |
import consoleFrameStyle from './site-style'; |
||||||
import { newStore } from './store'; |
import * as routes from './routes'; |
||||||
|
|
||||||
const store = newStore(); |
|
||||||
|
|
||||||
if (window.self === window.top) { |
if (window.self === window.top) { |
||||||
new TopContentComponent(window, store); // eslint-disable-line no-new
|
routes.routeMasterComponents(); |
||||||
} else { |
|
||||||
new FrameContentComponent(window, store); // eslint-disable-line no-new
|
new ConsoleFramePresenterImpl().initialize(); |
||||||
} |
} |
||||||
|
|
||||||
|
routes.routeComponents(); |
||||||
|
|
||||||
|
|
||||||
let style = window.document.createElement('style'); |
let style = window.document.createElement('style'); |
||||||
style.textContent = consoleFrameStyle; |
style.textContent = consoleFrameStyle; |
||||||
window.document.head.appendChild(style); |
window.document.head.appendChild(style); |
||||||
|
@ -1,83 +0,0 @@ |
|||||||
const REL_PATTERN: {[key: string]: RegExp} = { |
|
||||||
prev: /^(?:prev(?:ious)?|older)\b|\u2039|\u2190|\xab|\u226a|<</i, |
|
||||||
next: /^(?:next|newer)\b|\u203a|\u2192|\xbb|\u226b|>>/i, |
|
||||||
}; |
|
||||||
|
|
||||||
// Return the last element in the document matching the supplied selector
|
|
||||||
// and the optional filter, or null if there are no matches.
|
|
||||||
// eslint-disable-next-line func-style
|
|
||||||
function selectLast<E extends Element>( |
|
||||||
win: Window, |
|
||||||
selector: string, |
|
||||||
filter?: (e: E) => boolean, |
|
||||||
): E | null { |
|
||||||
let nodes = Array.from( |
|
||||||
win.document.querySelectorAll(selector) as NodeListOf<E> |
|
||||||
); |
|
||||||
|
|
||||||
if (filter) { |
|
||||||
nodes = nodes.filter(filter); |
|
||||||
} |
|
||||||
return nodes.length ? nodes[nodes.length - 1] : null; |
|
||||||
} |
|
||||||
|
|
||||||
const historyPrev = (win: Window): void => { |
|
||||||
win.history.back(); |
|
||||||
}; |
|
||||||
|
|
||||||
const historyNext = (win: Window): void => { |
|
||||||
win.history.forward(); |
|
||||||
}; |
|
||||||
|
|
||||||
// Code common to linkPrev and linkNext which navigates to the specified page.
|
|
||||||
const linkRel = (win: Window, rel: string): void => { |
|
||||||
let link = selectLast<HTMLLinkElement>(win, `link[rel~=${rel}][href]`); |
|
||||||
if (link) { |
|
||||||
win.location.href = link.href; |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
const pattern = REL_PATTERN[rel]; |
|
||||||
|
|
||||||
let a = selectLast<HTMLAnchorElement>(win, `a[rel~=${rel}][href]`) || |
|
||||||
// `innerText` is much slower than `textContent`, but produces much better
|
|
||||||
// (i.e. less unexpected) results
|
|
||||||
selectLast(win, 'a[href]', lnk => pattern.test(lnk.innerText)); |
|
||||||
|
|
||||||
if (a) { |
|
||||||
a.click(); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
const linkPrev = (win: Window): void => { |
|
||||||
linkRel(win, 'prev'); |
|
||||||
}; |
|
||||||
|
|
||||||
const linkNext = (win: Window): void => { |
|
||||||
linkRel(win, 'next'); |
|
||||||
}; |
|
||||||
|
|
||||||
const parent = (win: Window): void => { |
|
||||||
const loc = win.location; |
|
||||||
if (loc.hash !== '') { |
|
||||||
loc.hash = ''; |
|
||||||
return; |
|
||||||
} else if (loc.search !== '') { |
|
||||||
loc.search = ''; |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
const basenamePattern = /\/[^/]+$/; |
|
||||||
const lastDirPattern = /\/[^/]+\/$/; |
|
||||||
if (basenamePattern.test(loc.pathname)) { |
|
||||||
loc.pathname = loc.pathname.replace(basenamePattern, '/'); |
|
||||||
} else if (lastDirPattern.test(loc.pathname)) { |
|
||||||
loc.pathname = loc.pathname.replace(lastDirPattern, '/'); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
const root = (win: Window): void => { |
|
||||||
win.location.href = win.location.origin; |
|
||||||
}; |
|
||||||
|
|
||||||
export { historyPrev, historyNext, linkPrev, linkNext, parent, root }; |
|
@ -0,0 +1,25 @@ |
|||||||
|
export default interface ConsoleFramePresenter { |
||||||
|
initialize(): void; |
||||||
|
|
||||||
|
blur(): void; |
||||||
|
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
} |
||||||
|
|
||||||
|
export class ConsoleFramePresenterImpl implements ConsoleFramePresenter { |
||||||
|
initialize(): void { |
||||||
|
let iframe = document.createElement('iframe'); |
||||||
|
iframe.src = browser.runtime.getURL('build/console.html'); |
||||||
|
iframe.id = 'vimvixen-console-frame'; |
||||||
|
iframe.className = 'vimvixen-console-frame'; |
||||||
|
document.body.append(iframe); |
||||||
|
} |
||||||
|
|
||||||
|
blur(): void { |
||||||
|
let ele = document.getElementById('vimvixen-console-frame'); |
||||||
|
if (!ele) { |
||||||
|
throw new Error('console frame not created'); |
||||||
|
} |
||||||
|
ele.blur(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,52 @@ |
|||||||
|
|
||||||
|
export default interface FindPresenter { |
||||||
|
find(keyword: string, backwards: boolean): boolean; |
||||||
|
|
||||||
|
clearSelection(): void; |
||||||
|
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
} |
||||||
|
|
||||||
|
// window.find(aString, aCaseSensitive, aBackwards, aWrapAround,
|
||||||
|
// aWholeWord, aSearchInFrames);
|
||||||
|
//
|
||||||
|
// NOTE: window.find is not standard API
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Window/find
|
||||||
|
interface MyWindow extends Window { |
||||||
|
find( |
||||||
|
aString: string, |
||||||
|
aCaseSensitive?: boolean, |
||||||
|
aBackwards?: boolean, |
||||||
|
aWrapAround?: boolean, |
||||||
|
aWholeWord?: boolean, |
||||||
|
aSearchInFrames?: boolean, |
||||||
|
aShowDialog?: boolean): boolean; |
||||||
|
} |
||||||
|
|
||||||
|
// eslint-disable-next-line no-var, vars-on-top, init-declarations
|
||||||
|
declare var window: MyWindow; |
||||||
|
|
||||||
|
export class FindPresenterImpl implements FindPresenter { |
||||||
|
find(keyword: string, backwards: boolean): boolean { |
||||||
|
let caseSensitive = false; |
||||||
|
let wrapScan = true; |
||||||
|
|
||||||
|
|
||||||
|
// NOTE: aWholeWord dows not implemented, and aSearchInFrames does not work
|
||||||
|
// because of same origin policy
|
||||||
|
let found = window.find(keyword, caseSensitive, backwards, wrapScan); |
||||||
|
if (found) { |
||||||
|
return found; |
||||||
|
} |
||||||
|
this.clearSelection(); |
||||||
|
|
||||||
|
return window.find(keyword, caseSensitive, backwards, wrapScan); |
||||||
|
} |
||||||
|
|
||||||
|
clearSelection(): void { |
||||||
|
let sel = window.getSelection(); |
||||||
|
if (sel) { |
||||||
|
sel.removeAllRanges(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,25 @@ |
|||||||
|
import * as doms from '../../shared/utils/dom'; |
||||||
|
|
||||||
|
export default interface FocusPresenter { |
||||||
|
focusFirstElement(): boolean; |
||||||
|
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
} |
||||||
|
|
||||||
|
export class FocusPresenterImpl implements FocusPresenter { |
||||||
|
focusFirstElement(): boolean { |
||||||
|
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(); |
||||||
|
return true; |
||||||
|
} else if (target instanceof HTMLTextAreaElement) { |
||||||
|
target.focus(); |
||||||
|
return true; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
@ -0,0 +1,134 @@ |
|||||||
|
import Hint, { InputHint, LinkHint } from './Hint'; |
||||||
|
import * as doms from '../../shared/utils/dom'; |
||||||
|
|
||||||
|
const TARGET_SELECTOR = [ |
||||||
|
'a', 'button', 'input', 'textarea', 'area', |
||||||
|
'[contenteditable=true]', '[contenteditable=""]', '[tabindex]', |
||||||
|
'[role="button"]', 'summary' |
||||||
|
].join(','); |
||||||
|
|
||||||
|
interface Size { |
||||||
|
width: number; |
||||||
|
height: number; |
||||||
|
} |
||||||
|
|
||||||
|
interface Point { |
||||||
|
x: number; |
||||||
|
y: number; |
||||||
|
} |
||||||
|
|
||||||
|
const inViewport = ( |
||||||
|
win: Window, |
||||||
|
element: Element, |
||||||
|
viewSize: Size, |
||||||
|
framePosition: Point, |
||||||
|
): boolean => { |
||||||
|
let { |
||||||
|
top, left, bottom, right |
||||||
|
} = doms.viewportRect(element); |
||||||
|
let doc = win.document; |
||||||
|
let frameWidth = doc.documentElement.clientWidth; |
||||||
|
let frameHeight = doc.documentElement.clientHeight; |
||||||
|
|
||||||
|
if (right < 0 || bottom < 0 || top > frameHeight || left > frameWidth) { |
||||||
|
// out of frame
|
||||||
|
return false; |
||||||
|
} |
||||||
|
if (right + framePosition.x < 0 || bottom + framePosition.y < 0 || |
||||||
|
left + framePosition.x > viewSize.width || |
||||||
|
top + framePosition.y > viewSize.height) { |
||||||
|
// out of viewport
|
||||||
|
return false; |
||||||
|
} |
||||||
|
return true; |
||||||
|
}; |
||||||
|
|
||||||
|
const isAriaHiddenOrAriaDisabled = (win: Window, element: Element): boolean => { |
||||||
|
if (!element || win.document.documentElement === element) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
for (let attr of ['aria-hidden', 'aria-disabled']) { |
||||||
|
let value = element.getAttribute(attr); |
||||||
|
if (value !== null) { |
||||||
|
let hidden = value.toLowerCase(); |
||||||
|
if (hidden === '' || hidden === 'true') { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return isAriaHiddenOrAriaDisabled(win, element.parentElement as Element); |
||||||
|
}; |
||||||
|
|
||||||
|
export default interface FollowPresenter { |
||||||
|
getTargetCount(viewSize: Size, framePosition: Point): number; |
||||||
|
|
||||||
|
createHints(viewSize: Size, framePosition: Point, tags: string[]): void; |
||||||
|
|
||||||
|
filterHints(prefix: string): void; |
||||||
|
|
||||||
|
clearHints(): void; |
||||||
|
|
||||||
|
getHint(tag: string): Hint | undefined; |
||||||
|
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
} |
||||||
|
|
||||||
|
export class FollowPresenterImpl implements FollowPresenter { |
||||||
|
private hints: Hint[] |
||||||
|
|
||||||
|
constructor() { |
||||||
|
this.hints = []; |
||||||
|
} |
||||||
|
|
||||||
|
getTargetCount(viewSize: Size, framePosition: Point): number { |
||||||
|
let targets = this.getTargets(viewSize, framePosition); |
||||||
|
return targets.length; |
||||||
|
} |
||||||
|
|
||||||
|
createHints(viewSize: Size, framePosition: Point, tags: string[]): void { |
||||||
|
let targets = this.getTargets(viewSize, framePosition); |
||||||
|
let min = Math.min(targets.length, tags.length); |
||||||
|
for (let i = 0; i < min; ++i) { |
||||||
|
let target = targets[i]; |
||||||
|
if (target instanceof HTMLAnchorElement || |
||||||
|
target instanceof HTMLAreaElement) { |
||||||
|
this.hints.push(new LinkHint(target, tags[i])); |
||||||
|
} else { |
||||||
|
this.hints.push(new InputHint(target, tags[i])); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
filterHints(prefix: string): void { |
||||||
|
let shown = this.hints.filter(h => h.getTag().startsWith(prefix)); |
||||||
|
let hidden = this.hints.filter(h => !h.getTag().startsWith(prefix)); |
||||||
|
|
||||||
|
shown.forEach(h => h.show()); |
||||||
|
hidden.forEach(h => h.hide()); |
||||||
|
} |
||||||
|
|
||||||
|
clearHints(): void { |
||||||
|
this.hints.forEach(h => h.remove()); |
||||||
|
this.hints = []; |
||||||
|
} |
||||||
|
|
||||||
|
getHint(tag: string): Hint | undefined { |
||||||
|
return this.hints.find(h => h.getTag() === tag); |
||||||
|
} |
||||||
|
|
||||||
|
private getTargets(viewSize: Size, framePosition: Point): HTMLElement[] { |
||||||
|
let all = window.document.querySelectorAll(TARGET_SELECTOR); |
||||||
|
let filtered = Array.prototype.filter.call(all, (element: HTMLElement) => { |
||||||
|
let style = window.getComputedStyle(element); |
||||||
|
|
||||||
|
// AREA's 'display' in Browser style is 'none'
|
||||||
|
return (element.tagName === 'AREA' || style.display !== 'none') && |
||||||
|
style.visibility !== 'hidden' && |
||||||
|
(element as HTMLInputElement).type !== 'hidden' && |
||||||
|
element.offsetHeight > 0 && |
||||||
|
!isAriaHiddenOrAriaDisabled(window, element) && |
||||||
|
inViewport(window, element, viewSize, framePosition); |
||||||
|
}); |
||||||
|
return filtered; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,127 @@ |
|||||||
|
import * as doms from '../../shared/utils/dom'; |
||||||
|
|
||||||
|
interface Point { |
||||||
|
x: number; |
||||||
|
y: number; |
||||||
|
} |
||||||
|
|
||||||
|
const hintPosition = (element: Element): Point => { |
||||||
|
let { left, top, right, bottom } = doms.viewportRect(element); |
||||||
|
|
||||||
|
if (element.tagName !== 'AREA') { |
||||||
|
return { x: left, y: top }; |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
x: (left + right) / 2, |
||||||
|
y: (top + bottom) / 2, |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
export default abstract class Hint { |
||||||
|
private hint: HTMLElement; |
||||||
|
|
||||||
|
private tag: string; |
||||||
|
|
||||||
|
constructor(target: HTMLElement, tag: string) { |
||||||
|
this.tag = tag; |
||||||
|
|
||||||
|
let doc = target.ownerDocument; |
||||||
|
if (doc === null) { |
||||||
|
throw new TypeError('ownerDocument is null'); |
||||||
|
} |
||||||
|
|
||||||
|
let { x, y } = hintPosition(target); |
||||||
|
let { scrollX, scrollY } = window; |
||||||
|
|
||||||
|
let hint = doc.createElement('span'); |
||||||
|
hint.className = 'vimvixen-hint'; |
||||||
|
hint.textContent = tag; |
||||||
|
hint.style.left = x + scrollX + 'px'; |
||||||
|
hint.style.top = y + scrollY + 'px'; |
||||||
|
|
||||||
|
doc.body.append(hint); |
||||||
|
|
||||||
|
this.hint = hint; |
||||||
|
this.show(); |
||||||
|
} |
||||||
|
|
||||||
|
show(): void { |
||||||
|
this.hint.style.display = 'inline'; |
||||||
|
} |
||||||
|
|
||||||
|
hide(): void { |
||||||
|
this.hint.style.display = 'none'; |
||||||
|
} |
||||||
|
|
||||||
|
remove(): void { |
||||||
|
this.hint.remove(); |
||||||
|
} |
||||||
|
|
||||||
|
getTag(): string { |
||||||
|
return this.tag; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export class LinkHint extends Hint { |
||||||
|
private target: HTMLAnchorElement | HTMLAreaElement; |
||||||
|
|
||||||
|
constructor(target: HTMLAnchorElement | HTMLAreaElement, tag: string) { |
||||||
|
super(target, tag); |
||||||
|
|
||||||
|
this.target = target; |
||||||
|
} |
||||||
|
|
||||||
|
getLink(): string { |
||||||
|
return this.target.href; |
||||||
|
} |
||||||
|
|
||||||
|
getLinkTarget(): string | null { |
||||||
|
return this.target.getAttribute('target'); |
||||||
|
} |
||||||
|
|
||||||
|
click(): void { |
||||||
|
this.target.click(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export class InputHint extends Hint { |
||||||
|
private target: HTMLElement; |
||||||
|
|
||||||
|
constructor(target: HTMLElement, tag: string) { |
||||||
|
super(target, tag); |
||||||
|
|
||||||
|
this.target = target; |
||||||
|
} |
||||||
|
|
||||||
|
activate(): void { |
||||||
|
let target = this.target; |
||||||
|
switch (target.tagName.toLowerCase()) { |
||||||
|
case 'input': |
||||||
|
switch ((target as HTMLInputElement).type) { |
||||||
|
case 'file': |
||||||
|
case 'checkbox': |
||||||
|
case 'radio': |
||||||
|
case 'submit': |
||||||
|
case 'reset': |
||||||
|
case 'button': |
||||||
|
case 'image': |
||||||
|
case 'color': |
||||||
|
return target.click(); |
||||||
|
default: |
||||||
|
return target.focus(); |
||||||
|
} |
||||||
|
case 'textarea': |
||||||
|
return target.focus(); |
||||||
|
case 'button': |
||||||
|
case 'summary': |
||||||
|
return target.click(); |
||||||
|
default: |
||||||
|
if (doms.isContentEditable(target)) { |
||||||
|
return target.focus(); |
||||||
|
} else if (target.hasAttribute('tabindex')) { |
||||||
|
return target.click(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,98 @@ |
|||||||
|
export default interface NavigationPresenter { |
||||||
|
openHistoryPrev(): void; |
||||||
|
|
||||||
|
openHistoryNext(): void; |
||||||
|
|
||||||
|
openLinkPrev(): void; |
||||||
|
|
||||||
|
openLinkNext(): void; |
||||||
|
|
||||||
|
openParent(): void; |
||||||
|
|
||||||
|
openRoot(): void; |
||||||
|
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
} |
||||||
|
|
||||||
|
const REL_PATTERN: {[key: string]: RegExp} = { |
||||||
|
prev: /^(?:prev(?:ious)?|older)\b|\u2039|\u2190|\xab|\u226a|<</i, |
||||||
|
next: /^(?:next|newer)\b|\u203a|\u2192|\xbb|\u226b|>>/i, |
||||||
|
}; |
||||||
|
|
||||||
|
// Return the last element in the document matching the supplied selector
|
||||||
|
// and the optional filter, or null if there are no matches.
|
||||||
|
// eslint-disable-next-line func-style
|
||||||
|
function selectLast<E extends Element>( |
||||||
|
selector: string, |
||||||
|
filter?: (e: E) => boolean, |
||||||
|
): E | null { |
||||||
|
let nodes = Array.from( |
||||||
|
window.document.querySelectorAll(selector) as NodeListOf<E> |
||||||
|
); |
||||||
|
|
||||||
|
if (filter) { |
||||||
|
nodes = nodes.filter(filter); |
||||||
|
} |
||||||
|
return nodes.length ? nodes[nodes.length - 1] : null; |
||||||
|
} |
||||||
|
|
||||||
|
export class NavigationPresenterImpl implements NavigationPresenter { |
||||||
|
openHistoryPrev(): void { |
||||||
|
window.history.back(); |
||||||
|
} |
||||||
|
|
||||||
|
openHistoryNext(): void { |
||||||
|
window.history.forward(); |
||||||
|
} |
||||||
|
|
||||||
|
openLinkPrev(): void { |
||||||
|
this.linkRel('prev'); |
||||||
|
} |
||||||
|
|
||||||
|
openLinkNext(): void { |
||||||
|
this.linkRel('next'); |
||||||
|
} |
||||||
|
|
||||||
|
openParent(): void { |
||||||
|
const loc = window.location; |
||||||
|
if (loc.hash !== '') { |
||||||
|
loc.hash = ''; |
||||||
|
return; |
||||||
|
} else if (loc.search !== '') { |
||||||
|
loc.search = ''; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const basenamePattern = /\/[^/]+$/; |
||||||
|
const lastDirPattern = /\/[^/]+\/$/; |
||||||
|
if (basenamePattern.test(loc.pathname)) { |
||||||
|
loc.pathname = loc.pathname.replace(basenamePattern, '/'); |
||||||
|
} else if (lastDirPattern.test(loc.pathname)) { |
||||||
|
loc.pathname = loc.pathname.replace(lastDirPattern, '/'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
openRoot(): void { |
||||||
|
window.location.href = window.location.origin; |
||||||
|
} |
||||||
|
|
||||||
|
// Code common to linkPrev and linkNext which navigates to the specified page.
|
||||||
|
private linkRel(rel: 'prev' | 'next'): void { |
||||||
|
let link = selectLast<HTMLLinkElement>(`link[rel~=${rel}][href]`); |
||||||
|
if (link) { |
||||||
|
window.location.href = link.href; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const pattern = REL_PATTERN[rel]; |
||||||
|
|
||||||
|
let a = selectLast<HTMLAnchorElement>(`a[rel~=${rel}][href]`) || |
||||||
|
// `innerText` is much slower than `textContent`, but produces much better
|
||||||
|
// (i.e. less unexpected) results
|
||||||
|
selectLast('a[href]', lnk => pattern.test(lnk.innerText)); |
||||||
|
|
||||||
|
if (a) { |
||||||
|
a.click(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -1,22 +0,0 @@ |
|||||||
import * as actions from '../actions'; |
|
||||||
|
|
||||||
export interface State { |
|
||||||
enabled: boolean; |
|
||||||
} |
|
||||||
|
|
||||||
const defaultState: State = { |
|
||||||
enabled: true, |
|
||||||
}; |
|
||||||
|
|
||||||
export default function reducer( |
|
||||||
state: State = defaultState, |
|
||||||
action: actions.AddonAction, |
|
||||||
): State { |
|
||||||
switch (action.type) { |
|
||||||
case actions.ADDON_SET_ENABLED: |
|
||||||
return { ...state, |
|
||||||
enabled: action.enabled, }; |
|
||||||
default: |
|
||||||
return state; |
|
||||||
} |
|
||||||
} |
|
@ -1,25 +0,0 @@ |
|||||||
import * as actions from '../actions'; |
|
||||||
|
|
||||||
export interface State { |
|
||||||
keyword: string | null; |
|
||||||
found: boolean; |
|
||||||
} |
|
||||||
|
|
||||||
const defaultState: State = { |
|
||||||
keyword: null, |
|
||||||
found: false, |
|
||||||
}; |
|
||||||
|
|
||||||
export default function reducer( |
|
||||||
state: State = defaultState, |
|
||||||
action: actions.FindAction, |
|
||||||
): State { |
|
||||||
switch (action.type) { |
|
||||||
case actions.FIND_SET_KEYWORD: |
|
||||||
return { ...state, |
|
||||||
keyword: action.keyword, |
|
||||||
found: action.found, }; |
|
||||||
default: |
|
||||||
return state; |
|
||||||
} |
|
||||||
} |
|
@ -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,21 +0,0 @@ |
|||||||
import { combineReducers } from 'redux'; |
|
||||||
import addon, { State as AddonState } from './addon'; |
|
||||||
import find, { State as FindState } from './find'; |
|
||||||
import setting, { State as SettingState } from './setting'; |
|
||||||
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 { |
|
||||||
addon: AddonState; |
|
||||||
find: FindState; |
|
||||||
setting: SettingState; |
|
||||||
input: InputState; |
|
||||||
followController: FollowControllerState; |
|
||||||
mark: MarkState; |
|
||||||
} |
|
||||||
|
|
||||||
export default combineReducers({ |
|
||||||
addon, find, setting, input, followController, mark, |
|
||||||
}); |
|
@ -1,26 +0,0 @@ |
|||||||
import * as actions from '../actions'; |
|
||||||
import * as keyUtils from '../../shared/utils/keys'; |
|
||||||
|
|
||||||
export interface State { |
|
||||||
keys: keyUtils.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,35 +0,0 @@ |
|||||||
import Mark from '../Mark'; |
|
||||||
import * as actions from '../actions'; |
|
||||||
|
|
||||||
export interface State { |
|
||||||
setMode: boolean; |
|
||||||
jumpMode: boolean; |
|
||||||
marks: { [key: string]: Mark }; |
|
||||||
} |
|
||||||
|
|
||||||
const defaultState: State = { |
|
||||||
setMode: false, |
|
||||||
jumpMode: false, |
|
||||||
marks: {}, |
|
||||||
}; |
|
||||||
|
|
||||||
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 }; |
|
||||||
case actions.MARK_SET_LOCAL: { |
|
||||||
let marks = { ...state.marks }; |
|
||||||
marks[action.key] = { x: action.x, y: action.y }; |
|
||||||
return { ...state, setMode: false, marks }; |
|
||||||
} |
|
||||||
default: |
|
||||||
return state; |
|
||||||
} |
|
||||||
} |
|
@ -1,40 +0,0 @@ |
|||||||
import * as actions from '../actions'; |
|
||||||
import * as keyUtils from '../../shared/utils/keys'; |
|
||||||
import * as operations from '../../shared/operations'; |
|
||||||
import { Search, Properties, DefaultSetting } from '../../shared/Settings'; |
|
||||||
|
|
||||||
export interface State { |
|
||||||
keymaps: { key: keyUtils.Key[], op: operations.Operation }[]; |
|
||||||
search: Search; |
|
||||||
properties: Properties; |
|
||||||
} |
|
||||||
|
|
||||||
// defaultState does not refer due to the state is load from
|
|
||||||
// background on load.
|
|
||||||
const defaultState: State = { |
|
||||||
keymaps: [], |
|
||||||
search: DefaultSetting.search, |
|
||||||
properties: DefaultSetting.properties, |
|
||||||
}; |
|
||||||
|
|
||||||
export default function reducer( |
|
||||||
state: State = defaultState, |
|
||||||
action: actions.SettingAction, |
|
||||||
): State { |
|
||||||
switch (action.type) { |
|
||||||
case actions.SETTING_SET: |
|
||||||
return { |
|
||||||
keymaps: Object.entries(action.settings.keymaps).map((entry) => { |
|
||||||
return { |
|
||||||
key: keyUtils.fromMapKeys(entry[0]), |
|
||||||
op: entry[1], |
|
||||||
}; |
|
||||||
}), |
|
||||||
properties: action.settings.properties, |
|
||||||
search: action.settings.search, |
|
||||||
}; |
|
||||||
default: |
|
||||||
return state; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
@ -0,0 +1,19 @@ |
|||||||
|
let enabled: boolean = false; |
||||||
|
|
||||||
|
export default interface AddonEnabledRepository { |
||||||
|
set(on: boolean): void; |
||||||
|
|
||||||
|
get(): boolean; |
||||||
|
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
} |
||||||
|
|
||||||
|
export class AddonEnabledRepositoryImpl implements AddonEnabledRepository { |
||||||
|
set(on: boolean): void { |
||||||
|
enabled = on; |
||||||
|
} |
||||||
|
|
||||||
|
get(): boolean { |
||||||
|
return enabled; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,46 @@ |
|||||||
|
export default interface ClipboardRepository { |
||||||
|
read(): string; |
||||||
|
|
||||||
|
write(text: string): void; |
||||||
|
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
} |
||||||
|
|
||||||
|
export class ClipboardRepositoryImpl { |
||||||
|
read(): string { |
||||||
|
let textarea = window.document.createElement('textarea'); |
||||||
|
window.document.body.append(textarea); |
||||||
|
|
||||||
|
textarea.style.position = 'fixed'; |
||||||
|
textarea.style.top = '-100px'; |
||||||
|
textarea.contentEditable = 'true'; |
||||||
|
textarea.focus(); |
||||||
|
|
||||||
|
let ok = window.document.execCommand('paste'); |
||||||
|
let value = textarea.textContent!!; |
||||||
|
textarea.remove(); |
||||||
|
|
||||||
|
if (!ok) { |
||||||
|
throw new Error('failed to access clipbaord'); |
||||||
|
} |
||||||
|
|
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
write(text: string): void { |
||||||
|
let input = window.document.createElement('input'); |
||||||
|
window.document.body.append(input); |
||||||
|
|
||||||
|
input.style.position = 'fixed'; |
||||||
|
input.style.top = '-100px'; |
||||||
|
input.value = text; |
||||||
|
input.select(); |
||||||
|
|
||||||
|
let ok = window.document.execCommand('copy'); |
||||||
|
input.remove(); |
||||||
|
|
||||||
|
if (!ok) { |
||||||
|
throw new Error('failed to access clipbaord'); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
export default interface FindRepository { |
||||||
|
getLastKeyword(): string | null; |
||||||
|
|
||||||
|
setLastKeyword(keyword: string): void; |
||||||
|
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
} |
||||||
|
|
||||||
|
let current: string | null = null; |
||||||
|
|
||||||
|
export class FindRepositoryImpl implements FindRepository { |
||||||
|
getLastKeyword(): string | null { |
||||||
|
return current; |
||||||
|
} |
||||||
|
|
||||||
|
setLastKeyword(keyword: string): void { |
||||||
|
current = keyword; |
||||||
|
} |
||||||
|
} |
@ -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,24 @@ |
|||||||
|
import Key from '../domains/Key'; |
||||||
|
import KeySequence from '../domains/KeySequence'; |
||||||
|
|
||||||
|
export default interface KeymapRepository { |
||||||
|
enqueueKey(key: Key): KeySequence; |
||||||
|
|
||||||
|
clear(): void; |
||||||
|
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
} |
||||||
|
|
||||||
|
let current: KeySequence = KeySequence.from([]); |
||||||
|
|
||||||
|
export class KeymapRepositoryImpl { |
||||||
|
|
||||||
|
enqueueKey(key: Key): KeySequence { |
||||||
|
current.push(key); |
||||||
|
return current; |
||||||
|
} |
||||||
|
|
||||||
|
clear(): void { |
||||||
|
current = KeySequence.from([]); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,52 @@ |
|||||||
|
export default interface MarkKeyRepository { |
||||||
|
isSetMode(): boolean; |
||||||
|
|
||||||
|
enableSetMode(): void; |
||||||
|
|
||||||
|
disabeSetMode(): void; |
||||||
|
|
||||||
|
isJumpMode(): boolean; |
||||||
|
|
||||||
|
enableJumpMode(): void; |
||||||
|
|
||||||
|
disabeJumpMode(): void; |
||||||
|
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
} |
||||||
|
|
||||||
|
interface Mode { |
||||||
|
setMode: boolean; |
||||||
|
jumpMode: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
let current: Mode = { |
||||||
|
setMode: false, |
||||||
|
jumpMode: false, |
||||||
|
}; |
||||||
|
|
||||||
|
export class MarkKeyRepositoryImpl implements MarkKeyRepository { |
||||||
|
|
||||||
|
isSetMode(): boolean { |
||||||
|
return current.setMode; |
||||||
|
} |
||||||
|
|
||||||
|
enableSetMode(): void { |
||||||
|
current.setMode = true; |
||||||
|
} |
||||||
|
|
||||||
|
disabeSetMode(): void { |
||||||
|
current.setMode = false; |
||||||
|
} |
||||||
|
|
||||||
|
isJumpMode(): boolean { |
||||||
|
return current.jumpMode; |
||||||
|
} |
||||||
|
|
||||||
|
enableJumpMode(): void { |
||||||
|
current.jumpMode = true; |
||||||
|
} |
||||||
|
|
||||||
|
disabeJumpMode(): void { |
||||||
|
current.jumpMode = false; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,25 @@ |
|||||||
|
import Mark from '../domains/Mark'; |
||||||
|
|
||||||
|
export default interface MarkRepository { |
||||||
|
set(key: string, mark: Mark): void; |
||||||
|
|
||||||
|
get(key: string): Mark | null; |
||||||
|
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
} |
||||||
|
|
||||||
|
const saved: {[key: string]: Mark} = {}; |
||||||
|
|
||||||
|
export class MarkRepositoryImpl implements MarkRepository { |
||||||
|
set(key: string, mark: Mark): void { |
||||||
|
saved[key] = mark; |
||||||
|
} |
||||||
|
|
||||||
|
get(key: string): Mark | null { |
||||||
|
let v = saved[key]; |
||||||
|
if (!v) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return { ...v }; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
import Settings, { DefaultSetting } from '../../shared/Settings'; |
||||||
|
|
||||||
|
let current: Settings = DefaultSetting; |
||||||
|
|
||||||
|
export default interface SettingRepository { |
||||||
|
set(setting: Settings): void; |
||||||
|
|
||||||
|
get(): Settings; |
||||||
|
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
} |
||||||
|
|
||||||
|
export class SettingRepositoryImpl implements SettingRepository { |
||||||
|
set(setting: Settings): void { |
||||||
|
current = setting; |
||||||
|
} |
||||||
|
|
||||||
|
get(): Settings { |
||||||
|
return current; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,97 @@ |
|||||||
|
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 InputDriver from './InputDriver'; |
||||||
|
import KeymapController from './controllers/KeymapController'; |
||||||
|
import AddonEnabledUseCase from './usecases/AddonEnabledUseCase'; |
||||||
|
import MarkKeyController from './controllers/MarkKeyController'; |
||||||
|
import AddonEnabledController from './controllers/AddonEnabledController'; |
||||||
|
import SettingController from './controllers/SettingController'; |
||||||
|
import ConsoleFrameController from './controllers/ConsoleFrameController'; |
||||||
|
import * as messages from '../shared/messages'; |
||||||
|
|
||||||
|
export const routeComponents = () => { |
||||||
|
let listener = new MessageListener(); |
||||||
|
|
||||||
|
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 => followKeyController.press(key)); |
||||||
|
inputDriver.onKey(key => markKeyController.press(key)); |
||||||
|
inputDriver.onKey(key => keymapController.press(key)); |
||||||
|
|
||||||
|
let settingController = new SettingController(); |
||||||
|
settingController.initSettings(); |
||||||
|
|
||||||
|
listener.onBackgroundMessage((message: messages.Message): any => { |
||||||
|
let addonEnabledUseCase = new AddonEnabledUseCase(); |
||||||
|
|
||||||
|
switch (message.type) { |
||||||
|
case messages.SETTINGS_CHANGED: |
||||||
|
return settingController.reloadSettings(message); |
||||||
|
case messages.ADDON_TOGGLE_ENABLED: |
||||||
|
return addonEnabledUseCase.toggle(); |
||||||
|
} |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
export const routeMasterComponents = () => { |
||||||
|
let listener = new MessageListener(); |
||||||
|
|
||||||
|
let findController = new FindController(); |
||||||
|
let followMasterController = new FollowMasterController(); |
||||||
|
let markController = new MarkController(); |
||||||
|
let addonEnabledController = new AddonEnabledController(); |
||||||
|
let consoleFrameController = new ConsoleFrameController(); |
||||||
|
|
||||||
|
listener.onWebMessage((message: messages.Message, sender: Window) => { |
||||||
|
switch (message.type) { |
||||||
|
case messages.CONSOLE_ENTER_FIND: |
||||||
|
return findController.start(message); |
||||||
|
case messages.FIND_NEXT: |
||||||
|
return findController.next(message); |
||||||
|
case messages.FIND_PREV: |
||||||
|
return findController.prev(message); |
||||||
|
case messages.CONSOLE_UNFOCUS: |
||||||
|
return consoleFrameController.unfocus(message); |
||||||
|
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; |
||||||
|
}); |
||||||
|
|
||||||
|
listener.onBackgroundMessage((message: messages.Message) => { |
||||||
|
switch (message.type) { |
||||||
|
case messages.ADDON_ENABLED_QUERY: |
||||||
|
return addonEnabledController.getAddonEnabled(message); |
||||||
|
case messages.TAB_SCROLL_TO: |
||||||
|
return markController.scrollTo(message); |
||||||
|
} |
||||||
|
return undefined; |
||||||
|
}); |
||||||
|
}; |
@ -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,41 +0,0 @@ |
|||||||
import * as messages from '../shared/messages'; |
|
||||||
import * as urls from '../shared/urls'; |
|
||||||
import { Search } from '../shared/Settings'; |
|
||||||
|
|
||||||
const yank = (win: Window) => { |
|
||||||
let input = win.document.createElement('input'); |
|
||||||
win.document.body.append(input); |
|
||||||
|
|
||||||
input.style.position = 'fixed'; |
|
||||||
input.style.top = '-100px'; |
|
||||||
input.value = win.location.href; |
|
||||||
input.select(); |
|
||||||
|
|
||||||
win.document.execCommand('copy'); |
|
||||||
|
|
||||||
input.remove(); |
|
||||||
}; |
|
||||||
|
|
||||||
const paste = (win: Window, newTab: boolean, search: Search) => { |
|
||||||
let textarea = win.document.createElement('textarea'); |
|
||||||
win.document.body.append(textarea); |
|
||||||
|
|
||||||
textarea.style.position = 'fixed'; |
|
||||||
textarea.style.top = '-100px'; |
|
||||||
textarea.contentEditable = 'true'; |
|
||||||
textarea.focus(); |
|
||||||
|
|
||||||
if (win.document.execCommand('paste')) { |
|
||||||
let value = textarea.textContent as string; |
|
||||||
let url = urls.searchUrl(value, search); |
|
||||||
browser.runtime.sendMessage({ |
|
||||||
type: messages.OPEN_URL, |
|
||||||
url, |
|
||||||
newTab, |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
textarea.remove(); |
|
||||||
}; |
|
||||||
|
|
||||||
export { yank, paste }; |
|
@ -0,0 +1,40 @@ |
|||||||
|
import AddonIndicatorClient, { AddonIndicatorClientImpl } |
||||||
|
from '../client/AddonIndicatorClient'; |
||||||
|
import AddonEnabledRepository, { AddonEnabledRepositoryImpl } |
||||||
|
from '../repositories/AddonEnabledRepository'; |
||||||
|
|
||||||
|
export default class AddonEnabledUseCase { |
||||||
|
private indicator: AddonIndicatorClient; |
||||||
|
|
||||||
|
private repository: AddonEnabledRepository; |
||||||
|
|
||||||
|
constructor({ |
||||||
|
indicator = new AddonIndicatorClientImpl(), |
||||||
|
repository = new AddonEnabledRepositoryImpl(), |
||||||
|
} = {}) { |
||||||
|
this.indicator = indicator; |
||||||
|
this.repository = repository; |
||||||
|
} |
||||||
|
|
||||||
|
async enable(): Promise<void> { |
||||||
|
await this.setEnabled(true); |
||||||
|
} |
||||||
|
|
||||||
|
async disable(): Promise<void> { |
||||||
|
await this.setEnabled(false); |
||||||
|
} |
||||||
|
|
||||||
|
async toggle(): Promise<void> { |
||||||
|
let current = this.repository.get(); |
||||||
|
await this.setEnabled(!current); |
||||||
|
} |
||||||
|
|
||||||
|
getEnabled(): boolean { |
||||||
|
return this.repository.get(); |
||||||
|
} |
||||||
|
|
||||||
|
private async setEnabled(on: boolean): Promise<void> { |
||||||
|
this.repository.set(on); |
||||||
|
await this.indicator.setEnabled(on); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,44 @@ |
|||||||
|
import * as urls from '../../shared/urls'; |
||||||
|
import ClipboardRepository, { ClipboardRepositoryImpl } |
||||||
|
from '../repositories/ClipboardRepository'; |
||||||
|
import SettingRepository, { SettingRepositoryImpl } |
||||||
|
from '../repositories/SettingRepository'; |
||||||
|
import TabsClient, { TabsClientImpl } |
||||||
|
from '../client/TabsClient'; |
||||||
|
import ConsoleClient, { ConsoleClientImpl } from '../client/ConsoleClient'; |
||||||
|
|
||||||
|
export default class ClipboardUseCase { |
||||||
|
private repository: ClipboardRepository; |
||||||
|
|
||||||
|
private settingRepository: SettingRepository; |
||||||
|
|
||||||
|
private client: TabsClient; |
||||||
|
|
||||||
|
private consoleClient: ConsoleClient; |
||||||
|
|
||||||
|
constructor({ |
||||||
|
repository = new ClipboardRepositoryImpl(), |
||||||
|
settingRepository = new SettingRepositoryImpl(), |
||||||
|
client = new TabsClientImpl(), |
||||||
|
consoleClient = new ConsoleClientImpl(), |
||||||
|
} = {}) { |
||||||
|
this.repository = repository; |
||||||
|
this.settingRepository = settingRepository; |
||||||
|
this.client = client; |
||||||
|
this.consoleClient = consoleClient; |
||||||
|
} |
||||||
|
|
||||||
|
async yankCurrentURL(): Promise<string> { |
||||||
|
let url = window.location.href; |
||||||
|
this.repository.write(url); |
||||||
|
await this.consoleClient.info('Yanked ' + url); |
||||||
|
return Promise.resolve(url); |
||||||
|
} |
||||||
|
|
||||||
|
async openOrSearch(newTab: boolean): Promise<void> { |
||||||
|
let search = this.settingRepository.get().search; |
||||||
|
let text = this.repository.read(); |
||||||
|
let url = urls.searchUrl(text, search); |
||||||
|
await this.client.openUrl(url, newTab); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
import ConsoleFramePresenter, { ConsoleFramePresenterImpl } |
||||||
|
from '../presenters/ConsoleFramePresenter'; |
||||||
|
|
||||||
|
export default class ConsoleFrameUseCase { |
||||||
|
private consoleFramePresenter: ConsoleFramePresenter; |
||||||
|
|
||||||
|
constructor({ |
||||||
|
consoleFramePresenter = new ConsoleFramePresenterImpl(), |
||||||
|
} = {}) { |
||||||
|
this.consoleFramePresenter = consoleFramePresenter; |
||||||
|
} |
||||||
|
|
||||||
|
unfocus() { |
||||||
|
window.focus(); |
||||||
|
this.consoleFramePresenter.blur(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
import FindMasterClient, { FindMasterClientImpl } |
||||||
|
from '../client/FindMasterClient'; |
||||||
|
|
||||||
|
export default class FindSlaveUseCase { |
||||||
|
private findMasterClient: FindMasterClient; |
||||||
|
|
||||||
|
constructor({ |
||||||
|
findMasterClient = new FindMasterClientImpl(), |
||||||
|
} = {}) { |
||||||
|
this.findMasterClient = findMasterClient; |
||||||
|
} |
||||||
|
|
||||||
|
findNext() { |
||||||
|
this.findMasterClient.findNext(); |
||||||
|
} |
||||||
|
|
||||||
|
findPrev() { |
||||||
|
this.findMasterClient.findPrev(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,81 @@ |
|||||||
|
import FindPresenter, { FindPresenterImpl } from '../presenters/FindPresenter'; |
||||||
|
import FindRepository, { FindRepositoryImpl } |
||||||
|
from '../repositories/FindRepository'; |
||||||
|
import FindClient, { FindClientImpl } from '../client/FindClient'; |
||||||
|
import ConsoleClient, { ConsoleClientImpl } from '../client/ConsoleClient'; |
||||||
|
|
||||||
|
export default class FindUseCase { |
||||||
|
private presenter: FindPresenter; |
||||||
|
|
||||||
|
private repository: FindRepository; |
||||||
|
|
||||||
|
private client: FindClient; |
||||||
|
|
||||||
|
private consoleClient: ConsoleClient; |
||||||
|
|
||||||
|
constructor({ |
||||||
|
presenter = new FindPresenterImpl() as FindPresenter, |
||||||
|
repository = new FindRepositoryImpl(), |
||||||
|
client = new FindClientImpl(), |
||||||
|
consoleClient = new ConsoleClientImpl(), |
||||||
|
} = {}) { |
||||||
|
this.presenter = presenter; |
||||||
|
this.repository = repository; |
||||||
|
this.client = client; |
||||||
|
this.consoleClient = consoleClient; |
||||||
|
} |
||||||
|
|
||||||
|
async startFind(keyword?: string): Promise<void> { |
||||||
|
this.presenter.clearSelection(); |
||||||
|
if (keyword) { |
||||||
|
this.saveKeyword(keyword); |
||||||
|
} else { |
||||||
|
let lastKeyword = await this.getKeyword(); |
||||||
|
if (!lastKeyword) { |
||||||
|
return this.showNoLastKeywordError(); |
||||||
|
} |
||||||
|
this.saveKeyword(lastKeyword); |
||||||
|
} |
||||||
|
return this.findNext(); |
||||||
|
} |
||||||
|
|
||||||
|
findNext(): Promise<void> { |
||||||
|
return this.findNextPrev(false); |
||||||
|
} |
||||||
|
|
||||||
|
findPrev(): Promise<void> { |
||||||
|
return this.findNextPrev(true); |
||||||
|
} |
||||||
|
|
||||||
|
private async findNextPrev( |
||||||
|
backwards: boolean, |
||||||
|
): Promise<void> { |
||||||
|
let keyword = await this.getKeyword(); |
||||||
|
if (!keyword) { |
||||||
|
return this.showNoLastKeywordError(); |
||||||
|
} |
||||||
|
let found = this.presenter.find(keyword, backwards); |
||||||
|
if (found) { |
||||||
|
this.consoleClient.info('Pattern found: ' + keyword); |
||||||
|
} else { |
||||||
|
this.consoleClient.error('Pattern not found: ' + keyword); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private async getKeyword(): Promise<string | null> { |
||||||
|
let keyword = this.repository.getLastKeyword(); |
||||||
|
if (!keyword) { |
||||||
|
keyword = await this.client.getGlobalLastKeyword(); |
||||||
|
} |
||||||
|
return keyword; |
||||||
|
} |
||||||
|
|
||||||
|
private async saveKeyword(keyword: string): Promise<void> { |
||||||
|
this.repository.setLastKeyword(keyword); |
||||||
|
await this.client.setGlobalLastKeyword(keyword); |
||||||
|
} |
||||||
|
|
||||||
|
private async showNoLastKeywordError(): Promise<void> { |
||||||
|
await this.consoleClient.error('No previous search keywords'); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
import FocusPresenter, { FocusPresenterImpl } |
||||||
|
from '../presenters/FocusPresenter'; |
||||||
|
|
||||||
|
export default class FocusUseCases { |
||||||
|
private presenter: FocusPresenter; |
||||||
|
|
||||||
|
constructor({ |
||||||
|
presenter = new FocusPresenterImpl(), |
||||||
|
} = {}) { |
||||||
|
this.presenter = presenter; |
||||||
|
} |
||||||
|
|
||||||
|
focusFirstInput() { |
||||||
|
this.presenter.focusFirstElement(); |
||||||
|
} |
||||||
|
} |
@ -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 './HintKeyProducer'; |
||||||
|
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(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,38 @@ |
|||||||
|
export default class HintKeyProducer { |
||||||
|
private charset: string; |
||||||
|
|
||||||
|
private counter: number[]; |
||||||
|
|
||||||
|
constructor(charset: string) { |
||||||
|
if (charset.length === 0) { |
||||||
|
throw new TypeError('charset is empty'); |
||||||
|
} |
||||||
|
|
||||||
|
this.charset = charset; |
||||||
|
this.counter = []; |
||||||
|
} |
||||||
|
|
||||||
|
produce(): string { |
||||||
|
this.increment(); |
||||||
|
|
||||||
|
return this.counter.map(x => this.charset[x]).join(''); |
||||||
|
} |
||||||
|
|
||||||
|
private increment(): void { |
||||||
|
let max = this.charset.length - 1; |
||||||
|
if (this.counter.every(x => x === max)) { |
||||||
|
this.counter = new Array(this.counter.length + 1).fill(0); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
this.counter.reverse(); |
||||||
|
let len = this.charset.length; |
||||||
|
let num = this.counter.reduce((x, y, index) => x + y * len ** index) + 1; |
||||||
|
for (let i = 0; i < this.counter.length; ++i) { |
||||||
|
this.counter[i] = num % len; |
||||||
|
num = ~~(num / len); |
||||||
|
} |
||||||
|
this.counter.reverse(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
@ -0,0 +1,87 @@ |
|||||||
|
import KeymapRepository, { KeymapRepositoryImpl } |
||||||
|
from '../repositories/KeymapRepository'; |
||||||
|
import SettingRepository, { SettingRepositoryImpl } |
||||||
|
from '../repositories/SettingRepository'; |
||||||
|
import AddonEnabledRepository, { AddonEnabledRepositoryImpl } |
||||||
|
from '../repositories/AddonEnabledRepository'; |
||||||
|
|
||||||
|
import * as operations from '../../shared/operations'; |
||||||
|
import { Keymaps } from '../../shared/Settings'; |
||||||
|
import Key from '../domains/Key'; |
||||||
|
import KeySequence, * as keySequenceUtils from '../domains/KeySequence'; |
||||||
|
|
||||||
|
type KeymapEntityMap = Map<KeySequence, operations.Operation>; |
||||||
|
|
||||||
|
const reservedKeymaps: Keymaps = { |
||||||
|
'<Esc>': { type: operations.CANCEL }, |
||||||
|
'<C-[>': { type: operations.CANCEL }, |
||||||
|
}; |
||||||
|
|
||||||
|
|
||||||
|
export default class KeymapUseCase { |
||||||
|
private repository: KeymapRepository; |
||||||
|
|
||||||
|
private settingRepository: SettingRepository; |
||||||
|
|
||||||
|
private addonEnabledRepository: AddonEnabledRepository; |
||||||
|
|
||||||
|
constructor({ |
||||||
|
repository = new KeymapRepositoryImpl(), |
||||||
|
settingRepository = new SettingRepositoryImpl(), |
||||||
|
addonEnabledRepository = new AddonEnabledRepositoryImpl(), |
||||||
|
} = {}) { |
||||||
|
this.repository = repository; |
||||||
|
this.settingRepository = settingRepository; |
||||||
|
this.addonEnabledRepository = addonEnabledRepository; |
||||||
|
} |
||||||
|
|
||||||
|
nextOp(key: Key): operations.Operation | null { |
||||||
|
let sequence = this.repository.enqueueKey(key); |
||||||
|
|
||||||
|
let keymaps = this.keymapEntityMap(); |
||||||
|
let matched = Array.from(keymaps.keys()).filter( |
||||||
|
(mapping: KeySequence) => { |
||||||
|
return mapping.startsWith(sequence); |
||||||
|
}); |
||||||
|
if (!this.addonEnabledRepository.get()) { |
||||||
|
// available keymaps are only ADDON_ENABLE and ADDON_TOGGLE_ENABLED if
|
||||||
|
// the addon disabled
|
||||||
|
matched = matched.filter((keymap) => { |
||||||
|
let type = (keymaps.get(keymap) as operations.Operation).type; |
||||||
|
return type === operations.ADDON_ENABLE || |
||||||
|
type === operations.ADDON_TOGGLE_ENABLED; |
||||||
|
}); |
||||||
|
} |
||||||
|
if (matched.length === 0) { |
||||||
|
// No operations to match with inputs
|
||||||
|
this.repository.clear(); |
||||||
|
return null; |
||||||
|
} else if (matched.length > 1 || |
||||||
|
matched.length === 1 && sequence.length() < matched[0].length()) { |
||||||
|
// More than one operations are matched
|
||||||
|
return null; |
||||||
|
} |
||||||
|
// Exactly one operation is matched
|
||||||
|
let operation = keymaps.get(matched[0]) as operations.Operation; |
||||||
|
this.repository.clear(); |
||||||
|
return operation; |
||||||
|
} |
||||||
|
|
||||||
|
clear(): void { |
||||||
|
this.repository.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
private keymapEntityMap(): KeymapEntityMap { |
||||||
|
let keymaps = { |
||||||
|
...this.settingRepository.get().keymaps, |
||||||
|
...reservedKeymaps, |
||||||
|
}; |
||||||
|
let entries = Object.entries(keymaps).map((entry) => { |
||||||
|
return [ |
||||||
|
keySequenceUtils.fromMapKeys(entry[0]), |
||||||
|
entry[1], |
||||||
|
]; |
||||||
|
}) as [KeySequence, operations.Operation][]; |
||||||
|
return new Map<KeySequence, operations.Operation>(entries); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
import MarkKeyRepository, { MarkKeyRepositoryImpl } |
||||||
|
from '../repositories/MarkKeyRepository'; |
||||||
|
|
||||||
|
export default class MarkKeyUseCase { |
||||||
|
private repository: MarkKeyRepository; |
||||||
|
|
||||||
|
constructor({ |
||||||
|
repository = new MarkKeyRepositoryImpl() |
||||||
|
} = {}) { |
||||||
|
this.repository = repository; |
||||||
|
} |
||||||
|
|
||||||
|
isSetMode(): boolean { |
||||||
|
return this.repository.isSetMode(); |
||||||
|
} |
||||||
|
|
||||||
|
isJumpMode(): boolean { |
||||||
|
return this.repository.isJumpMode(); |
||||||
|
} |
||||||
|
|
||||||
|
enableSetMode(): void { |
||||||
|
this.repository.enableSetMode(); |
||||||
|
} |
||||||
|
|
||||||
|
disableSetMode(): void { |
||||||
|
this.repository.disabeSetMode(); |
||||||
|
} |
||||||
|
|
||||||
|
enableJumpMode(): void { |
||||||
|
this.repository.enableJumpMode(); |
||||||
|
} |
||||||
|
|
||||||
|
disableJumpMode(): void { |
||||||
|
this.repository.disabeJumpMode(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,66 @@ |
|||||||
|
import ScrollPresenter, { ScrollPresenterImpl } |
||||||
|
from '../presenters/ScrollPresenter'; |
||||||
|
import MarkClient, { MarkClientImpl } from '../client/MarkClient'; |
||||||
|
import MarkRepository, { MarkRepositoryImpl } |
||||||
|
from '../repositories/MarkRepository'; |
||||||
|
import SettingRepository, { SettingRepositoryImpl } |
||||||
|
from '../repositories/SettingRepository'; |
||||||
|
import ConsoleClient, { ConsoleClientImpl } from '../client/ConsoleClient'; |
||||||
|
|
||||||
|
export default class MarkUseCase { |
||||||
|
private scrollPresenter: ScrollPresenter; |
||||||
|
|
||||||
|
private client: MarkClient; |
||||||
|
|
||||||
|
private repository: MarkRepository; |
||||||
|
|
||||||
|
private settingRepository: SettingRepository; |
||||||
|
|
||||||
|
private consoleClient: ConsoleClient; |
||||||
|
|
||||||
|
constructor({ |
||||||
|
scrollPresenter = new ScrollPresenterImpl(), |
||||||
|
client = new MarkClientImpl(), |
||||||
|
repository = new MarkRepositoryImpl(), |
||||||
|
settingRepository = new SettingRepositoryImpl(), |
||||||
|
consoleClient = new ConsoleClientImpl(), |
||||||
|
} = {}) { |
||||||
|
this.scrollPresenter = scrollPresenter; |
||||||
|
this.client = client; |
||||||
|
this.repository = repository; |
||||||
|
this.settingRepository = settingRepository; |
||||||
|
this.consoleClient = consoleClient; |
||||||
|
} |
||||||
|
|
||||||
|
async set(key: string): Promise<void> { |
||||||
|
let pos = this.scrollPresenter.getScroll(); |
||||||
|
if (this.globalKey(key)) { |
||||||
|
this.client.setGloablMark(key, pos); |
||||||
|
await this.consoleClient.info(`Set global mark to '${key}'`); |
||||||
|
} else { |
||||||
|
this.repository.set(key, pos); |
||||||
|
await this.consoleClient.info(`Set local mark to '${key}'`); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async jump(key: string): Promise<void> { |
||||||
|
if (this.globalKey(key)) { |
||||||
|
await this.client.jumpGlobalMark(key); |
||||||
|
} else { |
||||||
|
let pos = this.repository.get(key); |
||||||
|
if (!pos) { |
||||||
|
throw new Error('Mark is not set'); |
||||||
|
} |
||||||
|
this.scroll(pos.x, pos.y); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
scroll(x: number, y: number): void { |
||||||
|
let smooth = this.settingRepository.get().properties.smoothscroll; |
||||||
|
this.scrollPresenter.scrollTo(x, y, smooth); |
||||||
|
} |
||||||
|
|
||||||
|
private globalKey(key: string) { |
||||||
|
return (/^[A-Z0-9]$/).test(key); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
import NavigationPresenter, { NavigationPresenterImpl } |
||||||
|
from '../presenters/NavigationPresenter'; |
||||||
|
|
||||||
|
export default class NavigateUseCase { |
||||||
|
private navigationPresenter: NavigationPresenter; |
||||||
|
|
||||||
|
constructor({ |
||||||
|
navigationPresenter = new NavigationPresenterImpl(), |
||||||
|
} = {}) { |
||||||
|
this.navigationPresenter = navigationPresenter; |
||||||
|
} |
||||||
|
|
||||||
|
openHistoryPrev(): void { |
||||||
|
this.navigationPresenter.openHistoryPrev(); |
||||||
|
} |
||||||
|
|
||||||
|
openHistoryNext(): void { |
||||||
|
this.navigationPresenter.openHistoryNext(); |
||||||
|
} |
||||||
|
|
||||||
|
openLinkPrev(): void { |
||||||
|
this.navigationPresenter.openLinkPrev(); |
||||||
|
} |
||||||
|
|
||||||
|
openLinkNext(): void { |
||||||
|
this.navigationPresenter.openLinkNext(); |
||||||
|
} |
||||||
|
|
||||||
|
openParent(): void { |
||||||
|
this.navigationPresenter.openParent(); |
||||||
|
} |
||||||
|
|
||||||
|
openRoot(): void { |
||||||
|
this.navigationPresenter.openRoot(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,58 @@ |
|||||||
|
import ScrollPresenter, { ScrollPresenterImpl } |
||||||
|
from '../presenters/ScrollPresenter'; |
||||||
|
import SettingRepository, { SettingRepositoryImpl } |
||||||
|
from '../repositories/SettingRepository'; |
||||||
|
|
||||||
|
export default class ScrollUseCase { |
||||||
|
private presenter: ScrollPresenter; |
||||||
|
|
||||||
|
private settingRepository: SettingRepository; |
||||||
|
|
||||||
|
constructor({ |
||||||
|
presenter = new ScrollPresenterImpl(), |
||||||
|
settingRepository = new SettingRepositoryImpl(), |
||||||
|
} = {}) { |
||||||
|
this.presenter = presenter; |
||||||
|
this.settingRepository = settingRepository; |
||||||
|
} |
||||||
|
|
||||||
|
scrollVertically(count: number): void { |
||||||
|
let smooth = this.getSmoothScroll(); |
||||||
|
this.presenter.scrollVertically(count, smooth); |
||||||
|
} |
||||||
|
|
||||||
|
scrollHorizonally(count: number): void { |
||||||
|
let smooth = this.getSmoothScroll(); |
||||||
|
this.presenter.scrollHorizonally(count, smooth); |
||||||
|
} |
||||||
|
|
||||||
|
scrollPages(count: number): void { |
||||||
|
let smooth = this.getSmoothScroll(); |
||||||
|
this.presenter.scrollPages(count, smooth); |
||||||
|
} |
||||||
|
|
||||||
|
scrollToTop(): void { |
||||||
|
let smooth = this.getSmoothScroll(); |
||||||
|
this.presenter.scrollToTop(smooth); |
||||||
|
} |
||||||
|
|
||||||
|
scrollToBottom(): void { |
||||||
|
let smooth = this.getSmoothScroll(); |
||||||
|
this.presenter.scrollToBottom(smooth); |
||||||
|
} |
||||||
|
|
||||||
|
scrollToHome(): void { |
||||||
|
let smooth = this.getSmoothScroll(); |
||||||
|
this.presenter.scrollToHome(smooth); |
||||||
|
} |
||||||
|
|
||||||
|
scrollToEnd(): void { |
||||||
|
let smooth = this.getSmoothScroll(); |
||||||
|
this.presenter.scrollToEnd(smooth); |
||||||
|
} |
||||||
|
|
||||||
|
private getSmoothScroll(): boolean { |
||||||
|
let settings = this.settingRepository.get(); |
||||||
|
return settings.properties.smoothscroll; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,24 @@ |
|||||||
|
import SettingRepository, { SettingRepositoryImpl } |
||||||
|
from '../repositories/SettingRepository'; |
||||||
|
import SettingClient, { SettingClientImpl } from '../client/SettingClient'; |
||||||
|
import Settings from '../../shared/Settings'; |
||||||
|
|
||||||
|
export default class SettingUseCase { |
||||||
|
private repository: SettingRepository; |
||||||
|
|
||||||
|
private client: SettingClient; |
||||||
|
|
||||||
|
constructor({ |
||||||
|
repository = new SettingRepositoryImpl(), |
||||||
|
client = new SettingClientImpl(), |
||||||
|
} = {}) { |
||||||
|
this.repository = repository; |
||||||
|
this.client = client; |
||||||
|
} |
||||||
|
|
||||||
|
async reload(): Promise<Settings> { |
||||||
|
let settings = await this.client.load(); |
||||||
|
this.repository.set(settings); |
||||||
|
return settings; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,129 @@ |
|||||||
|
import InputDriver from '../../src/content/InputDriver'; |
||||||
|
import { expect } from 'chai'; |
||||||
|
import Key from '../../src/content/domains/Key'; |
||||||
|
|
||||||
|
describe('InputDriver', () => { |
||||||
|
let target: HTMLElement; |
||||||
|
let driver: InputDriver; |
||||||
|
|
||||||
|
beforeEach(() => { |
||||||
|
target = document.createElement('div'); |
||||||
|
document.body.appendChild(target); |
||||||
|
driver = new InputDriver(target); |
||||||
|
}); |
||||||
|
|
||||||
|
afterEach(() => { |
||||||
|
target.remove(); |
||||||
|
target = null; |
||||||
|
driver = null; |
||||||
|
}); |
||||||
|
|
||||||
|
it('register callbacks', (done) => { |
||||||
|
driver.onKey((key: Key): boolean => { |
||||||
|
expect(key.key).to.equal('a'); |
||||||
|
expect(key.ctrlKey).to.be.true; |
||||||
|
expect(key.shiftKey).to.be.false; |
||||||
|
expect(key.altKey).to.be.false; |
||||||
|
expect(key.metaKey).to.be.false; |
||||||
|
done(); |
||||||
|
return true; |
||||||
|
}); |
||||||
|
|
||||||
|
target.dispatchEvent(new KeyboardEvent('keydown', { |
||||||
|
key: 'a', |
||||||
|
ctrlKey: true, |
||||||
|
shiftKey: false, |
||||||
|
altKey: false, |
||||||
|
metaKey: false, |
||||||
|
})); |
||||||
|
}); |
||||||
|
|
||||||
|
it('invoke callback once', () => { |
||||||
|
let a = 0, b = 0; |
||||||
|
driver.onKey((key: Key): boolean => { |
||||||
|
if (key.key == 'a') { |
||||||
|
++a; |
||||||
|
} else { |
||||||
|
key.key == 'b' |
||||||
|
++b; |
||||||
|
} |
||||||
|
return true; |
||||||
|
}); |
||||||
|
|
||||||
|
let events = [ |
||||||
|
new KeyboardEvent('keydown', { key: 'a' }), |
||||||
|
new KeyboardEvent('keydown', { key: 'b' }), |
||||||
|
new KeyboardEvent('keypress', { key: 'a' }), |
||||||
|
new KeyboardEvent('keyup', { key: 'a' }), |
||||||
|
new KeyboardEvent('keypress', { key: 'b' }), |
||||||
|
new KeyboardEvent('keyup', { key: 'b' }), |
||||||
|
]; |
||||||
|
for (let e of events) { |
||||||
|
target.dispatchEvent(e); |
||||||
|
} |
||||||
|
|
||||||
|
expect(a).to.equal(1); |
||||||
|
expect(b).to.equal(1); |
||||||
|
}) |
||||||
|
|
||||||
|
it('propagates and stop handler chain', () => { |
||||||
|
let a = 0, b = 0, c = 0; |
||||||
|
driver.onKey((key: Key): boolean => { |
||||||
|
a++; |
||||||
|
return false; |
||||||
|
}); |
||||||
|
driver.onKey((key: Key): boolean => { |
||||||
|
b++; |
||||||
|
return true; |
||||||
|
}); |
||||||
|
driver.onKey((key: Key): boolean => { |
||||||
|
c++; |
||||||
|
return true; |
||||||
|
}); |
||||||
|
|
||||||
|
target.dispatchEvent(new KeyboardEvent('keydown', { key: 'b' })); |
||||||
|
|
||||||
|
expect(a).to.equal(1); |
||||||
|
expect(b).to.equal(1); |
||||||
|
expect(c).to.equal(0); |
||||||
|
}) |
||||||
|
|
||||||
|
it('does not invoke only meta keys', () => { |
||||||
|
driver.onKey((key: Key): boolean=> { |
||||||
|
expect.fail(); |
||||||
|
return false; |
||||||
|
}); |
||||||
|
|
||||||
|
target.dispatchEvent(new KeyboardEvent('keydown', { key: 'Shift' })); |
||||||
|
target.dispatchEvent(new KeyboardEvent('keydown', { key: 'Control' })); |
||||||
|
target.dispatchEvent(new KeyboardEvent('keydown', { key: 'Alt' })); |
||||||
|
target.dispatchEvent(new KeyboardEvent('keydown', { key: 'OS' })); |
||||||
|
}) |
||||||
|
|
||||||
|
it('ignores events from input elements', () => { |
||||||
|
['input', 'textarea', 'select'].forEach((name) => { |
||||||
|
let input = window.document.createElement(name); |
||||||
|
let driver = new InputDriver(input); |
||||||
|
driver.onKey((key: Key): boolean => { |
||||||
|
expect.fail(); |
||||||
|
return false; |
||||||
|
}); |
||||||
|
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'x' })); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
it('ignores events from contenteditable elements', () => { |
||||||
|
let div = window.document.createElement('div'); |
||||||
|
let driver = new InputDriver(div); |
||||||
|
driver.onKey((key: Key): boolean => { |
||||||
|
expect.fail(); |
||||||
|
return false; |
||||||
|
}); |
||||||
|
|
||||||
|
div.setAttribute('contenteditable', ''); |
||||||
|
div.dispatchEvent(new KeyboardEvent('keydown', { key: 'x' })); |
||||||
|
|
||||||
|
div.setAttribute('contenteditable', 'true'); |
||||||
|
div.dispatchEvent(new KeyboardEvent('keydown', { key: 'x' })); |
||||||
|
}); |
||||||
|
}); |
@ -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,35 +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); |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('setLocal', () => { |
|
||||||
it('create setLocal action', () => { |
|
||||||
let action = markActions.setLocal('a', 20, 30); |
|
||||||
expect(action.type).to.equal(actions.MARK_SET_LOCAL); |
|
||||||
expect(action.key).to.equal('a'); |
|
||||||
expect(action.x).to.equal(20); |
|
||||||
expect(action.y).to.equal(30); |
|
||||||
}); |
|
||||||
}); |
|
||||||
}); |
|
@ -1,43 +0,0 @@ |
|||||||
import * as actions from 'content/actions'; |
|
||||||
import * as settingActions from 'content/actions/setting'; |
|
||||||
|
|
||||||
describe("setting actions", () => { |
|
||||||
describe("set", () => { |
|
||||||
it('create SETTING_SET action', () => { |
|
||||||
let action = settingActions.set({ |
|
||||||
keymaps: { |
|
||||||
'dd': 'remove current tab', |
|
||||||
'z<C-A>': 'increment', |
|
||||||
}, |
|
||||||
search: { |
|
||||||
default: "google", |
|
||||||
engines: { |
|
||||||
google: 'https://google.com/search?q={}', |
|
||||||
} |
|
||||||
}, |
|
||||||
properties: { |
|
||||||
hintchars: 'abcd1234', |
|
||||||
}, |
|
||||||
blacklist: [], |
|
||||||
}); |
|
||||||
expect(action.type).to.equal(actions.SETTING_SET); |
|
||||||
expect(action.settings.properties.hintchars).to.equal('abcd1234'); |
|
||||||
}); |
|
||||||
|
|
||||||
it('overrides cancel keys', () => { |
|
||||||
let action = settingActions.set({ |
|
||||||
keymaps: { |
|
||||||
"k": { "type": "scroll.vertically", "count": -1 }, |
|
||||||
"j": { "type": "scroll.vertically", "count": 1 }, |
|
||||||
} |
|
||||||
}); |
|
||||||
let keymaps = action.settings.keymaps; |
|
||||||
expect(action.settings.keymaps).to.deep.equals({ |
|
||||||
"k": { type: "scroll.vertically", count: -1 }, |
|
||||||
"j": { type: "scroll.vertically", count: 1 }, |
|
||||||
'<Esc>': { type: 'cancel' }, |
|
||||||
'<C-[>': { type: '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> |
|
Some files were not shown because too many files have changed in this diff Show More
Reference in new issue