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 FrameContentComponent from './components/frame-content'; |
||||
import { ConsoleFramePresenterImpl } from './presenters/ConsoleFramePresenter'; |
||||
import consoleFrameStyle from './site-style'; |
||||
import { newStore } from './store'; |
||||
|
||||
const store = newStore(); |
||||
import * as routes from './routes'; |
||||
|
||||
if (window.self === window.top) { |
||||
new TopContentComponent(window, store); // eslint-disable-line no-new
|
||||
} else { |
||||
new FrameContentComponent(window, store); // eslint-disable-line no-new
|
||||
routes.routeMasterComponents(); |
||||
|
||||
new ConsoleFramePresenterImpl().initialize(); |
||||
} |
||||
|
||||
routes.routeComponents(); |
||||
|
||||
|
||||
let style = window.document.createElement('style'); |
||||
style.textContent = consoleFrameStyle; |
||||
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