aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShin'ya Ueoka <ueokande@i-beam.org>2019-05-19 15:59:05 +0900
committerGitHub <noreply@github.com>2019-05-19 15:59:05 +0900
commit3f4bc62ed515f1c5da90ee1c3e42f3d435ea6e39 (patch)
tree8af9f8e5b12d007ce9628b40f3046b73f18e29f8
parent6ec560bca33e774ff7e363270c423c919fdcf4ce (diff)
parentc4dcdff9844e2404e3bc035f4cea9fce2f7770ab (diff)
Merge pull request #587 from ueokande/refactor-content
Refactor content scripts
-rw-r--r--.eslintrc2
-rw-r--r--src/background/infrastructures/ContentMessageClient.ts6
-rw-r--r--src/console/actions/console.ts2
-rw-r--r--src/console/components/Console.tsx3
-rw-r--r--src/content/InputDriver.ts (renamed from src/content/components/common/input.ts)20
-rw-r--r--src/content/MessageListener.ts8
-rw-r--r--src/content/actions/addon.ts19
-rw-r--r--src/content/actions/find.ts100
-rw-r--r--src/content/actions/follow-controller.ts32
-rw-r--r--src/content/actions/index.ts122
-rw-r--r--src/content/actions/input.ts17
-rw-r--r--src/content/actions/mark.ts46
-rw-r--r--src/content/actions/operation.ts107
-rw-r--r--src/content/actions/setting.ts28
-rw-r--r--src/content/client/AddonIndicatorClient.ts16
-rw-r--r--src/content/client/BackgroundClient.ts11
-rw-r--r--src/content/client/ConsoleClient.ts30
-rw-r--r--src/content/client/FindClient.ts25
-rw-r--r--src/content/client/FindMasterClient.ts23
-rw-r--r--src/content/client/FollowMasterClient.ts47
-rw-r--r--src/content/client/FollowSlaveClient.ts76
-rw-r--r--src/content/client/MarkClient.ts28
-rw-r--r--src/content/client/SettingClient.ts17
-rw-r--r--src/content/client/TabsClient.ts22
-rw-r--r--src/content/components/common/follow.ts231
-rw-r--r--src/content/components/common/hint.ts62
-rw-r--r--src/content/components/common/index.ts61
-rw-r--r--src/content/components/common/keymapper.ts68
-rw-r--r--src/content/components/common/mark.ts79
-rw-r--r--src/content/components/frame-content.ts3
-rw-r--r--src/content/components/top-content/find.ts46
-rw-r--r--src/content/components/top-content/follow-controller.ts166
-rw-r--r--src/content/components/top-content/index.ts51
-rw-r--r--src/content/console-frames.ts38
-rw-r--r--src/content/controllers/AddonEnabledController.ts19
-rw-r--r--src/content/controllers/ConsoleFrameController.ts16
-rw-r--r--src/content/controllers/FindController.ts24
-rw-r--r--src/content/controllers/FollowKeyController.ts21
-rw-r--r--src/content/controllers/FollowMasterController.ts31
-rw-r--r--src/content/controllers/FollowSlaveController.ts32
-rw-r--r--src/content/controllers/KeymapController.ts148
-rw-r--r--src/content/controllers/MarkController.ts16
-rw-r--r--src/content/controllers/MarkKeyController.ts31
-rw-r--r--src/content/controllers/SettingController.ts41
-rw-r--r--src/content/domains/Key.ts (renamed from src/shared/utils/keys.ts)47
-rw-r--r--src/content/domains/KeySequence.ts64
-rw-r--r--src/content/domains/Mark.ts (renamed from src/content/Mark.ts)0
-rw-r--r--src/content/focuses.ts15
-rw-r--r--src/content/index.ts16
-rw-r--r--src/content/navigates.ts83
-rw-r--r--src/content/presenters/ConsoleFramePresenter.ts25
-rw-r--r--src/content/presenters/FindPresenter.ts52
-rw-r--r--src/content/presenters/FocusPresenter.ts25
-rw-r--r--src/content/presenters/FollowPresenter.ts134
-rw-r--r--src/content/presenters/Hint.ts127
-rw-r--r--src/content/presenters/NavigationPresenter.ts98
-rw-r--r--src/content/presenters/ScrollPresenter.ts (renamed from src/content/scrolls.ts)133
-rw-r--r--src/content/reducers/addon.ts22
-rw-r--r--src/content/reducers/find.ts25
-rw-r--r--src/content/reducers/follow-controller.ts40
-rw-r--r--src/content/reducers/index.ts21
-rw-r--r--src/content/reducers/input.ts26
-rw-r--r--src/content/reducers/mark.ts35
-rw-r--r--src/content/reducers/setting.ts40
-rw-r--r--src/content/repositories/AddonEnabledRepository.ts19
-rw-r--r--src/content/repositories/ClipboardRepository.ts46
-rw-r--r--src/content/repositories/FindRepository.ts19
-rw-r--r--src/content/repositories/FollowKeyRepository.ts35
-rw-r--r--src/content/repositories/FollowMasterRepository.ts59
-rw-r--r--src/content/repositories/FollowSlaveRepository.ts31
-rw-r--r--src/content/repositories/KeymapRepository.ts24
-rw-r--r--src/content/repositories/MarkKeyRepository.ts52
-rw-r--r--src/content/repositories/MarkRepository.ts25
-rw-r--r--src/content/repositories/SettingRepository.ts21
-rw-r--r--src/content/routes.ts97
-rw-r--r--src/content/store/index.ts8
-rw-r--r--src/content/urls.ts41
-rw-r--r--src/content/usecases/AddonEnabledUseCase.ts40
-rw-r--r--src/content/usecases/ClipboardUseCase.ts44
-rw-r--r--src/content/usecases/ConsoleFrameUseCase.ts17
-rw-r--r--src/content/usecases/FindSlaveUseCase.ts20
-rw-r--r--src/content/usecases/FindUseCase.ts81
-rw-r--r--src/content/usecases/FocusUseCase.ts16
-rw-r--r--src/content/usecases/FollowMasterUseCase.ts150
-rw-r--r--src/content/usecases/FollowSlaveUseCase.ts91
-rw-r--r--src/content/usecases/HintKeyProducer.ts38
-rw-r--r--src/content/usecases/KeymapUseCase.ts87
-rw-r--r--src/content/usecases/MarkKeyUseCase.ts36
-rw-r--r--src/content/usecases/MarkUseCase.ts66
-rw-r--r--src/content/usecases/NavigateUseCase.ts36
-rw-r--r--src/content/usecases/ScrollUseCase.ts58
-rw-r--r--src/content/usecases/SettingUseCase.ts24
-rw-r--r--src/shared/messages.ts78
-rw-r--r--src/shared/urls.ts10
-rw-r--r--test/content/InputDriver.test.ts129
-rw-r--r--test/content/actions/follow-controller.test.ts34
-rw-r--r--test/content/actions/input.test.ts19
-rw-r--r--test/content/actions/mark.test.ts35
-rw-r--r--test/content/actions/setting.test.ts43
-rw-r--r--test/content/components/common/follow.html17
-rw-r--r--test/content/components/common/follow.test.ts25
-rw-r--r--test/content/components/common/hint.test.ts57
-rw-r--r--test/content/components/common/input.test.ts72
-rw-r--r--test/content/domains/Key.test.ts (renamed from test/shared/utils/keys.test.ts)69
-rw-r--r--test/content/domains/KeySequence.test.ts72
-rw-r--r--test/content/mock/MockConsoleClient.ts26
-rw-r--r--test/content/mock/MockScrollPresenter.ts47
-rw-r--r--test/content/presenters/Hint.test.html (renamed from test/content/components/common/hint.html)0
-rw-r--r--test/content/presenters/Hint.test.ts158
-rw-r--r--test/content/presenters/NavigationPresenter.test.ts (renamed from test/content/navigates.test.ts)41
-rw-r--r--test/content/reducers/addon.test.ts17
-rw-r--r--test/content/reducers/find.test.ts22
-rw-r--r--test/content/reducers/follow-controller.test.ts47
-rw-r--r--test/content/reducers/input.test.ts25
-rw-r--r--test/content/reducers/mark.test.ts41
-rw-r--r--test/content/reducers/setting.test.ts31
-rw-r--r--test/content/repositories/AddonEnabledRepository.test.ts15
-rw-r--r--test/content/repositories/FindRepository.test.ts15
-rw-r--r--test/content/repositories/FollowKeyRepository.test.ts31
-rw-r--r--test/content/repositories/FollowMasterRepository.test.ts49
-rw-r--r--test/content/repositories/FollowSlaveRepository.test.ts24
-rw-r--r--test/content/repositories/KeymapRepository.test.ts37
-rw-r--r--test/content/repositories/MarkKeyRepository.test.ts36
-rw-r--r--test/content/repositories/MarkRepository.test.ts13
-rw-r--r--test/content/repositories/SettingRepository.test.ts30
-rw-r--r--test/content/usecases/AddonEnabledUseCase.test.ts90
-rw-r--r--test/content/usecases/ClipboardUseCase.test.ts76
-rw-r--r--test/content/usecases/FindUseCase.test.ts161
-rw-r--r--test/content/usecases/HintKeyProducer.test.ts (renamed from test/content/hint-key-producer.test.ts)5
-rw-r--r--test/content/usecases/MarkUseCase.test.ts107
-rw-r--r--test/content/usecases/SettingUseCaase.test.ts71
131 files changed, 3825 insertions, 2350 deletions
diff --git a/.eslintrc b/.eslintrc
index 7845ca5..48826fa 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -79,6 +79,6 @@
"react/jsx-indent": ["error", 2],
"react/prop-types": "off",
"react/react-in-jsx-scope": "off",
- "@typescript-eslint/no-unused-vars": "error"
+ "@typescript-eslint/no-unused-vars": ["error", { args: "none" }],
}
}
diff --git a/src/background/infrastructures/ContentMessageClient.ts b/src/background/infrastructures/ContentMessageClient.ts
index d4bc476..2215330 100644
--- a/src/background/infrastructures/ContentMessageClient.ts
+++ b/src/background/infrastructures/ContentMessageClient.ts
@@ -14,10 +14,10 @@ export default class ContentMessageClient {
}
async getAddonEnabled(tabId: number): Promise<boolean> {
- let { enabled } = await browser.tabs.sendMessage(tabId, {
+ let enabled = await browser.tabs.sendMessage(tabId, {
type: messages.ADDON_ENABLED_QUERY,
- }) as { enabled: boolean };
- return enabled;
+ });
+ return enabled as any as boolean;
}
toggleAddonEnabled(tabId: number): Promise<void> {
diff --git a/src/console/actions/console.ts b/src/console/actions/console.ts
index b1494b0..d03f52c 100644
--- a/src/console/actions/console.ts
+++ b/src/console/actions/console.ts
@@ -53,7 +53,7 @@ const enterCommand = async(
return hideCommand();
};
-const enterFind = (text: string): actions.ConsoleAction => {
+const enterFind = (text?: string): actions.ConsoleAction => {
window.top.postMessage(JSON.stringify({
type: messages.CONSOLE_ENTER_FIND,
text,
diff --git a/src/console/components/Console.tsx b/src/console/components/Console.tsx
index 3274047..68cc523 100644
--- a/src/console/components/Console.tsx
+++ b/src/console/components/Console.tsx
@@ -38,7 +38,8 @@ class Console extends React.Component<Props> {
if (this.props.mode === 'command') {
return this.props.dispatch(consoleActions.enterCommand(value));
} else if (this.props.mode === 'find') {
- return this.props.dispatch(consoleActions.enterFind(value));
+ return this.props.dispatch(consoleActions.enterFind(
+ value === '' ? undefined : value));
}
}
diff --git a/src/content/components/common/input.ts b/src/content/InputDriver.ts
index 1fe34c9..cddc825 100644
--- a/src/content/components/common/input.ts
+++ b/src/content/InputDriver.ts
@@ -1,14 +1,14 @@
-import * as dom from '../../../shared/utils/dom';
-import * as keys from '../../../shared/utils/keys';
+import * as dom from '../shared/utils/dom';
+import Key, * as keys from './domains/Key';
const cancelKey = (e: KeyboardEvent): boolean => {
return e.key === 'Escape' || e.key === '[' && e.ctrlKey;
};
-export default class InputComponent {
+export default class InputDriver {
private pressed: {[key: string]: string} = {};
- private onKeyListeners: ((key: keys.Key) => boolean)[] = [];
+ private onKeyListeners: ((key: Key) => boolean)[] = [];
constructor(target: HTMLElement) {
this.pressed = {};
@@ -19,11 +19,11 @@ export default class InputComponent {
target.addEventListener('keyup', this.onKeyUp.bind(this));
}
- onKey(cb: (key: keys.Key) => boolean) {
+ onKey(cb: (key: Key) => boolean) {
this.onKeyListeners.push(cb);
}
- onKeyPress(e: KeyboardEvent) {
+ private onKeyPress(e: KeyboardEvent) {
if (this.pressed[e.key] && this.pressed[e.key] !== 'keypress') {
return;
}
@@ -31,7 +31,7 @@ export default class InputComponent {
this.capture(e);
}
- onKeyDown(e: KeyboardEvent) {
+ private onKeyDown(e: KeyboardEvent) {
if (this.pressed[e.key] && this.pressed[e.key] !== 'keydown') {
return;
}
@@ -39,12 +39,12 @@ export default class InputComponent {
this.capture(e);
}
- onKeyUp(e: KeyboardEvent) {
+ private onKeyUp(e: KeyboardEvent) {
delete this.pressed[e.key];
}
// eslint-disable-next-line max-statements
- capture(e: KeyboardEvent) {
+ private capture(e: KeyboardEvent) {
let target = e.target;
if (!(target instanceof HTMLElement)) {
return;
@@ -71,7 +71,7 @@ export default class InputComponent {
}
}
- fromInput(e: Element) {
+ private fromInput(e: Element) {
return e instanceof HTMLInputElement ||
e instanceof HTMLTextAreaElement ||
e instanceof HTMLSelectElement ||
diff --git a/src/content/MessageListener.ts b/src/content/MessageListener.ts
index 105d028..e545cab 100644
--- a/src/content/MessageListener.ts
+++ b/src/content/MessageListener.ts
@@ -1,14 +1,16 @@
import { Message, valueOf } from '../shared/messages';
-export type WebMessageSender = Window | MessagePort | ServiceWorker | null;
export type WebExtMessageSender = browser.runtime.MessageSender;
export default class MessageListener {
onWebMessage(
- listener: (msg: Message, sender: WebMessageSender) => void,
+ listener: (msg: Message, sender: Window) => void,
) {
window.addEventListener('message', (event: MessageEvent) => {
let sender = event.source;
+ if (!(sender instanceof Window)) {
+ return;
+ }
let message = null;
try {
message = JSON.parse(event.data);
@@ -25,7 +27,7 @@ export default class MessageListener {
) {
browser.runtime.onMessage.addListener(
(msg: any, sender: WebExtMessageSender) => {
- listener(valueOf(msg), sender);
+ return listener(valueOf(msg), sender);
},
);
}
diff --git a/src/content/actions/addon.ts b/src/content/actions/addon.ts
deleted file mode 100644
index 8dedae0..0000000
--- a/src/content/actions/addon.ts
+++ /dev/null
@@ -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 };
diff --git a/src/content/actions/find.ts b/src/content/actions/find.ts
deleted file mode 100644
index 53e03ae..0000000
--- a/src/content/actions/find.ts
+++ /dev/null
@@ -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 };
diff --git a/src/content/actions/follow-controller.ts b/src/content/actions/follow-controller.ts
deleted file mode 100644
index 115b3b6..0000000
--- a/src/content/actions/follow-controller.ts
+++ /dev/null
@@ -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 };
diff --git a/src/content/actions/index.ts b/src/content/actions/index.ts
deleted file mode 100644
index 8aa9c23..0000000
--- a/src/content/actions/index.ts
+++ /dev/null
@@ -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;
diff --git a/src/content/actions/input.ts b/src/content/actions/input.ts
deleted file mode 100644
index 1df6452..0000000
--- a/src/content/actions/input.ts
+++ /dev/null
@@ -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 };
diff --git a/src/content/actions/mark.ts b/src/content/actions/mark.ts
deleted file mode 100644
index 5eb9554..0000000
--- a/src/content/actions/mark.ts
+++ /dev/null
@@ -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,
-};
diff --git a/src/content/actions/operation.ts b/src/content/actions/operation.ts
deleted file mode 100644
index 41e080b..0000000
--- a/src/content/actions/operation.ts
+++ /dev/null
@@ -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 };
diff --git a/src/content/actions/setting.ts b/src/content/actions/setting.ts
deleted file mode 100644
index 92f8559..0000000
--- a/src/content/actions/setting.ts
+++ /dev/null
@@ -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 };
diff --git a/src/content/client/AddonIndicatorClient.ts b/src/content/client/AddonIndicatorClient.ts
new file mode 100644
index 0000000..afb9fa4
--- /dev/null
+++ b/src/content/client/AddonIndicatorClient.ts
@@ -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,
+ });
+ }
+}
diff --git a/src/content/client/BackgroundClient.ts b/src/content/client/BackgroundClient.ts
new file mode 100644
index 0000000..2fe8d01
--- /dev/null
+++ b/src/content/client/BackgroundClient.ts
@@ -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,
+ });
+ }
+}
diff --git a/src/content/client/ConsoleClient.ts b/src/content/client/ConsoleClient.ts
new file mode 100644
index 0000000..e7046e5
--- /dev/null
+++ b/src/content/client/ConsoleClient.ts
@@ -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,
+ },
+ });
+ }
+}
diff --git a/src/content/client/FindClient.ts b/src/content/client/FindClient.ts
new file mode 100644
index 0000000..22cd3cb
--- /dev/null
+++ b/src/content/client/FindClient.ts
@@ -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,
+ });
+ }
+}
diff --git a/src/content/client/FindMasterClient.ts b/src/content/client/FindMasterClient.ts
new file mode 100644
index 0000000..0481ec1
--- /dev/null
+++ b/src/content/client/FindMasterClient.ts
@@ -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,
+ }), '*');
+ }
+}
diff --git a/src/content/client/FollowMasterClient.ts b/src/content/client/FollowMasterClient.ts
new file mode 100644
index 0000000..c841902
--- /dev/null
+++ b/src/content/client/FollowMasterClient.ts
@@ -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), '*');
+ }
+}
diff --git a/src/content/client/FollowSlaveClient.ts b/src/content/client/FollowSlaveClient.ts
new file mode 100644
index 0000000..0905cd9
--- /dev/null
+++ b/src/content/client/FollowSlaveClient.ts
@@ -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), '*');
+ }
+}
diff --git a/src/content/client/MarkClient.ts b/src/content/client/MarkClient.ts
new file mode 100644
index 0000000..b7cf535
--- /dev/null
+++ b/src/content/client/MarkClient.ts
@@ -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,
+ });
+ }
+}
diff --git a/src/content/client/SettingClient.ts b/src/content/client/SettingClient.ts
new file mode 100644
index 0000000..c67f544
--- /dev/null
+++ b/src/content/client/SettingClient.ts
@@ -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;
+ }
+}
diff --git a/src/content/client/TabsClient.ts b/src/content/client/TabsClient.ts
new file mode 100644
index 0000000..e1af078
--- /dev/null
+++ b/src/content/client/TabsClient.ts
@@ -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,
+ });
+ }
+}
diff --git a/src/content/components/common/follow.ts b/src/content/components/common/follow.ts
deleted file mode 100644
index 67f2dd9..0000000
--- a/src/content/components/common/follow.ts
+++ /dev/null
@@ -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;
- }
-}
diff --git a/src/content/components/common/hint.ts b/src/content/components/common/hint.ts
deleted file mode 100644
index 2fcbb0f..0000000
--- a/src/content/components/common/hint.ts
+++ /dev/null
@@ -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;
- }
-}
diff --git a/src/content/components/common/index.ts b/src/content/components/common/index.ts
deleted file mode 100644
index 5b097b6..0000000
--- a/src/content/components/common/index.ts
+++ /dev/null
@@ -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);
- }
- }
-}
diff --git a/src/content/components/common/keymapper.ts b/src/content/components/common/keymapper.ts
deleted file mode 100644
index c94bae0..0000000
--- a/src/content/components/common/keymapper.ts
+++ /dev/null
@@ -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;
- }
-}
diff --git a/src/content/components/common/mark.ts b/src/content/components/common/mark.ts
deleted file mode 100644
index 1237385..0000000
--- a/src/content/components/common/mark.ts
+++ /dev/null
@@ -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));
- }
-}
diff --git a/src/content/components/frame-content.ts b/src/content/components/frame-content.ts
deleted file mode 100644
index ca999ba..0000000
--- a/src/content/components/frame-content.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import CommonComponent from './common';
-
-export default CommonComponent;
diff --git a/src/content/components/top-content/find.ts b/src/content/components/top-content/find.ts
deleted file mode 100644
index 74b95bc..0000000
--- a/src/content/components/top-content/find.ts
+++ /dev/null
@@ -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));
- }
-}
diff --git a/src/content/components/top-content/follow-controller.ts b/src/content/components/top-content/follow-controller.ts
deleted file mode 100644
index d49b22a..0000000
--- a/src/content/components/top-content/follow-controller.ts
+++ /dev/null
@@ -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;
- }
-}
diff --git a/src/content/components/top-content/index.ts b/src/content/components/top-content/index.ts
deleted file mode 100644
index ac95ea9..0000000
--- a/src/content/components/top-content/index.ts
+++ /dev/null
@@ -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);
- }
- }
-}
diff --git a/src/content/console-frames.ts b/src/content/console-frames.ts
deleted file mode 100644
index bd6b835..0000000
--- a/src/content/console-frames.ts
+++ /dev/null
@@ -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 };
diff --git a/src/content/controllers/AddonEnabledController.ts b/src/content/controllers/AddonEnabledController.ts
new file mode 100644
index 0000000..4e19b6a
--- /dev/null
+++ b/src/content/controllers/AddonEnabledController.ts
@@ -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);
+ }
+}
diff --git a/src/content/controllers/ConsoleFrameController.ts b/src/content/controllers/ConsoleFrameController.ts
new file mode 100644
index 0000000..fafadf4
--- /dev/null
+++ b/src/content/controllers/ConsoleFrameController.ts
@@ -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();
+ }
+}
diff --git a/src/content/controllers/FindController.ts b/src/content/controllers/FindController.ts
new file mode 100644
index 0000000..cf27a8d
--- /dev/null
+++ b/src/content/controllers/FindController.ts
@@ -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();
+ }
+}
diff --git a/src/content/controllers/FollowKeyController.ts b/src/content/controllers/FollowKeyController.ts
new file mode 100644
index 0000000..eb45e01
--- /dev/null
+++ b/src/content/controllers/FollowKeyController.ts
@@ -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;
+ }
+}
diff --git a/src/content/controllers/FollowMasterController.ts b/src/content/controllers/FollowMasterController.ts
new file mode 100644
index 0000000..89294ff
--- /dev/null
+++ b/src/content/controllers/FollowMasterController.ts
@@ -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);
+ }
+ }
+}
+
diff --git a/src/content/controllers/FollowSlaveController.ts b/src/content/controllers/FollowSlaveController.ts
new file mode 100644
index 0000000..88dccf3
--- /dev/null
+++ b/src/content/controllers/FollowSlaveController.ts
@@ -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();
+ }
+}
diff --git a/src/content/controllers/KeymapController.ts b/src/content/controllers/KeymapController.ts
new file mode 100644
index 0000000..20c24c0
--- /dev/null
+++ b/src/content/controllers/KeymapController.ts
@@ -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;
+ }
+}
diff --git a/src/content/controllers/MarkController.ts b/src/content/controllers/MarkController.ts
new file mode 100644
index 0000000..365794c
--- /dev/null
+++ b/src/content/controllers/MarkController.ts
@@ -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);
+ }
+}
diff --git a/src/content/controllers/MarkKeyController.ts b/src/content/controllers/MarkKeyController.ts
new file mode 100644
index 0000000..395dee3
--- /dev/null
+++ b/src/content/controllers/MarkKeyController.ts
@@ -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;
+ }
+}
diff --git a/src/content/controllers/SettingController.ts b/src/content/controllers/SettingController.ts
new file mode 100644
index 0000000..f0e770b
--- /dev/null
+++ b/src/content/controllers/SettingController.ts
@@ -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();
+ }
+}
diff --git a/src/shared/utils/keys.ts b/src/content/domains/Key.ts
index e9b0365..fbbb4bb 100644
--- a/src/shared/utils/keys.ts
+++ b/src/content/domains/Key.ts
@@ -1,9 +1,11 @@
-export interface Key {
- key: string;
- shiftKey: boolean | undefined;
- ctrlKey: boolean | undefined;
- altKey: boolean | undefined;
- metaKey: boolean | undefined;
+export default interface Key {
+ key: string;
+ shiftKey?: boolean;
+ ctrlKey?: boolean;
+ altKey?: boolean;
+ metaKey?: boolean;
+
+ // eslint-disable-next-line semi
}
const modifiedKeyName = (name: string): string => {
@@ -18,7 +20,7 @@ const modifiedKeyName = (name: string): string => {
return name;
};
-const fromKeyboardEvent = (e: KeyboardEvent): Key => {
+export const fromKeyboardEvent = (e: KeyboardEvent): Key => {
let key = modifiedKeyName(e.key);
let shift = e.shiftKey;
if (key.length === 1 && key.toUpperCase() === key.toLowerCase()) {
@@ -36,7 +38,7 @@ const fromKeyboardEvent = (e: KeyboardEvent): Key => {
};
};
-const fromMapKey = (key: string): Key => {
+export const fromMapKey = (key: string): Key => {
if (key.startsWith('<') && key.endsWith('>')) {
let inner = key.slice(1, -1);
let shift = inner.includes('S-');
@@ -63,37 +65,10 @@ const fromMapKey = (key: string): Key => {
};
};
-const fromMapKeys = (keys: string): Key[] => {
- 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([fromMapKey(remainings.slice(0, nextPos))])
- );
- };
-
- return fromMapKeysRecursive(keys, []);
-};
-
-const equals = (e1: Key, e2: Key): boolean => {
+export const equals = (e1: Key, e2: Key): boolean => {
return e1.key === e2.key &&
e1.ctrlKey === e2.ctrlKey &&
e1.metaKey === e2.metaKey &&
e1.altKey === e2.altKey &&
e1.shiftKey === e2.shiftKey;
};
-
-export { fromKeyboardEvent, fromMapKey, fromMapKeys, equals };
diff --git a/src/content/domains/KeySequence.ts b/src/content/domains/KeySequence.ts
new file mode 100644
index 0000000..6a05c2f
--- /dev/null
+++ b/src/content/domains/KeySequence.ts
@@ -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);
+};
+
diff --git a/src/content/Mark.ts b/src/content/domains/Mark.ts
index f1282fc..f1282fc 100644
--- a/src/content/Mark.ts
+++ b/src/content/domains/Mark.ts
diff --git a/src/content/focuses.ts b/src/content/focuses.ts
deleted file mode 100644
index 8f53881..0000000
--- a/src/content/focuses.ts
+++ /dev/null
@@ -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 };
diff --git a/src/content/index.ts b/src/content/index.ts
index 9d791fc..660ebf5 100644
--- a/src/content/index.ts
+++ b/src/content/index.ts
@@ -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);
diff --git a/src/content/navigates.ts b/src/content/navigates.ts
deleted file mode 100644
index a2007a6..0000000
--- a/src/content/navigates.ts
+++ /dev/null
@@ -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 };
diff --git a/src/content/presenters/ConsoleFramePresenter.ts b/src/content/presenters/ConsoleFramePresenter.ts
new file mode 100644
index 0000000..3c7477b
--- /dev/null
+++ b/src/content/presenters/ConsoleFramePresenter.ts
@@ -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();
+ }
+}
diff --git a/src/content/presenters/FindPresenter.ts b/src/content/presenters/FindPresenter.ts
new file mode 100644
index 0000000..d9bc835
--- /dev/null
+++ b/src/content/presenters/FindPresenter.ts
@@ -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();
+ }
+ }
+}
diff --git a/src/content/presenters/FocusPresenter.ts b/src/content/presenters/FocusPresenter.ts
new file mode 100644
index 0000000..4cef5bf
--- /dev/null
+++ b/src/content/presenters/FocusPresenter.ts
@@ -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;
+ }
+}
+
diff --git a/src/content/presenters/FollowPresenter.ts b/src/content/presenters/FollowPresenter.ts
new file mode 100644
index 0000000..f0d115c
--- /dev/null
+++ b/src/content/presenters/FollowPresenter.ts
@@ -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;
+ }
+}
diff --git a/src/content/presenters/Hint.ts b/src/content/presenters/Hint.ts
new file mode 100644
index 0000000..60c0f4c
--- /dev/null
+++ b/src/content/presenters/Hint.ts
@@ -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();
+ }
+ }
+ }
+}
diff --git a/src/content/presenters/NavigationPresenter.ts b/src/content/presenters/NavigationPresenter.ts
new file mode 100644
index 0000000..66110e5
--- /dev/null
+++ b/src/content/presenters/NavigationPresenter.ts
@@ -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();
+ }
+ }
+}
diff --git a/src/content/scrolls.ts b/src/content/presenters/ScrollPresenter.ts
index 6a35315..9286fb0 100644
--- a/src/content/scrolls.ts
+++ b/src/content/presenters/ScrollPresenter.ts
@@ -1,4 +1,4 @@
-import * as doms from '../shared/utils/dom';
+import * as doms from '../../shared/utils/dom';
const SCROLL_DELTA_X = 64;
const SCROLL_DELTA_Y = 64;
@@ -94,75 +94,86 @@ class Scroller {
}
}
-const getScroll = () => {
- let target = scrollTarget();
- return { x: target.scrollLeft, y: target.scrollTop };
-};
+export type Point = { x: number, y: number };
+
+export default interface ScrollPresenter {
+ getScroll(): Point;
+ scrollVertically(amount: number, smooth: boolean): void;
+ scrollHorizonally(amount: number, smooth: boolean): void;
+ scrollPages(amount: number, smooth: boolean): void;
+ scrollTo(x: number, y: number, smooth: boolean): void;
+ scrollToTop(smooth: boolean): void;
+ scrollToBottom(smooth: boolean): void;
+ scrollToHome(smooth: boolean): void;
+ scrollToEnd(smooth: boolean): void;
+
+ // eslint-disable-next-line semi
+}
-const scrollVertically = (count: number, smooth: boolean): void => {
- let target = scrollTarget();
- let delta = SCROLL_DELTA_Y * count;
- if (scrolling) {
- delta = SCROLL_DELTA_Y * count * 4;
+export class ScrollPresenterImpl {
+ getScroll(): Point {
+ let target = scrollTarget();
+ return { x: target.scrollLeft, y: target.scrollTop };
}
- new Scroller(target, smooth).scrollBy(0, delta);
-};
-const scrollHorizonally = (count: number, smooth: boolean): void => {
- let target = scrollTarget();
- let delta = SCROLL_DELTA_X * count;
- if (scrolling) {
- delta = SCROLL_DELTA_X * count * 4;
+ scrollVertically(count: number, smooth: boolean): void {
+ let target = scrollTarget();
+ let delta = SCROLL_DELTA_Y * count;
+ if (scrolling) {
+ delta = SCROLL_DELTA_Y * count * 4;
+ }
+ new Scroller(target, smooth).scrollBy(0, delta);
}
- new Scroller(target, smooth).scrollBy(delta, 0);
-};
-const scrollPages = (count: number, smooth: boolean): void => {
- let target = scrollTarget();
- let height = target.clientHeight;
- let delta = height * count;
- if (scrolling) {
- delta = height * count;
+ scrollHorizonally(count: number, smooth: boolean): void {
+ let target = scrollTarget();
+ let delta = SCROLL_DELTA_X * count;
+ if (scrolling) {
+ delta = SCROLL_DELTA_X * count * 4;
+ }
+ new Scroller(target, smooth).scrollBy(delta, 0);
}
- new Scroller(target, smooth).scrollBy(0, delta);
-};
-const scrollTo = (x: number, y: number, smooth: boolean): void => {
- let target = scrollTarget();
- new Scroller(target, smooth).scrollTo(x, y);
-};
+ scrollPages(count: number, smooth: boolean): void {
+ let target = scrollTarget();
+ let height = target.clientHeight;
+ let delta = height * count;
+ if (scrolling) {
+ delta = height * count;
+ }
+ new Scroller(target, smooth).scrollBy(0, delta);
+ }
-const scrollToTop = (smooth: boolean): void => {
- let target = scrollTarget();
- let x = target.scrollLeft;
- let y = 0;
- new Scroller(target, smooth).scrollTo(x, y);
-};
+ scrollTo(x: number, y: number, smooth: boolean): void {
+ let target = scrollTarget();
+ new Scroller(target, smooth).scrollTo(x, y);
+ }
-const scrollToBottom = (smooth: boolean): void => {
- let target = scrollTarget();
- let x = target.scrollLeft;
- let y = target.scrollHeight;
- new Scroller(target, smooth).scrollTo(x, y);
-};
+ scrollToTop(smooth: boolean): void {
+ let target = scrollTarget();
+ let x = target.scrollLeft;
+ let y = 0;
+ new Scroller(target, smooth).scrollTo(x, y);
+ }
-const scrollToHome = (smooth: boolean): void => {
- let target = scrollTarget();
- let x = 0;
- let y = target.scrollTop;
- new Scroller(target, smooth).scrollTo(x, y);
-};
+ scrollToBottom(smooth: boolean): void {
+ let target = scrollTarget();
+ let x = target.scrollLeft;
+ let y = target.scrollHeight;
+ new Scroller(target, smooth).scrollTo(x, y);
+ }
-const scrollToEnd = (smooth: boolean): void => {
- let target = scrollTarget();
- let x = target.scrollWidth;
- let y = target.scrollTop;
- new Scroller(target, smooth).scrollTo(x, y);
-};
+ scrollToHome(smooth: boolean): void {
+ let target = scrollTarget();
+ let x = 0;
+ let y = target.scrollTop;
+ new Scroller(target, smooth).scrollTo(x, y);
+ }
-export {
- getScroll,
- scrollVertically, scrollHorizonally, scrollPages,
- scrollTo,
- scrollToTop, scrollToBottom, scrollToHome, scrollToEnd
-};
+ scrollToEnd(smooth: boolean): void {
+ let target = scrollTarget();
+ let x = target.scrollWidth;
+ let y = target.scrollTop;
+ new Scroller(target, smooth).scrollTo(x, y);
+ }
+}
diff --git a/src/content/reducers/addon.ts b/src/content/reducers/addon.ts
deleted file mode 100644
index 2131228..0000000
--- a/src/content/reducers/addon.ts
+++ /dev/null
@@ -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;
- }
-}
diff --git a/src/content/reducers/find.ts b/src/content/reducers/find.ts
deleted file mode 100644
index 8c3e637..0000000
--- a/src/content/reducers/find.ts
+++ /dev/null
@@ -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;
- }
-}
diff --git a/src/content/reducers/follow-controller.ts b/src/content/reducers/follow-controller.ts
deleted file mode 100644
index 6965704..0000000
--- a/src/content/reducers/follow-controller.ts
+++ /dev/null
@@ -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;
- }
-}
diff --git a/src/content/reducers/index.ts b/src/content/reducers/index.ts
deleted file mode 100644
index fb5eb84..0000000
--- a/src/content/reducers/index.ts
+++ /dev/null
@@ -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,
-});
diff --git a/src/content/reducers/input.ts b/src/content/reducers/input.ts
deleted file mode 100644
index 35b9075..0000000
--- a/src/content/reducers/input.ts
+++ /dev/null
@@ -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;
- }
-}
diff --git a/src/content/reducers/mark.ts b/src/content/reducers/mark.ts
deleted file mode 100644
index 7409938..0000000
--- a/src/content/reducers/mark.ts
+++ /dev/null
@@ -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;
- }
-}
diff --git a/src/content/reducers/setting.ts b/src/content/reducers/setting.ts
deleted file mode 100644
index 9ca1380..0000000
--- a/src/content/reducers/setting.ts
+++ /dev/null
@@ -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;
- }
-}
-
diff --git a/src/content/repositories/AddonEnabledRepository.ts b/src/content/repositories/AddonEnabledRepository.ts
new file mode 100644
index 0000000..4eaabb1
--- /dev/null
+++ b/src/content/repositories/AddonEnabledRepository.ts
@@ -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;
+ }
+}
diff --git a/src/content/repositories/ClipboardRepository.ts b/src/content/repositories/ClipboardRepository.ts
new file mode 100644
index 0000000..747ae6a
--- /dev/null
+++ b/src/content/repositories/ClipboardRepository.ts
@@ -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');
+ }
+ }
+}
diff --git a/src/content/repositories/FindRepository.ts b/src/content/repositories/FindRepository.ts
new file mode 100644
index 0000000..85eca40
--- /dev/null
+++ b/src/content/repositories/FindRepository.ts
@@ -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;
+ }
+}
diff --git a/src/content/repositories/FollowKeyRepository.ts b/src/content/repositories/FollowKeyRepository.ts
new file mode 100644
index 0000000..a671b5c
--- /dev/null
+++ b/src/content/repositories/FollowKeyRepository.ts
@@ -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 = [];
+ }
+}
diff --git a/src/content/repositories/FollowMasterRepository.ts b/src/content/repositories/FollowMasterRepository.ts
new file mode 100644
index 0000000..a964953
--- /dev/null
+++ b/src/content/repositories/FollowMasterRepository.ts
@@ -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;
+ }
+}
+
diff --git a/src/content/repositories/FollowSlaveRepository.ts b/src/content/repositories/FollowSlaveRepository.ts
new file mode 100644
index 0000000..4c2de72
--- /dev/null
+++ b/src/content/repositories/FollowSlaveRepository.ts
@@ -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;
+ }
+}
+
+
diff --git a/src/content/repositories/KeymapRepository.ts b/src/content/repositories/KeymapRepository.ts
new file mode 100644
index 0000000..770ba0b
--- /dev/null
+++ b/src/content/repositories/KeymapRepository.ts
@@ -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([]);
+ }
+}
diff --git a/src/content/repositories/MarkKeyRepository.ts b/src/content/repositories/MarkKeyRepository.ts
new file mode 100644
index 0000000..c24548a
--- /dev/null
+++ b/src/content/repositories/MarkKeyRepository.ts
@@ -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;
+ }
+}
diff --git a/src/content/repositories/MarkRepository.ts b/src/content/repositories/MarkRepository.ts
new file mode 100644
index 0000000..ed5afe2
--- /dev/null
+++ b/src/content/repositories/MarkRepository.ts
@@ -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 };
+ }
+}
diff --git a/src/content/repositories/SettingRepository.ts b/src/content/repositories/SettingRepository.ts
new file mode 100644
index 0000000..711b2a2
--- /dev/null
+++ b/src/content/repositories/SettingRepository.ts
@@ -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;
+ }
+}
diff --git a/src/content/routes.ts b/src/content/routes.ts
new file mode 100644
index 0000000..0bce4f5
--- /dev/null
+++ b/src/content/routes.ts
@@ -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;
+ });
+};
diff --git a/src/content/store/index.ts b/src/content/store/index.ts
deleted file mode 100644
index 5c41744..0000000
--- a/src/content/store/index.ts
+++ /dev/null
@@ -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),
-);
diff --git a/src/content/urls.ts b/src/content/urls.ts
deleted file mode 100644
index 035b9bb..0000000
--- a/src/content/urls.ts
+++ /dev/null
@@ -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 };
diff --git a/src/content/usecases/AddonEnabledUseCase.ts b/src/content/usecases/AddonEnabledUseCase.ts
new file mode 100644
index 0000000..e9ce0a6
--- /dev/null
+++ b/src/content/usecases/AddonEnabledUseCase.ts
@@ -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);
+ }
+}
diff --git a/src/content/usecases/ClipboardUseCase.ts b/src/content/usecases/ClipboardUseCase.ts
new file mode 100644
index 0000000..b2ece2f
--- /dev/null
+++ b/src/content/usecases/ClipboardUseCase.ts
@@ -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);
+ }
+}
diff --git a/src/content/usecases/ConsoleFrameUseCase.ts b/src/content/usecases/ConsoleFrameUseCase.ts
new file mode 100644
index 0000000..b4c756c
--- /dev/null
+++ b/src/content/usecases/ConsoleFrameUseCase.ts
@@ -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();
+ }
+}
diff --git a/src/content/usecases/FindSlaveUseCase.ts b/src/content/usecases/FindSlaveUseCase.ts
new file mode 100644
index 0000000..b733cbd
--- /dev/null
+++ b/src/content/usecases/FindSlaveUseCase.ts
@@ -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();
+ }
+}
diff --git a/src/content/usecases/FindUseCase.ts b/src/content/usecases/FindUseCase.ts
new file mode 100644
index 0000000..74cbc97
--- /dev/null
+++ b/src/content/usecases/FindUseCase.ts
@@ -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');
+ }
+}
diff --git a/src/content/usecases/FocusUseCase.ts b/src/content/usecases/FocusUseCase.ts
new file mode 100644
index 0000000..0ad4021
--- /dev/null
+++ b/src/content/usecases/FocusUseCase.ts
@@ -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();
+ }
+}
diff --git a/src/content/usecases/FollowMasterUseCase.ts b/src/content/usecases/FollowMasterUseCase.ts
new file mode 100644
index 0000000..9cbb790
--- /dev/null
+++ b/src/content/usecases/FollowMasterUseCase.ts
@@ -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('');
+ }
+}
diff --git a/src/content/usecases/FollowSlaveUseCase.ts b/src/content/usecases/FollowSlaveUseCase.ts
new file mode 100644
index 0000000..eb011de
--- /dev/null
+++ b/src/content/usecases/FollowSlaveUseCase.ts
@@ -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();
+ }
+}
diff --git a/src/content/usecases/HintKeyProducer.ts b/src/content/usecases/HintKeyProducer.ts
new file mode 100644
index 0000000..241cd56
--- /dev/null
+++ b/src/content/usecases/HintKeyProducer.ts
@@ -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();
+ }
+}
+
diff --git a/src/content/usecases/KeymapUseCase.ts b/src/content/usecases/KeymapUseCase.ts
new file mode 100644
index 0000000..af0ad77
--- /dev/null
+++ b/src/content/usecases/KeymapUseCase.ts
@@ -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);
+ }
+}
diff --git a/src/content/usecases/MarkKeyUseCase.ts b/src/content/usecases/MarkKeyUseCase.ts
new file mode 100644
index 0000000..c0aa655
--- /dev/null
+++ b/src/content/usecases/MarkKeyUseCase.ts
@@ -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();
+ }
+}
diff --git a/src/content/usecases/MarkUseCase.ts b/src/content/usecases/MarkUseCase.ts
new file mode 100644
index 0000000..530f141
--- /dev/null
+++ b/src/content/usecases/MarkUseCase.ts
@@ -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);
+ }
+}
diff --git a/src/content/usecases/NavigateUseCase.ts b/src/content/usecases/NavigateUseCase.ts
new file mode 100644
index 0000000..6f82d3f
--- /dev/null
+++ b/src/content/usecases/NavigateUseCase.ts
@@ -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();
+ }
+}
diff --git a/src/content/usecases/ScrollUseCase.ts b/src/content/usecases/ScrollUseCase.ts
new file mode 100644
index 0000000..6a1f801
--- /dev/null
+++ b/src/content/usecases/ScrollUseCase.ts
@@ -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;
+ }
+}
diff --git a/src/content/usecases/SettingUseCase.ts b/src/content/usecases/SettingUseCase.ts
new file mode 100644
index 0000000..765cb45
--- /dev/null
+++ b/src/content/usecases/SettingUseCase.ts
@@ -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;
+ }
+}
diff --git a/src/shared/messages.ts b/src/shared/messages.ts
index 41b0f0b..fbd3478 100644
--- a/src/shared/messages.ts
+++ b/src/shared/messages.ts
@@ -42,162 +42,164 @@ export const SETTINGS_QUERY = 'settings.query';
export const CONSOLE_FRAME_MESSAGE = 'console.frame.message';
-interface BackgroundOperationMessage {
+export interface BackgroundOperationMessage {
type: typeof BACKGROUND_OPERATION;
operation: operations.Operation;
}
-interface ConsoleUnfocusMessage {
+export interface ConsoleUnfocusMessage {
type: typeof CONSOLE_UNFOCUS;
}
-interface ConsoleEnterCommandMessage {
+export interface ConsoleEnterCommandMessage {
type: typeof CONSOLE_ENTER_COMMAND;
text: string;
}
-interface ConsoleEnterFindMessage {
+export interface ConsoleEnterFindMessage {
type: typeof CONSOLE_ENTER_FIND;
- text: string;
+ text?: string;
}
-interface ConsoleQueryCompletionsMessage {
+export interface ConsoleQueryCompletionsMessage {
type: typeof CONSOLE_QUERY_COMPLETIONS;
text: string;
}
-interface ConsoleShowCommandMessage {
+export interface ConsoleShowCommandMessage {
type: typeof CONSOLE_SHOW_COMMAND;
command: string;
}
-interface ConsoleShowErrorMessage {
+export interface ConsoleShowErrorMessage {
type: typeof CONSOLE_SHOW_ERROR;
text: string;
}
-interface ConsoleShowInfoMessage {
+export interface ConsoleShowInfoMessage {
type: typeof CONSOLE_SHOW_INFO;
text: string;
}
-interface ConsoleShowFindMessage {
+export interface ConsoleShowFindMessage {
type: typeof CONSOLE_SHOW_FIND;
}
-interface ConsoleHideMessage {
+export interface ConsoleHideMessage {
type: typeof CONSOLE_HIDE;
}
-interface FollowStartMessage {
+export interface FollowStartMessage {
type: typeof FOLLOW_START;
newTab: boolean;
background: boolean;
}
-interface FollowRequestCountTargetsMessage {
+export interface FollowRequestCountTargetsMessage {
type: typeof FOLLOW_REQUEST_COUNT_TARGETS;
viewSize: { width: number, height: number };
framePosition: { x: number, y: number };
}
-interface FollowResponseCountTargetsMessage {
+export interface FollowResponseCountTargetsMessage {
type: typeof FOLLOW_RESPONSE_COUNT_TARGETS;
count: number;
}
-interface FollowCreateHintsMessage {
+export interface FollowCreateHintsMessage {
type: typeof FOLLOW_CREATE_HINTS;
- keysArray: string[];
- newTab: boolean;
- background: boolean;
+ tags: string[];
+ viewSize: { width: number, height: number };
+ framePosition: { x: number, y: number };
}
-interface FollowShowHintsMessage {
+export interface FollowShowHintsMessage {
type: typeof FOLLOW_SHOW_HINTS;
- keys: string;
+ prefix: string;
}
-interface FollowRemoveHintsMessage {
+export interface FollowRemoveHintsMessage {
type: typeof FOLLOW_REMOVE_HINTS;
}
-interface FollowActivateMessage {
+export interface FollowActivateMessage {
type: typeof FOLLOW_ACTIVATE;
- keys: string;
+ tag: string;
+ newTab: boolean;
+ background: boolean;
}
-interface FollowKeyPressMessage {
+export interface FollowKeyPressMessage {
type: typeof FOLLOW_KEY_PRESS;
key: string;
ctrlKey: boolean;
}
-interface MarkSetGlobalMessage {
+export interface MarkSetGlobalMessage {
type: typeof MARK_SET_GLOBAL;
key: string;
x: number;
y: number;
}
-interface MarkJumpGlobalMessage {
+export interface MarkJumpGlobalMessage {
type: typeof MARK_JUMP_GLOBAL;
key: string;
}
-interface TabScrollToMessage {
+export interface TabScrollToMessage {
type: typeof TAB_SCROLL_TO;
x: number;
y: number;
}
-interface FindNextMessage {
+export interface FindNextMessage {
type: typeof FIND_NEXT;
}
-interface FindPrevMessage {
+export interface FindPrevMessage {
type: typeof FIND_PREV;
}
-interface FindGetKeywordMessage {
+export interface FindGetKeywordMessage {
type: typeof FIND_GET_KEYWORD;
}
-interface FindSetKeywordMessage {
+export interface FindSetKeywordMessage {
type: typeof FIND_SET_KEYWORD;
keyword: string;
found: boolean;
}
-interface AddonEnabledQueryMessage {
+export interface AddonEnabledQueryMessage {
type: typeof ADDON_ENABLED_QUERY;
}
-interface AddonEnabledResponseMessage {
+export interface AddonEnabledResponseMessage {
type: typeof ADDON_ENABLED_RESPONSE;
enabled: boolean;
}
-interface AddonToggleEnabledMessage {
+export interface AddonToggleEnabledMessage {
type: typeof ADDON_TOGGLE_ENABLED;
}
-interface OpenUrlMessage {
+export interface OpenUrlMessage {
type: typeof OPEN_URL;
url: string;
newTab: boolean;
background: boolean;
}
-interface SettingsChangedMessage {
+export interface SettingsChangedMessage {
type: typeof SETTINGS_CHANGED;
}
-interface SettingsQueryMessage {
+export interface SettingsQueryMessage {
type: typeof SETTINGS_QUERY;
}
-interface ConsoleFrameMessageMessage {
+export interface ConsoleFrameMessageMessage {
type: typeof CONSOLE_FRAME_MESSAGE;
message: any;
}
diff --git a/src/shared/urls.ts b/src/shared/urls.ts
index 18349c8..bbdb1ea 100644
--- a/src/shared/urls.ts
+++ b/src/shared/urls.ts
@@ -1,3 +1,5 @@
+import { Search } from './Settings';
+
const trimStart = (str: string): string => {
// NOTE String.trimStart is available on Firefox 61
return str.replace(/^\s+/, '');
@@ -5,7 +7,7 @@ const trimStart = (str: string): string => {
const SUPPORTED_PROTOCOLS = ['http:', 'https:', 'ftp:', 'mailto:', 'about:'];
-const searchUrl = (keywords: string, searchSettings: any): string => {
+const searchUrl = (keywords: string, search: Search): string => {
try {
let u = new URL(keywords);
if (SUPPORTED_PROTOCOLS.includes(u.protocol.toLowerCase())) {
@@ -17,12 +19,12 @@ const searchUrl = (keywords: string, searchSettings: any): string => {
if (keywords.includes('.') && !keywords.includes(' ')) {
return 'http://' + keywords;
}
- let template = searchSettings.engines[searchSettings.default];
+ let template = search.engines[search.default];
let query = keywords;
let first = trimStart(keywords).split(' ')[0];
- if (Object.keys(searchSettings.engines).includes(first)) {
- template = searchSettings.engines[first];
+ if (Object.keys(search.engines).includes(first)) {
+ template = search.engines[first];
query = trimStart(trimStart(keywords).slice(first.length));
}
return template.replace('{}', encodeURIComponent(query));
diff --git a/test/content/InputDriver.test.ts b/test/content/InputDriver.test.ts
new file mode 100644
index 0000000..b9f2c28
--- /dev/null
+++ b/test/content/InputDriver.test.ts
@@ -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' }));
+ });
+});
diff --git a/test/content/actions/follow-controller.test.ts b/test/content/actions/follow-controller.test.ts
deleted file mode 100644
index a4b1710..0000000
--- a/test/content/actions/follow-controller.test.ts
+++ /dev/null
@@ -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);
- });
- });
-});
diff --git a/test/content/actions/input.test.ts b/test/content/actions/input.test.ts
deleted file mode 100644
index 33238a5..0000000
--- a/test/content/actions/input.test.ts
+++ /dev/null
@@ -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);
- });
- });
-});
diff --git a/test/content/actions/mark.test.ts b/test/content/actions/mark.test.ts
deleted file mode 100644
index 6c6d59e..0000000
--- a/test/content/actions/mark.test.ts
+++ /dev/null
@@ -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);
- });
- });
-});
diff --git a/test/content/actions/setting.test.ts b/test/content/actions/setting.test.ts
deleted file mode 100644
index c831433..0000000
--- a/test/content/actions/setting.test.ts
+++ /dev/null
@@ -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' },
- });
- });
- });
-});
diff --git a/test/content/components/common/follow.html b/test/content/components/common/follow.html
deleted file mode 100644
index b2a2d74..0000000
--- a/test/content/components/common/follow.html
+++ /dev/null
@@ -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>
diff --git a/test/content/components/common/follow.test.ts b/test/content/components/common/follow.test.ts
deleted file mode 100644
index 90d6cf5..0000000
--- a/test/content/components/common/follow.test.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import FollowComponent from 'content/components/common/follow';
-
-describe('FollowComponent', () => {
- describe('#getTargetElements', () => {
- beforeEach(() => {
- document.body.innerHTML = __html__['test/content/components/common/follow.html'];
- });
-
- it('returns visible links', () => {
- let targets = FollowComponent.getTargetElements(
- window,
- { width: window.innerWidth, height: window.innerHeight },
- { x: 0, y: 0 });
- expect(targets).to.have.lengthOf(4);
-
- let ids = Array.prototype.map.call(targets, (e) => e.id);
- expect(ids).to.include.members([
- 'visible_a',
- 'editable_div_1',
- 'editable_div_2',
- 'summary_1',
- ]);
- });
- });
-});
diff --git a/test/content/components/common/hint.test.ts b/test/content/components/common/hint.test.ts
deleted file mode 100644
index 42d571f..0000000
--- a/test/content/components/common/hint.test.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import Hint from 'content/components/common/hint';
-
-describe('Hint class', () => {
- beforeEach(() => {
- document.body.innerHTML = __html__['test/content/components/common/hint.html'];
- });
-
- describe('#constructor', () => {
- it('creates a hint element with tag name', () => {
- let link = document.getElementById('test-link');
- let hint = new Hint(link, 'abc');
- expect(hint.element.textContent.trim()).to.be.equal('abc');
- });
-
- it('throws an exception when non-element given', () => {
- expect(() => new Hint(window, 'abc')).to.throw(TypeError);
- });
- });
-
- describe('#show', () => {
- it('shows an element', () => {
- let link = document.getElementById('test-link');
- let hint = new Hint(link, 'abc');
- hint.hide();
- hint.show();
-
- expect(hint.element.style.display).to.not.equal('none');
- });
- });
-
- describe('#hide', () => {
- it('hides an element', () => {
- let link = document.getElementById('test-link');
- let hint = new Hint(link, 'abc');
- hint.hide();
-
- expect(hint.element.style.display).to.equal('none');
- });
- });
-
- describe('#remove', () => {
- it('removes an element', () => {
- let link = document.getElementById('test-link');
- let hint = new Hint(link, 'abc');
-
- expect(hint.element.parentElement).to.not.be.null;
- hint.remove();
- expect(hint.element.parentElement).to.be.null;
- });
- });
-
- describe('#activate', () => {
- // TODO test activations
- });
-});
-
-
diff --git a/test/content/components/common/input.test.ts b/test/content/components/common/input.test.ts
deleted file mode 100644
index f3a943c..0000000
--- a/test/content/components/common/input.test.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import InputComponent from 'content/components/common/input';
-
-describe('InputComponent', () => {
- it('register callbacks', () => {
- let component = new InputComponent(window.document);
- let key = { key: 'a', ctrlKey: true, shiftKey: false, altKey: false, metaKey: false };
- component.onKey((key) => {
- expect(key).to.deep.equal(key);
- });
- component.onKeyDown(key);
- });
-
- it('invoke callback once', () => {
- let component = new InputComponent(window.document);
- let a = 0, b = 0;
- component.onKey((key) => {
- if (key.key == 'a') {
- ++a;
- } else {
- key.key == 'b'
- ++b;
- }
- });
-
- let elem = document.body;
- component.onKeyDown({ key: 'a', target: elem });
- component.onKeyDown({ key: 'b', target: elem });
- component.onKeyPress({ key: 'a', target: elem });
- component.onKeyUp({ key: 'a', target: elem });
- component.onKeyPress({ key: 'b', target: elem });
- component.onKeyUp({ key: 'b', target: elem });
-
- expect(a).is.equals(1);
- expect(b).is.equals(1);
- })
-
- it('does not invoke only meta keys', () => {
- let component = new InputComponent(window.document);
- component.onKey((key) => {
- expect.fail();
- });
- component.onKeyDown({ key: 'Shift' });
- component.onKeyDown({ key: 'Control' });
- component.onKeyDown({ key: 'Alt' });
- component.onKeyDown({ key: 'OS' });
- })
-
- it('ignores events from input elements', () => {
- ['input', 'textarea', 'select'].forEach((name) => {
- let target = window.document.createElement(name);
- let component = new InputComponent(target);
- component.onKey((key) => {
- expect.fail();
- });
- component.onKeyDown({ key: 'x', target });
- });
- });
-
- it('ignores events from contenteditable elements', () => {
- let target = window.document.createElement('div');
- let component = new InputComponent(target);
- component.onKey((key) => {
- expect.fail();
- });
-
- target.setAttribute('contenteditable', '');
- component.onKeyDown({ key: 'x', target });
-
- target.setAttribute('contenteditable', 'true');
- component.onKeyDown({ key: 'x', target });
- })
-});
diff --git a/test/shared/utils/keys.test.ts b/test/content/domains/Key.test.ts
index b2ad3cb..b3f9fb6 100644
--- a/test/shared/utils/keys.test.ts
+++ b/test/content/domains/Key.test.ts
@@ -1,11 +1,12 @@
-import * as keys from 'shared/utils/keys';
+import Key, * as keys from '../../../src/content/domains/Key';
+import { expect } from 'chai'
-describe("keys util", () => {
+describe("Key", () => {
describe('fromKeyboardEvent', () => {
it('returns from keyboard input Ctrl+X', () => {
- let k = keys.fromKeyboardEvent({
- key: 'x', shiftKey: false, ctrlKey: true, altKey: false, metaKey: true
- });
+ let k = keys.fromKeyboardEvent(new KeyboardEvent('keydown', {
+ key: 'x', shiftKey: false, ctrlKey: true, altKey: false, metaKey: true,
+ }));
expect(k.key).to.equal('x');
expect(k.shiftKey).to.be.false;
expect(k.ctrlKey).to.be.true;
@@ -14,9 +15,9 @@ describe("keys util", () => {
});
it('returns from keyboard input Shift+Esc', () => {
- let k = keys.fromKeyboardEvent({
+ let k = keys.fromKeyboardEvent(new KeyboardEvent('keydown', {
key: 'Escape', shiftKey: true, ctrlKey: false, altKey: false, metaKey: true
- });
+ }));
expect(k.key).to.equal('Esc');
expect(k.shiftKey).to.be.true;
expect(k.ctrlKey).to.be.false;
@@ -26,9 +27,9 @@ describe("keys util", () => {
it('returns from keyboard input Ctrl+$', () => {
// $ required shift pressing on most keyboards
- let k = keys.fromKeyboardEvent({
+ let k = keys.fromKeyboardEvent(new KeyboardEvent('keydown', {
key: '$', shiftKey: true, ctrlKey: true, altKey: false, metaKey: false
- });
+ }));
expect(k.key).to.equal('$');
expect(k.shiftKey).to.be.false;
expect(k.ctrlKey).to.be.true;
@@ -37,9 +38,9 @@ describe("keys util", () => {
});
it('returns from keyboard input Crtl+Space', () => {
- let k = keys.fromKeyboardEvent({
+ let k = keys.fromKeyboardEvent(new KeyboardEvent('keydown', {
key: ' ', shiftKey: false, ctrlKey: true, altKey: false, metaKey: false
- });
+ }));
expect(k.key).to.equal('Space');
expect(k.shiftKey).to.be.false;
expect(k.ctrlKey).to.be.true;
@@ -122,43 +123,15 @@ describe("keys util", () => {
});
});
- describe('fromMapKeys', () => {
- it('returns mapped keys for Shift+Esc', () => {
- let keyArray = keys.fromMapKeys('<S-Esc>');
- expect(keyArray).to.have.lengthOf(1);
- expect(keyArray[0].key).to.equal('Esc');
- expect(keyArray[0].shiftKey).to.be.true;
- });
-
- it('returns mapped keys for a<C-B><A-C>d<M-e>', () => {
- let keyArray = keys.fromMapKeys('a<C-B><A-C>d<M-e>');
- expect(keyArray).to.have.lengthOf(5);
- expect(keyArray[0].key).to.equal('a');
- expect(keyArray[1].ctrlKey).to.be.true;
- expect(keyArray[1].key).to.equal('b');
- expect(keyArray[2].altKey).to.be.true;
- expect(keyArray[2].key).to.equal('c');
- expect(keyArray[3].key).to.equal('d');
- expect(keyArray[4].metaKey).to.be.true;
- expect(keyArray[4].key).to.equal('e');
- });
- })
-
describe('equals', () => {
- expect(keys.equals({
- key: 'x',
- ctrlKey: true,
- }, {
- key: 'x',
- ctrlKey: true,
- })).to.be.true;
-
- expect(keys.equals({
- key: 'X',
- shiftKey: true,
- }, {
- key: 'x',
- ctrlKey: true,
- })).to.be.false;
+ expect(keys.equals(
+ { key: 'x', ctrlKey: true, },
+ { key: 'x', ctrlKey: true, },
+ )).to.be.true;
+
+ expect(keys.equals(
+ { key: 'X', shiftKey: true, },
+ { key: 'x', ctrlKey: true, },
+ )).to.be.false;
});
});
diff --git a/test/content/domains/KeySequence.test.ts b/test/content/domains/KeySequence.test.ts
new file mode 100644
index 0000000..7387c06
--- /dev/null
+++ b/test/content/domains/KeySequence.test.ts
@@ -0,0 +1,72 @@
+import KeySequence, * as utils from '../../../src/content/domains/KeySequence';
+import { expect } from 'chai'
+
+describe("KeySequence", () => {
+ describe('#push', () => {
+ it('append a key to the sequence', () => {
+ let seq = KeySequence.from([]);
+ seq.push({ key: 'g' });
+ seq.push({ key: 'u', shiftKey: true });
+
+ let array = seq.getKeyArray();
+ expect(array[0]).to.deep.equal({ key: 'g' });
+ expect(array[1]).to.deep.equal({ key: 'u', shiftKey: true });
+ })
+ });
+
+ describe('#startsWith', () => {
+ it('returns true if the key sequence starts with param', () => {
+ let seq = KeySequence.from([
+ { key: 'g' },
+ { key: 'u', shiftKey: true },
+ ]);
+
+ expect(seq.startsWith(KeySequence.from([
+ ]))).to.be.true;
+ expect(seq.startsWith(KeySequence.from([
+ { key: 'g' },
+ ]))).to.be.true;
+ expect(seq.startsWith(KeySequence.from([
+ { key: 'g' }, { key: 'u', shiftKey: true },
+ ]))).to.be.true;
+ expect(seq.startsWith(KeySequence.from([
+ { key: 'g' }, { key: 'u', shiftKey: true }, { key: 'x' },
+ ]))).to.be.false;
+ expect(seq.startsWith(KeySequence.from([
+ { key: 'h' },
+ ]))).to.be.false;
+ })
+
+ it('returns true if the empty sequence starts with an empty sequence', () => {
+ let seq = KeySequence.from([]);
+
+ expect(seq.startsWith(KeySequence.from([]))).to.be.true;
+ expect(seq.startsWith(KeySequence.from([
+ { key: 'h' },
+ ]))).to.be.false;
+ })
+ });
+
+ describe('#fromMapKeys', () => {
+ it('returns mapped keys for Shift+Esc', () => {
+ let keyArray = utils.fromMapKeys('<S-Esc>').getKeyArray();
+ expect(keyArray).to.have.lengthOf(1);
+ expect(keyArray[0].key).to.equal('Esc');
+ expect(keyArray[0].shiftKey).to.be.true;
+ });
+
+ it('returns mapped keys for a<C-B><A-C>d<M-e>', () => {
+ let keyArray = utils.fromMapKeys('a<C-B><A-C>d<M-e>').getKeyArray();
+ expect(keyArray).to.have.lengthOf(5);
+ expect(keyArray[0].key).to.equal('a');
+ expect(keyArray[1].ctrlKey).to.be.true;
+ expect(keyArray[1].key).to.equal('b');
+ expect(keyArray[2].altKey).to.be.true;
+ expect(keyArray[2].key).to.equal('c');
+ expect(keyArray[3].key).to.equal('d');
+ expect(keyArray[4].metaKey).to.be.true;
+ expect(keyArray[4].key).to.equal('e');
+ });
+ })
+
+});
diff --git a/test/content/mock/MockConsoleClient.ts b/test/content/mock/MockConsoleClient.ts
new file mode 100644
index 0000000..8de2d83
--- /dev/null
+++ b/test/content/mock/MockConsoleClient.ts
@@ -0,0 +1,26 @@
+import ConsoleClient from '../../../src/content/client/ConsoleClient';
+
+export default class MockConsoleClient implements ConsoleClient {
+ public isError: boolean;
+
+ public text: string;
+
+ constructor() {
+ this.isError = false;
+ this.text = '';
+ }
+
+ info(text: string): Promise<void> {
+ this.isError = false;
+ this.text = text;
+ return Promise.resolve();
+ }
+
+ error(text: string): Promise<void> {
+ this.isError = true;
+ this.text = text;
+ return Promise.resolve();
+ }
+}
+
+
diff --git a/test/content/mock/MockScrollPresenter.ts b/test/content/mock/MockScrollPresenter.ts
new file mode 100644
index 0000000..819569a
--- /dev/null
+++ b/test/content/mock/MockScrollPresenter.ts
@@ -0,0 +1,47 @@
+import ScrollPresenter, { Point } from '../../../src/content/presenters/ScrollPresenter';
+
+export default class MockScrollPresenter implements ScrollPresenter {
+ private pos: Point;
+
+ constructor() {
+ this.pos = { x: 0, y: 0 };
+ }
+
+ getScroll(): Point {
+ return this.pos;
+ }
+
+ scrollVertically(amount: number, _smooth: boolean): void {
+ this.pos.y += amount;
+ }
+
+ scrollHorizonally(amount: number, _smooth: boolean): void {
+ this.pos.x += amount;
+ }
+
+ scrollPages(amount: number, _smooth: boolean): void {
+ this.pos.x += amount;
+ }
+
+ scrollTo(x: number, y: number, _smooth: boolean): void {
+ this.pos.x = x;
+ this.pos.y = y;
+ }
+
+ scrollToTop(_smooth: boolean): void {
+ this.pos.y = 0;
+ }
+
+ scrollToBottom(_smooth: boolean): void {
+ this.pos.y = Infinity;
+ }
+
+ scrollToHome(_smooth: boolean): void {
+ this.pos.x = 0;
+ }
+
+ scrollToEnd(_smooth: boolean): void {
+ this.pos.x = Infinity;
+ }
+}
+
diff --git a/test/content/components/common/hint.html b/test/content/presenters/Hint.test.html
index b50c5fe..b50c5fe 100644
--- a/test/content/components/common/hint.html
+++ b/test/content/presenters/Hint.test.html
diff --git a/test/content/presenters/Hint.test.ts b/test/content/presenters/Hint.test.ts
new file mode 100644
index 0000000..7994788
--- /dev/null
+++ b/test/content/presenters/Hint.test.ts
@@ -0,0 +1,158 @@
+import AbstractHint, { LinkHint, InputHint } from '../../../src/content/presenters/Hint';
+import { expect } from 'chai';
+
+class Hint extends AbstractHint {
+}
+
+describe('Hint', () => {
+ beforeEach(() => {
+ document.body.innerHTML = `<a id='test-link' href='#'>link</a>`;
+ });
+
+ describe('#constructor', () => {
+ it('creates a hint element with tag name', () => {
+ let link = document.getElementById('test-link');
+ let hint = new Hint(link, 'abc');
+
+ let elem = document.querySelector('.vimvixen-hint');
+ expect(elem.textContent.trim()).to.be.equal('abc');
+ });
+ });
+
+ describe('#show', () => {
+ it('shows an element', () => {
+ let link = document.getElementById('test-link');
+ let hint = new Hint(link, 'abc');
+ hint.hide();
+ hint.show();
+
+ let elem = document.querySelector('.vimvixen-hint') as HTMLElement;
+ expect(elem.style.display).to.not.equal('none');
+ });
+ });
+
+ describe('#hide', () => {
+ it('hides an element', () => {
+ let link = document.getElementById('test-link') as HTMLElement;
+ let hint = new Hint(link, 'abc');
+ hint.hide();
+
+ let elem = document.querySelector('.vimvixen-hint') as HTMLElement;
+ expect(elem.style.display).to.equal('none');
+ });
+ });
+
+ describe('#remove', () => {
+ it('removes an element', () => {
+ let link = document.getElementById('test-link');
+ let hint = new Hint(link, 'abc');
+
+ let elem = document.querySelector('.vimvixen-hint');
+ expect(elem.parentElement).to.not.be.null;
+ hint.remove();
+ expect(elem.parentElement).to.be.null;
+ });
+ });
+});
+
+describe('LinkHint', () => {
+ beforeEach(() => {
+ document.body.innerHTML = `
+<a id='test-link1' href='https://google.com/'>link</a>
+<a id='test-link2' href='https://yahoo.com/' target='_blank'>link</a>
+<a id='test-link3' href='#' target='_blank'>link</a>
+`;
+ });
+
+ describe('#getLink()', () => {
+ it('returns value of "href" attribute', () => {
+ let link = document.getElementById('test-link1') as HTMLAnchorElement;
+ let hint = new LinkHint(link, 'abc');
+
+ expect(hint.getLink()).to.equal('https://google.com/');
+ });
+ });
+
+ describe('#getLinkTarget()', () => {
+ it('returns value of "target" attribute', () => {
+ let link = document.getElementById('test-link1') as HTMLAnchorElement;
+ let hint = new LinkHint(link, 'abc');
+
+ expect(hint.getLinkTarget()).to.be.null;
+
+ link = document.getElementById('test-link2') as HTMLAnchorElement;
+ hint = new LinkHint(link, 'abc');
+
+ expect(hint.getLinkTarget()).to.equal('_blank');
+ });
+ });
+
+ describe('#click()', () => {
+ it('clicks a element', (done) => {
+ let link = document.getElementById('test-link3') as HTMLAnchorElement;
+ let hint = new LinkHint(link, 'abc');
+ link.onclick = () => { done() };
+
+ hint.click();
+ });
+ });
+});
+
+describe('InputHint', () => {
+ describe('#activate()', () => {
+ context('<input>', () => {
+ beforeEach(() => {
+ document.body.innerHTML = `<input id='test-input'></input>`;
+ });
+
+ it('focuses to the input', () => {
+ let input = document.getElementById('test-input') as HTMLInputElement;
+ let hint = new InputHint(input, 'abc');
+ hint.activate();
+
+ expect(document.activeElement).to.equal(input);
+ });
+ });
+
+ context('<input type="checkbox">', () => {
+ beforeEach(() => {
+ document.body.innerHTML = `<input type="checkbox" id='test-input'></input>`;
+ });
+
+ it('checks and focuses to the input', () => {
+ let input = document.getElementById('test-input') as HTMLInputElement;
+ let hint = new InputHint(input, 'abc');
+ hint.activate();
+
+ expect(input.checked).to.be.true;
+ });
+ });
+ context('<textarea>', () => {
+ beforeEach(() => {
+ document.body.innerHTML = `<textarea id='test-textarea'></textarea>`;
+ });
+
+ it('focuses to the textarea', () => {
+ let textarea = document.getElementById('test-textarea') as HTMLTextAreaElement;
+ let hint = new InputHint(textarea, 'abc');
+ hint.activate();
+
+ expect(document.activeElement).to.equal(textarea);
+ });
+ });
+
+ context('<button>', () => {
+ beforeEach(() => {
+ document.body.innerHTML = `<button id='test-button'></button>`;
+ });
+
+ it('clicks the button', (done) => {
+ let button = document.getElementById('test-button') as HTMLButtonElement;
+ button.onclick = () => { done() };
+
+ let hint = new InputHint(button, 'abc');
+ hint.activate();
+ });
+ });
+ });
+});
diff --git a/test/content/navigates.test.ts b/test/content/presenters/NavigationPresenter.test.ts
index 1d73344..c1aca9a 100644
--- a/test/content/navigates.test.ts
+++ b/test/content/presenters/NavigationPresenter.test.ts
@@ -1,19 +1,26 @@
-import * as navigates from 'content/navigates';
-
-const testRel = (done, rel, html) => {
- const method = rel === 'prev' ? 'linkPrev' : 'linkNext';
- document.body.innerHTML = html;
- navigates[method](window);
- setTimeout(() => {
- expect(document.location.hash).to.equal(`#${rel}`);
- done();
- }, 0);
-};
-
-const testPrev = html => done => testRel(done, 'prev', html);
-const testNext = html => done => testRel(done, 'next', html);
-
-describe('navigates module', () => {
+import NavigationPresenter, { NavigationPresenterImpl }
+ from '../../../src/content/presenters/NavigationPresenter';
+import { expect } from 'chai';
+
+describe('NavigationPresenter', () => {
+ let sut;
+
+ const testRel = (done, rel, html) => {
+ const method = rel === 'prev' ? sut.openLinkPrev.bind(sut) : sut.openLinkNext.bind(sut);
+ document.body.innerHTML = html;
+ method();
+ setTimeout(() => {
+ expect(document.location.hash).to.equal(`#${rel}`);
+ done();
+ }, 0);
+ };
+ const testPrev = html => done => testRel(done, 'prev', html);
+ const testNext = html => done => testRel(done, 'next', html);
+
+ before(() => {
+ sut = new NavigationPresenterImpl();
+ });
+
describe('#linkPrev', () => {
it('navigates to <link> elements whose rel attribute is "prev"', testPrev(
'<link rel="prev" href="#prev" />'
@@ -130,7 +137,7 @@ describe('navigates module', () => {
// NOTE: not able to test location
it('removes hash', () => {
window.location.hash = '#section-1';
- navigates.parent(window);
+ sut.openParent();
expect(document.location.hash).to.be.empty;
});
});
diff --git a/test/content/reducers/addon.test.ts b/test/content/reducers/addon.test.ts
deleted file mode 100644
index fb05244..0000000
--- a/test/content/reducers/addon.test.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import * as actions from 'content/actions';
-import addonReducer from 'content/reducers/addon';
-
-describe("addon reducer", () => {
- it('return the initial state', () => {
- let state = addonReducer(undefined, {});
- expect(state).to.have.property('enabled', true);
- });
-
- it('return next state for ADDON_SET_ENABLED', () => {
- let action = { type: actions.ADDON_SET_ENABLED, enabled: true };
- let prev = { enabled: false };
- let state = addonReducer(prev, action);
-
- expect(state.enabled).is.equal(true);
- });
-});
diff --git a/test/content/reducers/find.test.ts b/test/content/reducers/find.test.ts
deleted file mode 100644
index 66a2c67..0000000
--- a/test/content/reducers/find.test.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import * as actions from 'content/actions';
-import findReducer from 'content/reducers/find';
-
-describe("find reducer", () => {
- it('return the initial state', () => {
- let state = findReducer(undefined, {});
- expect(state).to.have.property('keyword', null);
- expect(state).to.have.property('found', false);
- });
-
- it('return next state for FIND_SET_KEYWORD', () => {
- let action = {
- type: actions.FIND_SET_KEYWORD,
- keyword: 'xyz',
- found: true,
- };
- let state = findReducer({}, action);
-
- expect(state.keyword).is.equal('xyz');
- expect(state.found).to.be.true;
- });
-});
diff --git a/test/content/reducers/follow-controller.test.ts b/test/content/reducers/follow-controller.test.ts
deleted file mode 100644
index 39f326c..0000000
--- a/test/content/reducers/follow-controller.test.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import * as actions from 'content/actions';
-import followControllerReducer from 'content/reducers/follow-controller';
-
-describe('follow-controller reducer', () => {
- it ('returns the initial state', () => {
- let state = followControllerReducer(undefined, {});
- expect(state).to.have.property('enabled', false);
- expect(state).to.have.property('newTab');
- expect(state).to.have.deep.property('keys', '');
- });
-
- it ('returns next state for FOLLOW_CONTROLLER_ENABLE', () => {
- let action = { type: actions.FOLLOW_CONTROLLER_ENABLE, newTab: true };
- let state = followControllerReducer({ enabled: false, newTab: false }, action);
- expect(state).to.have.property('enabled', true);
- expect(state).to.have.property('newTab', true);
- expect(state).to.have.property('keys', '');
- });
-
- it ('returns next state for FOLLOW_CONTROLLER_DISABLE', () => {
- let action = { type: actions.FOLLOW_CONTROLLER_DISABLE };
- let state = followControllerReducer({ enabled: true }, action);
- expect(state).to.have.property('enabled', false);
- });
-
- it ('returns next state for FOLLOW_CONTROLLER_KEY_PRESS', () => {
- let action = { type: actions.FOLLOW_CONTROLLER_KEY_PRESS, key: 'a'};
- let state = followControllerReducer({ keys: '' }, action);
- expect(state).to.have.deep.property('keys', 'a');
-
- action = { type: actions.FOLLOW_CONTROLLER_KEY_PRESS, key: 'b'};
- state = followControllerReducer(state, action);
- expect(state).to.have.deep.property('keys', 'ab');
- });
-
- it ('returns next state for FOLLOW_CONTROLLER_BACKSPACE', () => {
- let action = { type: actions.FOLLOW_CONTROLLER_BACKSPACE };
- let state = followControllerReducer({ keys: 'ab' }, action);
- expect(state).to.have.deep.property('keys', 'a');
-
- state = followControllerReducer(state, action);
- expect(state).to.have.deep.property('keys', '');
-
- state = followControllerReducer(state, action);
- expect(state).to.have.deep.property('keys', '');
- });
-});
diff --git a/test/content/reducers/input.test.ts b/test/content/reducers/input.test.ts
deleted file mode 100644
index f892201..0000000
--- a/test/content/reducers/input.test.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import * as actions from 'content/actions';
-import inputReducer from 'content/reducers/input';
-
-describe("input reducer", () => {
- it('return the initial state', () => {
- let state = inputReducer(undefined, {});
- expect(state).to.have.deep.property('keys', []);
- });
-
- it('return next state for INPUT_KEY_PRESS', () => {
- let action = { type: actions.INPUT_KEY_PRESS, key: 'a' };
- let state = inputReducer(undefined, action);
- expect(state).to.have.deep.property('keys', ['a']);
-
- action = { type: actions.INPUT_KEY_PRESS, key: 'b' };
- state = inputReducer(state, action);
- expect(state).to.have.deep.property('keys', ['a', 'b']);
- });
-
- it('return next state for INPUT_CLEAR_KEYS', () => {
- let action = { type: actions.INPUT_CLEAR_KEYS };
- let state = inputReducer({ keys: [1, 2, 3] }, action);
- expect(state).to.have.deep.property('keys', []);
- });
-});
diff --git a/test/content/reducers/mark.test.ts b/test/content/reducers/mark.test.ts
deleted file mode 100644
index 1a51c3e..0000000
--- a/test/content/reducers/mark.test.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import * as actions from 'content/actions';
-import reducer from 'content/reducers/mark';
-
-describe("mark reducer", () => {
- it('return the initial state', () => {
- let state = reducer(undefined, {});
- expect(state.setMode).to.be.false;
- expect(state.jumpMode).to.be.false;
- expect(state.marks).to.be.empty;
- });
-
- it('starts set mode', () => {
- let action = { type: actions.MARK_START_SET };
- let state = reducer(undefined, action);
- expect(state.setMode).to.be.true;
- });
-
- it('starts jump mode', () => {
- let action = { type: actions.MARK_START_JUMP };
- let state = reducer(undefined, action);
- expect(state.jumpMode).to.be.true;
- });
-
- it('cancels set and jump mode', () => {
- let action = { type: actions.MARK_CANCEL };
- let state = reducer({ setMode: true }, action);
- expect(state.setMode).to.be.false;
-
- state = reducer({ jumpMode: true }, action);
- expect(state.jumpMode).to.be.false;
- });
-
- it('stores local mark', () => {
- let action = { type: actions.MARK_SET_LOCAL, key: 'a', x: 20, y: 30};
- let state = reducer({ setMode: true }, action);
- expect(state.setMode).to.be.false;
- expect(state.marks['a']).to.be.an('object')
- expect(state.marks['a'].x).to.equal(20)
- expect(state.marks['a'].y).to.equal(30)
- });
-});
diff --git a/test/content/reducers/setting.test.ts b/test/content/reducers/setting.test.ts
deleted file mode 100644
index 9b332aa..0000000
--- a/test/content/reducers/setting.test.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import * as actions from 'content/actions';
-import settingReducer from 'content/reducers/setting';
-
-describe("content setting reducer", () => {
- it('return the initial state', () => {
- let state = settingReducer(undefined, {});
- expect(state.keymaps).to.be.empty;
- });
-
- it('return next state for SETTING_SET', () => {
- let newSettings = { red: 'apple', yellow: 'banana' };
- let action = {
- type: actions.SETTING_SET,
- settings: {
- keymaps: {
- "zz": { type: "zoom.neutral" },
- "<S-Esc>": { "type": "addon.toggle.enabled" }
- },
- "blacklist": []
- }
- }
- let state = settingReducer(undefined, action);
- expect(state.keymaps).to.have.deep.all.members([
- { key: [{ key: 'z', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false },
- { key: 'z', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }],
- op: { type: 'zoom.neutral' }},
- { key: [{ key: 'Esc', shiftKey: true, ctrlKey: false, altKey: false, metaKey: false }],
- op: { type: 'addon.toggle.enabled' }},
- ]);
- });
-});
diff --git a/test/content/repositories/AddonEnabledRepository.test.ts b/test/content/repositories/AddonEnabledRepository.test.ts
new file mode 100644
index 0000000..3cea897
--- /dev/null
+++ b/test/content/repositories/AddonEnabledRepository.test.ts
@@ -0,0 +1,15 @@
+import { AddonEnabledRepositoryImpl } from '../../../src/content/repositories/AddonEnabledRepository';
+import { expect } from 'chai';
+
+describe('AddonEnabledRepositoryImpl', () => {
+ it('updates and gets current value', () => {
+ let sut = new AddonEnabledRepositoryImpl();
+
+ sut.set(true);
+ expect(sut.get()).to.be.true;
+
+ sut.set(false);
+ expect(sut.get()).to.be.false;
+ });
+});
+
diff --git a/test/content/repositories/FindRepository.test.ts b/test/content/repositories/FindRepository.test.ts
new file mode 100644
index 0000000..dcb2dff
--- /dev/null
+++ b/test/content/repositories/FindRepository.test.ts
@@ -0,0 +1,15 @@
+import { FindRepositoryImpl } from '../../../src/content/repositories/FindRepository';
+import { expect } from 'chai';
+
+describe('FindRepositoryImpl', () => {
+ it('updates and gets last keyword', () => {
+ let sut = new FindRepositoryImpl();
+
+ expect(sut.getLastKeyword()).to.be.null;
+
+ sut.setLastKeyword('monkey');
+
+ expect(sut.getLastKeyword()).to.equal('monkey');
+ });
+});
+
diff --git a/test/content/repositories/FollowKeyRepository.test.ts b/test/content/repositories/FollowKeyRepository.test.ts
new file mode 100644
index 0000000..eae58b9
--- /dev/null
+++ b/test/content/repositories/FollowKeyRepository.test.ts
@@ -0,0 +1,31 @@
+import FollowKeyRepository, { FollowKeyRepositoryImpl }
+ from '../../../src/content/repositories/FollowKeyRepository';
+import { expect } from 'chai';
+
+describe('FollowKeyRepositoryImpl', () => {
+ let sut: FollowKeyRepository;
+
+ before(() => {
+ sut = new FollowKeyRepositoryImpl();
+ });
+
+ describe('#getKeys()/#pushKey()/#popKey()', () => {
+ it('enqueues keys', () => {
+ expect(sut.getKeys()).to.be.empty;
+
+ sut.pushKey('a');
+ sut.pushKey('b');
+ sut.pushKey('c');
+ expect(sut.getKeys()).to.deep.equal(['a', 'b', 'c']);
+
+ sut.popKey();
+ expect(sut.getKeys()).to.deep.equal(['a', 'b']);
+
+ sut.clearKeys();
+ expect(sut.getKeys()).to.be.empty;
+ });
+ });
+});
+
+
+
diff --git a/test/content/repositories/FollowMasterRepository.test.ts b/test/content/repositories/FollowMasterRepository.test.ts
new file mode 100644
index 0000000..8c3f34e
--- /dev/null
+++ b/test/content/repositories/FollowMasterRepository.test.ts
@@ -0,0 +1,49 @@
+import FollowMasterRepository, { FollowMasterRepositoryImpl }
+ from '../../../src/content/repositories/FollowMasterRepository';
+import { expect } from 'chai';
+
+describe('FollowMasterRepositoryImpl', () => {
+ let sut: FollowMasterRepository;
+
+ before(() => {
+ sut = new FollowMasterRepositoryImpl();
+ });
+
+ describe('#getTags()/#addTag()/#clearTags()', () => {
+ it('gets, adds and clears tags', () => {
+ expect(sut.getTags()).to.be.empty;
+
+ sut.addTag('a');
+ sut.addTag('b');
+ sut.addTag('c');
+ expect(sut.getTags()).to.deep.equal(['a', 'b', 'c']);
+
+ sut.clearTags();
+ expect(sut.getTags()).to.be.empty;
+ });
+ });
+
+ describe('#getTagsByPrefix', () => {
+ it('gets tags matched by prefix', () => {
+ for (let tag of ['a', 'aa', 'ab', 'b', 'ba', 'bb']) {
+ sut.addTag(tag);
+ }
+ expect(sut.getTagsByPrefix('a')).to.deep.equal(['a', 'aa', 'ab']);
+ expect(sut.getTagsByPrefix('aa')).to.deep.equal(['aa']);
+ expect(sut.getTagsByPrefix('b')).to.deep.equal(['b', 'ba', 'bb']);
+ expect(sut.getTagsByPrefix('c')).to.be.empty;
+ });
+ });
+
+ describe('#setCurrentFollowMode()/#getCurrentNewTabMode()/#getCurrentBackgroundMode', () => {
+ it('updates and gets follow mode', () => {
+ sut.setCurrentFollowMode(false, true);
+ expect(sut.getCurrentNewTabMode()).to.be.false;
+ expect(sut.getCurrentBackgroundMode()).to.be.true;
+
+ sut.setCurrentFollowMode(true, false);
+ expect(sut.getCurrentNewTabMode()).to.be.true;
+ expect(sut.getCurrentBackgroundMode()).to.be.false;
+ });
+ });
+});
diff --git a/test/content/repositories/FollowSlaveRepository.test.ts b/test/content/repositories/FollowSlaveRepository.test.ts
new file mode 100644
index 0000000..10cf094
--- /dev/null
+++ b/test/content/repositories/FollowSlaveRepository.test.ts
@@ -0,0 +1,24 @@
+import FollowSlaveRepository, { FollowSlaveRepositoryImpl }
+ from '../../../src/content/repositories/FollowSlaveRepository';
+import { expect } from 'chai';
+
+describe('FollowSlaveRepository', () => {
+ let sut: FollowSlaveRepository;
+
+ before(() => {
+ sut = new FollowSlaveRepositoryImpl();
+ });
+
+ describe('#isFollowMode()/#enableFollowMode()/#disableFollowMode()', () => {
+ it('gets, adds updates follow mode', () => {
+ expect(sut.isFollowMode()).to.be.false;
+
+ sut.enableFollowMode();
+ expect(sut.isFollowMode()).to.be.true;
+
+ sut.disableFollowMode();
+ expect(sut.isFollowMode()).to.be.false;
+ });
+ });
+});
+
diff --git a/test/content/repositories/KeymapRepository.test.ts b/test/content/repositories/KeymapRepository.test.ts
new file mode 100644
index 0000000..34704d9
--- /dev/null
+++ b/test/content/repositories/KeymapRepository.test.ts
@@ -0,0 +1,37 @@
+import KeymapRepository, { KeymapRepositoryImpl }
+ from '../../../src/content/repositories/KeymapRepository';
+import { expect } from 'chai';
+
+describe('KeymapRepositoryImpl', () => {
+ let sut: KeymapRepository;
+
+ before(() => {
+ sut = new KeymapRepositoryImpl();
+ });
+
+ describe('#enqueueKey()', () => {
+ it('enqueues keys', () => {
+ sut.enqueueKey({ key: 'a' });
+ sut.enqueueKey({ key: 'b' });
+ let sequence = sut.enqueueKey({ key: 'c' });
+
+ expect(sequence.getKeyArray()).deep.equals([
+ { key: 'a' }, { key: 'b' }, { key: 'c' },
+ ]);
+ });
+ });
+
+ describe('#clear()', () => {
+ it('clears keys', () => {
+ sut.enqueueKey({ key: 'a' });
+ sut.enqueueKey({ key: 'b' });
+ sut.enqueueKey({ key: 'c' });
+ sut.clear();
+
+ let sequence = sut.enqueueKey({ key: 'a' });
+ expect(sequence.length()).to.equal(1);
+ });
+ });
+});
+
+
diff --git a/test/content/repositories/MarkKeyRepository.test.ts b/test/content/repositories/MarkKeyRepository.test.ts
new file mode 100644
index 0000000..8592332
--- /dev/null
+++ b/test/content/repositories/MarkKeyRepository.test.ts
@@ -0,0 +1,36 @@
+import MarkRepository, { MarkKeyRepositoryImpl }
+ from '../../../src/content/repositories/MarkKeyRepository';
+import { expect } from 'chai';
+
+describe('MarkKeyRepositoryImpl', () => {
+ let sut: MarkRepository;
+
+ before(() => {
+ sut = new MarkKeyRepositoryImpl();
+ })
+
+ describe('#isSetMode/#enableSetMode/#disabeSetMode', () => {
+ it('enables and disables set mode', () => {
+ expect(sut.isSetMode()).to.be.false;
+
+ sut.enableSetMode();
+ expect(sut.isSetMode()).to.be.true;
+
+ sut.disabeSetMode();
+ expect(sut.isSetMode()).to.be.false;
+ });
+ });
+
+ describe('#isJumpMode/#enableJumpMode/#disabeJumpMode', () => {
+ it('enables and disables jump mode', () => {
+ expect(sut.isJumpMode()).to.be.false;
+
+ sut.enableJumpMode();
+ expect(sut.isJumpMode()).to.be.true;
+
+ sut.disabeJumpMode();
+ expect(sut.isJumpMode()).to.be.false;
+ });
+ });
+});
+
diff --git a/test/content/repositories/MarkRepository.test.ts b/test/content/repositories/MarkRepository.test.ts
new file mode 100644
index 0000000..7fced5f
--- /dev/null
+++ b/test/content/repositories/MarkRepository.test.ts
@@ -0,0 +1,13 @@
+import { MarkRepositoryImpl } from '../../../src/content/repositories/MarkRepository';
+import { expect } from 'chai';
+
+describe('MarkRepositoryImpl', () => {
+ it('save and load marks', () => {
+ let sut = new MarkRepositoryImpl();
+
+ sut.set('a', { x: 10, y: 20 });
+ expect(sut.get('a')).to.deep.equal({ x: 10, y: 20 });
+ expect(sut.get('b')).to.be.null;
+ });
+});
+
diff --git a/test/content/repositories/SettingRepository.test.ts b/test/content/repositories/SettingRepository.test.ts
new file mode 100644
index 0000000..fea70b7
--- /dev/null
+++ b/test/content/repositories/SettingRepository.test.ts
@@ -0,0 +1,30 @@
+import { SettingRepositoryImpl } from '../../../src/content/repositories/SettingRepository';
+import { expect } from 'chai';
+
+describe('SettingRepositoryImpl', () => {
+ it('updates and gets current value', () => {
+ let sut = new SettingRepositoryImpl();
+
+ let settings = {
+ keymaps: {},
+ search: {
+ default: 'google',
+ engines: {
+ google: 'https://google.com/?q={}',
+ }
+ },
+ properties: {
+ hintchars: 'abcd1234',
+ smoothscroll: false,
+ complete: 'sbh',
+ },
+ blacklist: [],
+ }
+
+ sut.set(settings);
+
+ let actual = sut.get();
+ expect(actual.properties.hintchars).to.equal('abcd1234');
+ });
+});
+
diff --git a/test/content/usecases/AddonEnabledUseCase.test.ts b/test/content/usecases/AddonEnabledUseCase.test.ts
new file mode 100644
index 0000000..912bddf
--- /dev/null
+++ b/test/content/usecases/AddonEnabledUseCase.test.ts
@@ -0,0 +1,90 @@
+import AddonEnabledRepository from '../../../src/content/repositories/AddonEnabledRepository';
+import AddonEnabledUseCase from '../../../src/content/usecases/AddonEnabledUseCase';
+import AddonIndicatorClient from '../../../src/content/client/AddonIndicatorClient';
+import { expect } from 'chai';
+
+class MockAddonEnabledRepository implements AddonEnabledRepository {
+ private enabled: boolean;
+
+ constructor(init: boolean) {
+ this.enabled = init;
+ }
+
+ set(on: boolean): void {
+ this.enabled = on;
+ }
+
+ get(): boolean {
+ return this.enabled;
+ }
+}
+
+class MockAddonIndicatorClient implements AddonIndicatorClient {
+ public enabled: boolean;
+
+ constructor(init: boolean) {
+ this.enabled = init;
+ }
+
+ async setEnabled(enabled: boolean): Promise<void> {
+ this.enabled = enabled;
+ return
+ }
+}
+
+describe('AddonEnabledUseCase', () => {
+ let repository: MockAddonEnabledRepository;
+ let indicator: MockAddonIndicatorClient;
+ let sut: AddonEnabledUseCase;
+
+ beforeEach(() => {
+ repository = new MockAddonEnabledRepository(true);
+ indicator = new MockAddonIndicatorClient(false);
+ sut = new AddonEnabledUseCase({ repository, indicator });
+ });
+
+ describe('#enable', () => {
+ it('store and indicate as enabled', async() => {
+ await sut.enable();
+
+ expect(repository.get()).to.be.true;
+ expect(indicator.enabled).to.be.true;
+ });
+ });
+
+ describe('#disable', async() => {
+ it('store and indicate as disabled', async() => {
+ await sut.disable();
+
+ expect(repository.get()).to.be.false;
+ expect(indicator.enabled).to.be.false;
+ });
+ });
+
+ describe('#toggle', () => {
+ it('toggled enabled and disabled', async() => {
+ repository.set(true);
+ await sut.toggle();
+
+ expect(repository.get()).to.be.false;
+ expect(indicator.enabled).to.be.false;
+
+ repository.set(false);
+
+ await sut.toggle();
+
+ expect(repository.get()).to.be.true;
+ expect(indicator.enabled).to.be.true;
+ });
+ });
+
+ describe('#getEnabled', () => {
+ it('returns current addon enabled', () => {
+ repository.set(true);
+ expect(sut.getEnabled()).to.be.true;
+
+ repository.set(false);
+ expect(sut.getEnabled()).to.be.false;
+ });
+ });
+});
diff --git a/test/content/usecases/ClipboardUseCase.test.ts b/test/content/usecases/ClipboardUseCase.test.ts
new file mode 100644
index 0000000..862ee8a
--- /dev/null
+++ b/test/content/usecases/ClipboardUseCase.test.ts
@@ -0,0 +1,76 @@
+import ClipboardRepository from '../../../src/content/repositories/ClipboardRepository';
+import TabsClient from '../../../src/content/client/TabsClient';
+import MockConsoleClient from '../mock/MockConsoleClient';
+import ClipboardUseCase from '../../../src/content/usecases/ClipboardUseCase';
+import { expect } from 'chai';
+
+class MockClipboardRepository implements ClipboardRepository {
+ public clipboard: string;
+
+ constructor() {
+ this.clipboard = '';
+ }
+
+ read(): string {
+ return this.clipboard;
+ }
+
+ write(text: string): void {
+ this.clipboard = text;
+ }
+}
+
+class MockTabsClient implements TabsClient {
+ public last: string;
+
+ constructor() {
+ this.last = '';
+ }
+
+ openUrl(url: string, _newTab: boolean): Promise<void> {
+ this.last = url;
+ return Promise.resolve();
+ }
+}
+
+describe('ClipboardUseCase', () => {
+ let repository: MockClipboardRepository;
+ let client: MockTabsClient;
+ let consoleClient: MockConsoleClient;
+ let sut: ClipboardUseCase;
+
+ beforeEach(() => {
+ repository = new MockClipboardRepository();
+ client = new MockTabsClient();
+ consoleClient = new MockConsoleClient();
+ sut = new ClipboardUseCase({ repository, client: client, consoleClient });
+ });
+
+ describe('#yankCurrentURL', () => {
+ it('yanks current url', async () => {
+ let yanked = await sut.yankCurrentURL();
+
+ expect(yanked).to.equal(window.location.href);
+ expect(repository.clipboard).to.equal(yanked);
+ expect(consoleClient.text).to.equal('Yanked ' + yanked);
+ });
+ });
+
+ describe('#openOrSearch', () => {
+ it('opens url from the clipboard', async () => {
+ let url = 'https://github.com/ueokande/vim-vixen'
+ repository.clipboard = url;
+ await sut.openOrSearch(true);
+
+ expect(client.last).to.equal(url);
+ });
+
+ it('opens search results from the clipboard', async () => {
+ repository.clipboard = 'banana';
+ await sut.openOrSearch(true);
+
+ expect(client.last).to.equal('https://google.com/search?q=banana');
+ });
+ });
+});
+
diff --git a/test/content/usecases/FindUseCase.test.ts b/test/content/usecases/FindUseCase.test.ts
new file mode 100644
index 0000000..c7bfd39
--- /dev/null
+++ b/test/content/usecases/FindUseCase.test.ts
@@ -0,0 +1,161 @@
+import FindRepository from '../../../src/content/repositories/FindRepository';
+import FindPresenter from '../../../src/content/presenters/FindPresenter';
+import FindClient from '../../../src/content/client/FindClient';
+import FindUseCase from '../../../src/content/usecases/FindUseCase';
+import MockConsoleClient from '../mock/MockConsoleClient';
+import { expect } from 'chai';
+
+class MockFindRepository implements FindRepository {
+ public keyword: string | null;
+
+ constructor() {
+ this.keyword = null;
+ }
+
+ getLastKeyword(): string | null {
+ return this.keyword;
+ }
+
+ setLastKeyword(keyword: string): void {
+ this.keyword = keyword;
+ }
+}
+
+class MockFindPresenter implements FindPresenter {
+ public document: string;
+
+ public highlighted: boolean;
+
+ constructor() {
+ this.document = '';
+ this.highlighted = false;
+ }
+
+ find(keyword: string, _backward: boolean): boolean {
+ let found = this.document.includes(keyword);
+ this.highlighted = found;
+ return found;
+ }
+
+ clearSelection(): void {
+ this.highlighted = false;
+ }
+}
+
+class MockFindClient implements FindClient {
+ public keyword: string | null;
+
+ constructor() {
+ this.keyword = null;
+ }
+
+ getGlobalLastKeyword(): Promise<string | null> {
+ return Promise.resolve(this.keyword);
+ }
+
+ setGlobalLastKeyword(keyword: string): Promise<void> {
+ this.keyword = keyword;
+ return Promise.resolve();
+ }
+}
+
+describe('FindUseCase', () => {
+ let repository: MockFindRepository;
+ let presenter: MockFindPresenter;
+ let client: MockFindClient;
+ let consoleClient: MockConsoleClient;
+ let sut: FindUseCase;
+
+ beforeEach(() => {
+ repository = new MockFindRepository();
+ presenter = new MockFindPresenter();
+ client = new MockFindClient();
+ consoleClient = new MockConsoleClient();
+ sut = new FindUseCase({ repository, presenter, client, consoleClient });
+ });
+
+ describe('#startFind', () => {
+ it('find next by ketword', async() => {
+ presenter.document = 'monkey punch';
+
+ await sut.startFind('monkey');
+
+ expect(await presenter.highlighted).to.be.true;
+ expect(await consoleClient.text).to.equal('Pattern found: monkey');
+ expect(await repository.getLastKeyword()).to.equal('monkey');
+ expect(await client.getGlobalLastKeyword()).to.equal('monkey');
+ });
+
+ it('find next by last keyword', async() => {
+ presenter.document = 'gorilla kick';
+ repository.keyword = 'gorilla';
+
+ await sut.startFind(undefined);
+
+ expect(await presenter.highlighted).to.be.true;
+ expect(await consoleClient.text).to.equal('Pattern found: gorilla');
+ expect(await repository.getLastKeyword()).to.equal('gorilla');
+ expect(await client.getGlobalLastKeyword()).to.equal('gorilla');
+ });
+
+ it('find next by global last keyword', async() => {
+ presenter.document = 'chimpanzee typing';
+
+ repository.keyword = null;
+ client.keyword = 'chimpanzee';
+
+ await sut.startFind(undefined);
+
+ expect(await presenter.highlighted).to.be.true;
+ expect(await consoleClient.text).to.equal('Pattern found: chimpanzee');
+ expect(await repository.getLastKeyword()).to.equal('chimpanzee');
+ expect(await client.getGlobalLastKeyword()).to.equal('chimpanzee');
+ });
+
+ it('find not found error', async() => {
+ presenter.document = 'zoo';
+
+ await sut.startFind('giraffe');
+
+ expect(await presenter.highlighted).to.be.false;
+ expect(await consoleClient.text).to.equal('Pattern not found: giraffe');
+ expect(await repository.getLastKeyword()).to.equal('giraffe');
+ expect(await client.getGlobalLastKeyword()).to.equal('giraffe');
+ });
+
+ it('show errors when no last keywords', async() => {
+ repository.keyword = null;
+ client.keyword = null;
+
+ await sut.startFind(undefined);
+
+ expect(await consoleClient.text).to.equal('No previous search keywords');
+ expect(await consoleClient.isError).to.be.true;
+ });
+ });
+
+ describe('#findNext', () => {
+ it('finds by last keyword', async() => {
+ presenter.document = 'monkey punch';
+ repository.keyword = 'monkey';
+
+ await sut.findNext();
+
+ expect(await presenter.highlighted).to.be.true;
+ expect(await consoleClient.text).to.equal('Pattern found: monkey');
+ });
+
+ it('show errors when no last keywords', async() => {
+ repository.keyword = null;
+ client.keyword = null;
+
+ await sut.findNext();
+
+ expect(await consoleClient.text).to.equal('No previous search keywords');
+ expect(await consoleClient.isError).to.be.true;
+ });
+ });
+
+ describe('#findPrev', () => {
+ });
+});
diff --git a/test/content/hint-key-producer.test.ts b/test/content/usecases/HintKeyProducer.test.ts
index dcf477d..feafffb 100644
--- a/test/content/hint-key-producer.test.ts
+++ b/test/content/usecases/HintKeyProducer.test.ts
@@ -1,9 +1,10 @@
-import HintKeyProducer from 'content/hint-key-producer';
+import HintKeyProducer from '../../../src/content/usecases/HintKeyProducer';
+import { expect } from 'chai';
describe('HintKeyProducer class', () => {
describe('#constructor', () => {
it('throws an exception on empty charset', () => {
- expect(() => new HintKeyProducer([])).to.throw(TypeError);
+ expect(() => new HintKeyProducer('')).to.throw(TypeError);
});
});
diff --git a/test/content/usecases/MarkUseCase.test.ts b/test/content/usecases/MarkUseCase.test.ts
new file mode 100644
index 0000000..4f2dee4
--- /dev/null
+++ b/test/content/usecases/MarkUseCase.test.ts
@@ -0,0 +1,107 @@
+import MarkRepository from '../../../src/content/repositories/MarkRepository';
+import MarkUseCase from '../../../src/content/usecases/MarkUseCase';
+import MarkClient from '../../../src/content/client/MarkClient';
+import MockConsoleClient from '../mock/MockConsoleClient';
+import MockScrollPresenter from '../mock/MockScrollPresenter';
+import Mark from '../../../src/content/domains/Mark';
+import { expect } from 'chai';
+
+class MockMarkRepository implements MarkRepository {
+ private current: {[key: string]: Mark};
+
+ constructor() {
+ this.current = {};
+ }
+
+ set(key: string, mark: Mark): void {
+ this.current[key] = mark;
+ }
+
+ get(key: string): Mark | null {
+ return this.current[key];
+ }
+}
+
+class MockMarkClient implements MarkClient {
+ public marks: {[key: string]: Mark};
+ public last: string;
+
+ constructor() {
+ this.marks = {};
+ this.last = '';
+ }
+
+ setGloablMark(key: string, mark: Mark): Promise<void> {
+ this.marks[key] = mark;
+ return Promise.resolve();
+ }
+
+ jumpGlobalMark(key: string): Promise<void> {
+ this.last = key
+ return Promise.resolve();
+ }
+}
+
+describe('MarkUseCase', () => {
+ let repository: MockMarkRepository;
+ let client: MockMarkClient;
+ let consoleClient: MockConsoleClient;
+ let scrollPresenter: MockScrollPresenter;
+ let sut: MarkUseCase;
+
+ beforeEach(() => {
+ repository = new MockMarkRepository();
+ client = new MockMarkClient();
+ consoleClient = new MockConsoleClient();
+ scrollPresenter = new MockScrollPresenter();
+ sut = new MarkUseCase({
+ repository, client, consoleClient, scrollPresenter,
+ });
+ });
+
+ describe('#set', () => {
+ it('sets local mark', async() => {
+ scrollPresenter.scrollTo(10, 20, false);
+
+ await sut.set('x');
+
+ expect(repository.get('x')).to.deep.equals({ x: 10, y: 20 });
+ expect(consoleClient.text).to.equal("Set local mark to 'x'");
+ });
+
+ it('sets global mark', async() => {
+ scrollPresenter.scrollTo(30, 40, false);
+
+ await sut.set('Z');
+
+ expect(client.marks['Z']).to.deep.equals({ x: 30, y: 40 });
+ expect(consoleClient.text).to.equal("Set global mark to 'Z'");
+ });
+ });
+
+ describe('#jump', () => {
+ it('jumps to local mark', async() => {
+ repository.set('x', { x: 20, y: 40 });
+
+ await sut.jump('x');
+
+ expect(scrollPresenter.getScroll()).to.deep.equals({ x: 20, y: 40 });
+ });
+
+ it('throws an error when no local marks', () => {
+ return sut.jump('a').then(() => {
+ throw new Error('error');
+ }).catch((e) => {
+ expect(e).to.be.instanceof(Error);
+ })
+ })
+
+ it('jumps to global mark', async() => {
+ client.marks['Z'] = { x: 20, y: 0 };
+
+ await sut.jump('Z');
+
+ expect(client.last).to.equal('Z')
+ });
+ });
+});
diff --git a/test/content/usecases/SettingUseCaase.test.ts b/test/content/usecases/SettingUseCaase.test.ts
new file mode 100644
index 0000000..02cef78
--- /dev/null
+++ b/test/content/usecases/SettingUseCaase.test.ts
@@ -0,0 +1,71 @@
+import SettingRepository from '../../../src/content/repositories/SettingRepository';
+import SettingClient from '../../../src/content/client/SettingClient';
+import SettingUseCase from '../../../src/content/usecases/SettingUseCase';
+import Settings, { DefaultSetting } from '../../../src/shared/Settings';
+import { expect } from 'chai';
+
+class MockSettingRepository implements SettingRepository {
+ private current: Settings;
+
+ constructor() {
+ this.current = DefaultSetting;
+ }
+
+ set(settings: Settings): void {
+ this.current= settings;
+ }
+
+ get(): Settings {
+ return this.current;
+ }
+}
+
+class MockSettingClient implements SettingClient {
+ private data: Settings;
+
+ constructor(data: Settings) {
+ this.data = data;
+ }
+
+ load(): Promise<Settings> {
+ return Promise.resolve(this.data);
+ }
+}
+
+describe('AddonEnabledUseCase', () => {
+ let repository: MockSettingRepository;
+ let client: MockSettingClient;
+ let sut: SettingUseCase;
+
+ beforeEach(() => {
+ let testSettings = {
+ keymaps: {},
+ search: {
+ default: 'google',
+ engines: {
+ google: 'https://google.com/?q={}',
+ }
+ },
+ properties: {
+ hintchars: 'abcd1234',
+ smoothscroll: false,
+ complete: 'sbh',
+ },
+ blacklist: [],
+ };
+
+ repository = new MockSettingRepository();
+ client = new MockSettingClient(testSettings);
+ sut = new SettingUseCase({ repository, client });
+ });
+
+ describe('#reload', () => {
+ it('loads settings and store to repository', async() => {
+ let settings = await sut.reload();
+ expect(settings.properties.hintchars).to.equal('abcd1234');
+
+ let saved = repository.get();
+ expect(saved.properties.hintchars).to.equal('abcd1234');
+ });
+ });
+});