Merge pull request #587 from ueokande/refactor-content
Refactor content scripts
This commit is contained in:
commit
3f4bc62ed5
131 changed files with 3828 additions and 2353 deletions
|
@ -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" }],
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ||
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import * as messages from '../../shared/messages';
|
||||
import * as actions from './index';
|
||||
|
||||
const enable = (): Promise<actions.AddonAction> => setEnabled(true);
|
||||
|
||||
const disable = (): Promise<actions.AddonAction> => setEnabled(false);
|
||||
|
||||
const setEnabled = async(enabled: boolean): Promise<actions.AddonAction> => {
|
||||
await browser.runtime.sendMessage({
|
||||
type: messages.ADDON_ENABLED_RESPONSE,
|
||||
enabled,
|
||||
});
|
||||
return {
|
||||
type: actions.ADDON_SET_ENABLED,
|
||||
enabled,
|
||||
};
|
||||
};
|
||||
|
||||
export { enable, disable, setEnabled };
|
|
@ -1,100 +0,0 @@
|
|||
//
|
||||
// window.find(aString, aCaseSensitive, aBackwards, aWrapAround,
|
||||
// aWholeWord, aSearchInFrames);
|
||||
//
|
||||
// NOTE: window.find is not standard API
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Window/find
|
||||
|
||||
import * as messages from '../../shared/messages';
|
||||
import * as actions from './index';
|
||||
import * as consoleFrames from '../console-frames';
|
||||
|
||||
interface MyWindow extends Window {
|
||||
find(
|
||||
aString: string,
|
||||
aCaseSensitive?: boolean,
|
||||
aBackwards?: boolean,
|
||||
aWrapAround?: boolean,
|
||||
aWholeWord?: boolean,
|
||||
aSearchInFrames?: boolean,
|
||||
aShowDialog?: boolean): boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-var, vars-on-top, init-declarations
|
||||
declare var window: MyWindow;
|
||||
|
||||
const find = (str: string, backwards: boolean): boolean => {
|
||||
let caseSensitive = false;
|
||||
let wrapScan = true;
|
||||
|
||||
|
||||
// NOTE: aWholeWord dows not implemented, and aSearchInFrames does not work
|
||||
// because of same origin policy
|
||||
|
||||
// eslint-disable-next-line no-extra-parens
|
||||
let found = window.find(str, caseSensitive, backwards, wrapScan);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
let sel = window.getSelection();
|
||||
if (sel) {
|
||||
sel.removeAllRanges();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-extra-parens
|
||||
return window.find(str, caseSensitive, backwards, wrapScan);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line max-statements
|
||||
const findNext = async(
|
||||
currentKeyword: string, reset: boolean, backwards: boolean,
|
||||
): Promise<actions.FindAction> => {
|
||||
if (reset) {
|
||||
let sel = window.getSelection();
|
||||
if (sel) {
|
||||
sel.removeAllRanges();
|
||||
}
|
||||
}
|
||||
|
||||
let keyword = currentKeyword;
|
||||
if (currentKeyword) {
|
||||
browser.runtime.sendMessage({
|
||||
type: messages.FIND_SET_KEYWORD,
|
||||
keyword: currentKeyword,
|
||||
});
|
||||
} else {
|
||||
keyword = await browser.runtime.sendMessage({
|
||||
type: messages.FIND_GET_KEYWORD,
|
||||
});
|
||||
}
|
||||
if (!keyword) {
|
||||
await consoleFrames.postError('No previous search keywords');
|
||||
return { type: actions.NOOP };
|
||||
}
|
||||
let found = find(keyword, backwards);
|
||||
if (found) {
|
||||
consoleFrames.postInfo('Pattern found: ' + keyword);
|
||||
} else {
|
||||
consoleFrames.postError('Pattern not found: ' + keyword);
|
||||
}
|
||||
|
||||
return {
|
||||
type: actions.FIND_SET_KEYWORD,
|
||||
keyword,
|
||||
found,
|
||||
};
|
||||
};
|
||||
|
||||
const next = (
|
||||
currentKeyword: string, reset: boolean,
|
||||
): Promise<actions.FindAction> => {
|
||||
return findNext(currentKeyword, reset, false);
|
||||
};
|
||||
|
||||
const prev = (
|
||||
currentKeyword: string, reset: boolean,
|
||||
): Promise<actions.FindAction> => {
|
||||
return findNext(currentKeyword, reset, true);
|
||||
};
|
||||
|
||||
export { next, prev };
|
|
@ -1,32 +0,0 @@
|
|||
import * as actions from './index';
|
||||
|
||||
const enable = (
|
||||
newTab: boolean, background: boolean,
|
||||
): actions.FollowAction => {
|
||||
return {
|
||||
type: actions.FOLLOW_CONTROLLER_ENABLE,
|
||||
newTab,
|
||||
background,
|
||||
};
|
||||
};
|
||||
|
||||
const disable = (): actions.FollowAction => {
|
||||
return {
|
||||
type: actions.FOLLOW_CONTROLLER_DISABLE,
|
||||
};
|
||||
};
|
||||
|
||||
const keyPress = (key: string): actions.FollowAction => {
|
||||
return {
|
||||
type: actions.FOLLOW_CONTROLLER_KEY_PRESS,
|
||||
key: key
|
||||
};
|
||||
};
|
||||
|
||||
const backspace = (): actions.FollowAction => {
|
||||
return {
|
||||
type: actions.FOLLOW_CONTROLLER_BACKSPACE,
|
||||
};
|
||||
};
|
||||
|
||||
export { enable, disable, keyPress, backspace };
|
|
@ -1,122 +0,0 @@
|
|||
import Redux from 'redux';
|
||||
import Settings from '../../shared/Settings';
|
||||
import * as keyUtils from '../../shared/utils/keys';
|
||||
|
||||
// Enable/disable
|
||||
export const ADDON_SET_ENABLED = 'addon.set.enabled';
|
||||
|
||||
// Find
|
||||
export const FIND_SET_KEYWORD = 'find.set.keyword';
|
||||
|
||||
// Settings
|
||||
export const SETTING_SET = 'setting.set';
|
||||
|
||||
// User input
|
||||
export const INPUT_KEY_PRESS = 'input.key.press';
|
||||
export const INPUT_CLEAR_KEYS = 'input.clear.keys';
|
||||
|
||||
// Completion
|
||||
export const COMPLETION_SET_ITEMS = 'completion.set.items';
|
||||
export const COMPLETION_SELECT_NEXT = 'completions.select.next';
|
||||
export const COMPLETION_SELECT_PREV = 'completions.select.prev';
|
||||
|
||||
// Follow
|
||||
export const FOLLOW_CONTROLLER_ENABLE = 'follow.controller.enable';
|
||||
export const FOLLOW_CONTROLLER_DISABLE = 'follow.controller.disable';
|
||||
export const FOLLOW_CONTROLLER_KEY_PRESS = 'follow.controller.key.press';
|
||||
export const FOLLOW_CONTROLLER_BACKSPACE = 'follow.controller.backspace';
|
||||
|
||||
// Mark
|
||||
export const MARK_START_SET = 'mark.start.set';
|
||||
export const MARK_START_JUMP = 'mark.start.jump';
|
||||
export const MARK_CANCEL = 'mark.cancel';
|
||||
export const MARK_SET_LOCAL = 'mark.set.local';
|
||||
|
||||
export const NOOP = 'noop';
|
||||
|
||||
export interface AddonSetEnabledAction extends Redux.Action {
|
||||
type: typeof ADDON_SET_ENABLED;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface FindSetKeywordAction extends Redux.Action {
|
||||
type: typeof FIND_SET_KEYWORD;
|
||||
keyword: string;
|
||||
found: boolean;
|
||||
}
|
||||
|
||||
export interface SettingSetAction extends Redux.Action {
|
||||
type: typeof SETTING_SET;
|
||||
settings: Settings,
|
||||
}
|
||||
|
||||
export interface InputKeyPressAction extends Redux.Action {
|
||||
type: typeof INPUT_KEY_PRESS;
|
||||
key: keyUtils.Key;
|
||||
}
|
||||
|
||||
export interface InputClearKeysAction extends Redux.Action {
|
||||
type: typeof INPUT_CLEAR_KEYS;
|
||||
}
|
||||
|
||||
export interface FollowControllerEnableAction extends Redux.Action {
|
||||
type: typeof FOLLOW_CONTROLLER_ENABLE;
|
||||
newTab: boolean;
|
||||
background: boolean;
|
||||
}
|
||||
|
||||
export interface FollowControllerDisableAction extends Redux.Action {
|
||||
type: typeof FOLLOW_CONTROLLER_DISABLE;
|
||||
}
|
||||
|
||||
export interface FollowControllerKeyPressAction extends Redux.Action {
|
||||
type: typeof FOLLOW_CONTROLLER_KEY_PRESS;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface FollowControllerBackspaceAction extends Redux.Action {
|
||||
type: typeof FOLLOW_CONTROLLER_BACKSPACE;
|
||||
}
|
||||
|
||||
export interface MarkStartSetAction extends Redux.Action {
|
||||
type: typeof MARK_START_SET;
|
||||
}
|
||||
|
||||
export interface MarkStartJumpAction extends Redux.Action {
|
||||
type: typeof MARK_START_JUMP;
|
||||
}
|
||||
|
||||
export interface MarkCancelAction extends Redux.Action {
|
||||
type: typeof MARK_CANCEL;
|
||||
}
|
||||
|
||||
export interface MarkSetLocalAction extends Redux.Action {
|
||||
type: typeof MARK_SET_LOCAL;
|
||||
key: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface NoopAction extends Redux.Action {
|
||||
type: typeof NOOP;
|
||||
}
|
||||
|
||||
export type AddonAction = AddonSetEnabledAction;
|
||||
export type FindAction = FindSetKeywordAction | NoopAction;
|
||||
export type SettingAction = SettingSetAction;
|
||||
export type InputAction = InputKeyPressAction | InputClearKeysAction;
|
||||
export type FollowAction =
|
||||
FollowControllerEnableAction | FollowControllerDisableAction |
|
||||
FollowControllerKeyPressAction | FollowControllerBackspaceAction;
|
||||
export type MarkAction =
|
||||
MarkStartSetAction | MarkStartJumpAction |
|
||||
MarkCancelAction | MarkSetLocalAction | NoopAction;
|
||||
|
||||
export type Action =
|
||||
AddonAction |
|
||||
FindAction |
|
||||
SettingAction |
|
||||
InputAction |
|
||||
FollowAction |
|
||||
MarkAction |
|
||||
NoopAction;
|
|
@ -1,17 +0,0 @@
|
|||
import * as actions from './index';
|
||||
import * as keyUtils from '../../shared/utils/keys';
|
||||
|
||||
const keyPress = (key: keyUtils.Key): actions.InputAction => {
|
||||
return {
|
||||
type: actions.INPUT_KEY_PRESS,
|
||||
key,
|
||||
};
|
||||
};
|
||||
|
||||
const clearKeys = (): actions.InputAction => {
|
||||
return {
|
||||
type: actions.INPUT_CLEAR_KEYS
|
||||
};
|
||||
};
|
||||
|
||||
export { keyPress, clearKeys };
|
|
@ -1,46 +0,0 @@
|
|||
import * as actions from './index';
|
||||
import * as messages from '../../shared/messages';
|
||||
|
||||
const startSet = (): actions.MarkAction => {
|
||||
return { type: actions.MARK_START_SET };
|
||||
};
|
||||
|
||||
const startJump = (): actions.MarkAction => {
|
||||
return { type: actions.MARK_START_JUMP };
|
||||
};
|
||||
|
||||
const cancel = (): actions.MarkAction => {
|
||||
return { type: actions.MARK_CANCEL };
|
||||
};
|
||||
|
||||
const setLocal = (key: string, x: number, y: number): actions.MarkAction => {
|
||||
return {
|
||||
type: actions.MARK_SET_LOCAL,
|
||||
key,
|
||||
x,
|
||||
y,
|
||||
};
|
||||
};
|
||||
|
||||
const setGlobal = (key: string, x: number, y: number): actions.MarkAction => {
|
||||
browser.runtime.sendMessage({
|
||||
type: messages.MARK_SET_GLOBAL,
|
||||
key,
|
||||
x,
|
||||
y,
|
||||
});
|
||||
return { type: actions.NOOP };
|
||||
};
|
||||
|
||||
const jumpGlobal = (key: string): actions.MarkAction => {
|
||||
browser.runtime.sendMessage({
|
||||
type: messages.MARK_JUMP_GLOBAL,
|
||||
key,
|
||||
});
|
||||
return { type: actions.NOOP };
|
||||
};
|
||||
|
||||
export {
|
||||
startSet, startJump, cancel, setLocal,
|
||||
setGlobal, jumpGlobal,
|
||||
};
|
|
@ -1,107 +0,0 @@
|
|||
import * as operations from '../../shared/operations';
|
||||
import * as actions from './index';
|
||||
import * as messages from '../../shared/messages';
|
||||
import * as scrolls from '../scrolls';
|
||||
import * as navigates from '../navigates';
|
||||
import * as focuses from '../focuses';
|
||||
import * as urls from '../urls';
|
||||
import * as consoleFrames from '../console-frames';
|
||||
import * as addonActions from './addon';
|
||||
import * as markActions from './mark';
|
||||
|
||||
// eslint-disable-next-line complexity, max-lines-per-function
|
||||
const exec = (
|
||||
operation: operations.Operation,
|
||||
settings: any,
|
||||
addonEnabled: boolean,
|
||||
): Promise<actions.Action> | actions.Action => {
|
||||
let smoothscroll = settings.properties.smoothscroll;
|
||||
switch (operation.type) {
|
||||
case operations.ADDON_ENABLE:
|
||||
return addonActions.enable();
|
||||
case operations.ADDON_DISABLE:
|
||||
return addonActions.disable();
|
||||
case operations.ADDON_TOGGLE_ENABLED:
|
||||
return addonActions.setEnabled(!addonEnabled);
|
||||
case operations.FIND_NEXT:
|
||||
window.top.postMessage(JSON.stringify({
|
||||
type: messages.FIND_NEXT,
|
||||
}), '*');
|
||||
break;
|
||||
case operations.FIND_PREV:
|
||||
window.top.postMessage(JSON.stringify({
|
||||
type: messages.FIND_PREV,
|
||||
}), '*');
|
||||
break;
|
||||
case operations.SCROLL_VERTICALLY:
|
||||
scrolls.scrollVertically(operation.count, smoothscroll);
|
||||
break;
|
||||
case operations.SCROLL_HORIZONALLY:
|
||||
scrolls.scrollHorizonally(operation.count, smoothscroll);
|
||||
break;
|
||||
case operations.SCROLL_PAGES:
|
||||
scrolls.scrollPages(operation.count, smoothscroll);
|
||||
break;
|
||||
case operations.SCROLL_TOP:
|
||||
scrolls.scrollToTop(smoothscroll);
|
||||
break;
|
||||
case operations.SCROLL_BOTTOM:
|
||||
scrolls.scrollToBottom(smoothscroll);
|
||||
break;
|
||||
case operations.SCROLL_HOME:
|
||||
scrolls.scrollToHome(smoothscroll);
|
||||
break;
|
||||
case operations.SCROLL_END:
|
||||
scrolls.scrollToEnd(smoothscroll);
|
||||
break;
|
||||
case operations.FOLLOW_START:
|
||||
window.top.postMessage(JSON.stringify({
|
||||
type: messages.FOLLOW_START,
|
||||
newTab: operation.newTab,
|
||||
background: operation.background,
|
||||
}), '*');
|
||||
break;
|
||||
case operations.MARK_SET_PREFIX:
|
||||
return markActions.startSet();
|
||||
case operations.MARK_JUMP_PREFIX:
|
||||
return markActions.startJump();
|
||||
case operations.NAVIGATE_HISTORY_PREV:
|
||||
navigates.historyPrev(window);
|
||||
break;
|
||||
case operations.NAVIGATE_HISTORY_NEXT:
|
||||
navigates.historyNext(window);
|
||||
break;
|
||||
case operations.NAVIGATE_LINK_PREV:
|
||||
navigates.linkPrev(window);
|
||||
break;
|
||||
case operations.NAVIGATE_LINK_NEXT:
|
||||
navigates.linkNext(window);
|
||||
break;
|
||||
case operations.NAVIGATE_PARENT:
|
||||
navigates.parent(window);
|
||||
break;
|
||||
case operations.NAVIGATE_ROOT:
|
||||
navigates.root(window);
|
||||
break;
|
||||
case operations.FOCUS_INPUT:
|
||||
focuses.focusInput();
|
||||
break;
|
||||
case operations.URLS_YANK:
|
||||
urls.yank(window);
|
||||
consoleFrames.postInfo('Yanked ' + window.location.href);
|
||||
break;
|
||||
case operations.URLS_PASTE:
|
||||
urls.paste(
|
||||
window, operation.newTab ? operation.newTab : false, settings.search
|
||||
);
|
||||
break;
|
||||
default:
|
||||
browser.runtime.sendMessage({
|
||||
type: messages.BACKGROUND_OPERATION,
|
||||
operation,
|
||||
});
|
||||
}
|
||||
return { type: actions.NOOP };
|
||||
};
|
||||
|
||||
export { exec };
|
|
@ -1,28 +0,0 @@
|
|||
import * as actions from './index';
|
||||
import * as operations from '../../shared/operations';
|
||||
import * as messages from '../../shared/messages';
|
||||
import Settings, { Keymaps } from '../../shared/Settings';
|
||||
|
||||
const reservedKeymaps: Keymaps = {
|
||||
'<Esc>': { type: operations.CANCEL },
|
||||
'<C-[>': { type: operations.CANCEL },
|
||||
};
|
||||
|
||||
const set = (settings: Settings): actions.SettingAction => {
|
||||
return {
|
||||
type: actions.SETTING_SET,
|
||||
settings: {
|
||||
...settings,
|
||||
keymaps: { ...settings.keymaps, ...reservedKeymaps },
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const load = async(): Promise<actions.SettingAction> => {
|
||||
let settings = await browser.runtime.sendMessage({
|
||||
type: messages.SETTINGS_QUERY,
|
||||
});
|
||||
return set(settings);
|
||||
};
|
||||
|
||||
export { set, load };
|
16
src/content/client/AddonIndicatorClient.ts
Normal file
16
src/content/client/AddonIndicatorClient.ts
Normal file
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
11
src/content/client/BackgroundClient.ts
Normal file
11
src/content/client/BackgroundClient.ts
Normal file
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
30
src/content/client/ConsoleClient.ts
Normal file
30
src/content/client/ConsoleClient.ts
Normal file
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
25
src/content/client/FindClient.ts
Normal file
25
src/content/client/FindClient.ts
Normal file
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
23
src/content/client/FindMasterClient.ts
Normal file
23
src/content/client/FindMasterClient.ts
Normal file
|
@ -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,
|
||||
}), '*');
|
||||
}
|
||||
}
|
47
src/content/client/FollowMasterClient.ts
Normal file
47
src/content/client/FollowMasterClient.ts
Normal file
|
@ -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), '*');
|
||||
}
|
||||
}
|
76
src/content/client/FollowSlaveClient.ts
Normal file
76
src/content/client/FollowSlaveClient.ts
Normal file
|
@ -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), '*');
|
||||
}
|
||||
}
|
28
src/content/client/MarkClient.ts
Normal file
28
src/content/client/MarkClient.ts
Normal file
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
17
src/content/client/SettingClient.ts
Normal file
17
src/content/client/SettingClient.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
22
src/content/client/TabsClient.ts
Normal file
22
src/content/client/TabsClient.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import * as messages from '../../shared/messages';
|
||||
|
||||
export default interface TabsClient {
|
||||
openUrl(url: string, newTab: boolean, background?: boolean): Promise<void>;
|
||||
|
||||
// eslint-disable-next-line semi
|
||||
}
|
||||
|
||||
export class TabsClientImpl implements TabsClient {
|
||||
async openUrl(
|
||||
url: string,
|
||||
newTab: boolean,
|
||||
background?: boolean,
|
||||
): Promise<void> {
|
||||
await browser.runtime.sendMessage({
|
||||
type: messages.OPEN_URL,
|
||||
url,
|
||||
newTab,
|
||||
background,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,231 +0,0 @@
|
|||
import MessageListener from '../../MessageListener';
|
||||
import Hint from './hint';
|
||||
import * as dom from '../../../shared/utils/dom';
|
||||
import * as messages from '../../../shared/messages';
|
||||
import * as keyUtils from '../../../shared/utils/keys';
|
||||
|
||||
const TARGET_SELECTOR = [
|
||||
'a', 'button', 'input', 'textarea', 'area',
|
||||
'[contenteditable=true]', '[contenteditable=""]', '[tabindex]',
|
||||
'[role="button"]', 'summary'
|
||||
].join(',');
|
||||
|
||||
interface Size {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
const inViewport = (
|
||||
win: Window,
|
||||
element: Element,
|
||||
viewSize: Size,
|
||||
framePosition: Point,
|
||||
): boolean => {
|
||||
let {
|
||||
top, left, bottom, right
|
||||
} = dom.viewportRect(element);
|
||||
let doc = win.document;
|
||||
let frameWidth = doc.documentElement.clientWidth;
|
||||
let frameHeight = doc.documentElement.clientHeight;
|
||||
|
||||
if (right < 0 || bottom < 0 || top > frameHeight || left > frameWidth) {
|
||||
// out of frame
|
||||
return false;
|
||||
}
|
||||
if (right + framePosition.x < 0 || bottom + framePosition.y < 0 ||
|
||||
left + framePosition.x > viewSize.width ||
|
||||
top + framePosition.y > viewSize.height) {
|
||||
// out of viewport
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const isAriaHiddenOrAriaDisabled = (win: Window, element: Element): boolean => {
|
||||
if (!element || win.document.documentElement === element) {
|
||||
return false;
|
||||
}
|
||||
for (let attr of ['aria-hidden', 'aria-disabled']) {
|
||||
let value = element.getAttribute(attr);
|
||||
if (value !== null) {
|
||||
let hidden = value.toLowerCase();
|
||||
if (hidden === '' || hidden === 'true') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return isAriaHiddenOrAriaDisabled(win, element.parentElement as Element);
|
||||
};
|
||||
|
||||
export default class Follow {
|
||||
private win: Window;
|
||||
|
||||
private newTab: boolean;
|
||||
|
||||
private background: boolean;
|
||||
|
||||
private hints: {[key: string]: Hint };
|
||||
|
||||
private targets: HTMLElement[] = [];
|
||||
|
||||
constructor(win: Window) {
|
||||
this.win = win;
|
||||
this.newTab = false;
|
||||
this.background = false;
|
||||
this.hints = {};
|
||||
this.targets = [];
|
||||
|
||||
new MessageListener().onWebMessage(this.onMessage.bind(this));
|
||||
}
|
||||
|
||||
key(key: keyUtils.Key): boolean {
|
||||
if (Object.keys(this.hints).length === 0) {
|
||||
return false;
|
||||
}
|
||||
this.win.parent.postMessage(JSON.stringify({
|
||||
type: messages.FOLLOW_KEY_PRESS,
|
||||
key: key.key,
|
||||
ctrlKey: key.ctrlKey,
|
||||
}), '*');
|
||||
return true;
|
||||
}
|
||||
|
||||
openLink(element: HTMLAreaElement|HTMLAnchorElement) {
|
||||
// Browser prevent new tab by link with target='_blank'
|
||||
if (!this.newTab && element.getAttribute('target') !== '_blank') {
|
||||
element.click();
|
||||
return;
|
||||
}
|
||||
|
||||
let href = element.getAttribute('href');
|
||||
|
||||
// eslint-disable-next-line no-script-url
|
||||
if (!href || href === '#' || href.toLowerCase().startsWith('javascript:')) {
|
||||
return;
|
||||
}
|
||||
return browser.runtime.sendMessage({
|
||||
type: messages.OPEN_URL,
|
||||
url: element.href,
|
||||
newTab: true,
|
||||
background: this.background,
|
||||
});
|
||||
}
|
||||
|
||||
countHints(sender: any, viewSize: Size, framePosition: Point) {
|
||||
this.targets = Follow.getTargetElements(this.win, viewSize, framePosition);
|
||||
sender.postMessage(JSON.stringify({
|
||||
type: messages.FOLLOW_RESPONSE_COUNT_TARGETS,
|
||||
count: this.targets.length,
|
||||
}), '*');
|
||||
}
|
||||
|
||||
createHints(keysArray: string[], newTab: boolean, background: boolean) {
|
||||
if (keysArray.length !== this.targets.length) {
|
||||
throw new Error('illegal hint count');
|
||||
}
|
||||
|
||||
this.newTab = newTab;
|
||||
this.background = background;
|
||||
this.hints = {};
|
||||
for (let i = 0; i < keysArray.length; ++i) {
|
||||
let keys = keysArray[i];
|
||||
let hint = new Hint(this.targets[i], keys);
|
||||
this.hints[keys] = hint;
|
||||
}
|
||||
}
|
||||
|
||||
showHints(keys: string) {
|
||||
Object.keys(this.hints).filter(key => key.startsWith(keys))
|
||||
.forEach(key => this.hints[key].show());
|
||||
Object.keys(this.hints).filter(key => !key.startsWith(keys))
|
||||
.forEach(key => this.hints[key].hide());
|
||||
}
|
||||
|
||||
removeHints() {
|
||||
Object.keys(this.hints).forEach((key) => {
|
||||
this.hints[key].remove();
|
||||
});
|
||||
this.hints = {};
|
||||
this.targets = [];
|
||||
}
|
||||
|
||||
activateHints(keys: string) {
|
||||
let hint = this.hints[keys];
|
||||
if (!hint) {
|
||||
return;
|
||||
}
|
||||
let element = hint.getTarget();
|
||||
switch (element.tagName.toLowerCase()) {
|
||||
case 'a':
|
||||
return this.openLink(element as HTMLAnchorElement);
|
||||
case 'area':
|
||||
return this.openLink(element as HTMLAreaElement);
|
||||
case 'input':
|
||||
switch ((element as HTMLInputElement).type) {
|
||||
case 'file':
|
||||
case 'checkbox':
|
||||
case 'radio':
|
||||
case 'submit':
|
||||
case 'reset':
|
||||
case 'button':
|
||||
case 'image':
|
||||
case 'color':
|
||||
return element.click();
|
||||
default:
|
||||
return element.focus();
|
||||
}
|
||||
case 'textarea':
|
||||
return element.focus();
|
||||
case 'button':
|
||||
case 'summary':
|
||||
return element.click();
|
||||
default:
|
||||
if (dom.isContentEditable(element)) {
|
||||
return element.focus();
|
||||
} else if (element.hasAttribute('tabindex')) {
|
||||
return element.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMessage(message: messages.Message, sender: any) {
|
||||
switch (message.type) {
|
||||
case messages.FOLLOW_REQUEST_COUNT_TARGETS:
|
||||
return this.countHints(sender, message.viewSize, message.framePosition);
|
||||
case messages.FOLLOW_CREATE_HINTS:
|
||||
return this.createHints(
|
||||
message.keysArray, message.newTab, message.background);
|
||||
case messages.FOLLOW_SHOW_HINTS:
|
||||
return this.showHints(message.keys);
|
||||
case messages.FOLLOW_ACTIVATE:
|
||||
return this.activateHints(message.keys);
|
||||
case messages.FOLLOW_REMOVE_HINTS:
|
||||
return this.removeHints();
|
||||
}
|
||||
}
|
||||
|
||||
static getTargetElements(
|
||||
win: Window,
|
||||
viewSize:
|
||||
Size, framePosition: Point,
|
||||
): HTMLElement[] {
|
||||
let all = win.document.querySelectorAll(TARGET_SELECTOR);
|
||||
let filtered = Array.prototype.filter.call(all, (element: HTMLElement) => {
|
||||
let style = win.getComputedStyle(element);
|
||||
|
||||
// AREA's 'display' in Browser style is 'none'
|
||||
return (element.tagName === 'AREA' || style.display !== 'none') &&
|
||||
style.visibility !== 'hidden' &&
|
||||
(element as HTMLInputElement).type !== 'hidden' &&
|
||||
element.offsetHeight > 0 &&
|
||||
!isAriaHiddenOrAriaDisabled(win, element) &&
|
||||
inViewport(win, element, viewSize, framePosition);
|
||||
});
|
||||
return filtered;
|
||||
}
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
import * as dom from '../../../shared/utils/dom';
|
||||
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
const hintPosition = (element: Element): Point => {
|
||||
let { left, top, right, bottom } = dom.viewportRect(element);
|
||||
|
||||
if (element.tagName !== 'AREA') {
|
||||
return { x: left, y: top };
|
||||
}
|
||||
|
||||
return {
|
||||
x: (left + right) / 2,
|
||||
y: (top + bottom) / 2,
|
||||
};
|
||||
};
|
||||
|
||||
export default class Hint {
|
||||
private target: HTMLElement;
|
||||
|
||||
private element: HTMLElement;
|
||||
|
||||
constructor(target: HTMLElement, tag: string) {
|
||||
let doc = target.ownerDocument;
|
||||
if (doc === null) {
|
||||
throw new TypeError('ownerDocument is null');
|
||||
}
|
||||
|
||||
let { x, y } = hintPosition(target);
|
||||
let { scrollX, scrollY } = window;
|
||||
|
||||
this.target = target;
|
||||
|
||||
this.element = doc.createElement('span');
|
||||
this.element.className = 'vimvixen-hint';
|
||||
this.element.textContent = tag;
|
||||
this.element.style.left = x + scrollX + 'px';
|
||||
this.element.style.top = y + scrollY + 'px';
|
||||
|
||||
this.show();
|
||||
doc.body.append(this.element);
|
||||
}
|
||||
|
||||
show(): void {
|
||||
this.element.style.display = 'inline';
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
this.element.style.display = 'none';
|
||||
}
|
||||
|
||||
remove(): void {
|
||||
this.element.remove();
|
||||
}
|
||||
|
||||
getTarget(): HTMLElement {
|
||||
return this.target;
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
import InputComponent from './input';
|
||||
import FollowComponent from './follow';
|
||||
import MarkComponent from './mark';
|
||||
import KeymapperComponent from './keymapper';
|
||||
import * as settingActions from '../../actions/setting';
|
||||
import * as messages from '../../../shared/messages';
|
||||
import MessageListener from '../../MessageListener';
|
||||
import * as addonActions from '../../actions/addon';
|
||||
import * as blacklists from '../../../shared/blacklists';
|
||||
import * as keys from '../../../shared/utils/keys';
|
||||
import * as actions from '../../actions';
|
||||
|
||||
export default class Common {
|
||||
private win: Window;
|
||||
|
||||
private store: any;
|
||||
|
||||
constructor(win: Window, store: any) {
|
||||
const input = new InputComponent(win.document.body);
|
||||
const follow = new FollowComponent(win);
|
||||
const mark = new MarkComponent(store);
|
||||
const keymapper = new KeymapperComponent(store);
|
||||
|
||||
input.onKey((key: keys.Key) => follow.key(key));
|
||||
input.onKey((key: keys.Key) => mark.key(key));
|
||||
input.onKey((key: keys.Key) => keymapper.key(key));
|
||||
|
||||
this.win = win;
|
||||
this.store = store;
|
||||
|
||||
this.reloadSettings();
|
||||
|
||||
new MessageListener().onBackgroundMessage(this.onMessage.bind(this));
|
||||
}
|
||||
|
||||
onMessage(message: messages.Message) {
|
||||
let { enabled } = this.store.getState().addon;
|
||||
switch (message.type) {
|
||||
case messages.SETTINGS_CHANGED:
|
||||
return this.reloadSettings();
|
||||
case messages.ADDON_TOGGLE_ENABLED:
|
||||
this.store.dispatch(addonActions.setEnabled(!enabled));
|
||||
}
|
||||
}
|
||||
|
||||
reloadSettings() {
|
||||
try {
|
||||
this.store.dispatch(settingActions.load())
|
||||
.then((action: actions.SettingAction) => {
|
||||
let enabled = !blacklists.includes(
|
||||
action.settings.blacklist, this.win.location.href
|
||||
);
|
||||
this.store.dispatch(addonActions.setEnabled(enabled));
|
||||
});
|
||||
} catch (e) {
|
||||
// Sometime sendMessage fails when background script is not ready.
|
||||
console.warn(e);
|
||||
setTimeout(() => this.reloadSettings(), 500);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
import * as inputActions from '../../actions/input';
|
||||
import * as operationActions from '../../actions/operation';
|
||||
import * as operations from '../../../shared/operations';
|
||||
import * as keyUtils from '../../../shared/utils/keys';
|
||||
|
||||
const mapStartsWith = (
|
||||
mapping: keyUtils.Key[],
|
||||
keys: keyUtils.Key[],
|
||||
): boolean => {
|
||||
if (mapping.length < keys.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
if (!keyUtils.equals(mapping[i], keys[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export default class KeymapperComponent {
|
||||
private store: any;
|
||||
|
||||
constructor(store: any) {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-statements
|
||||
key(key: keyUtils.Key): boolean {
|
||||
this.store.dispatch(inputActions.keyPress(key));
|
||||
|
||||
let state = this.store.getState();
|
||||
let input = state.input;
|
||||
let keymaps = new Map<keyUtils.Key[], operations.Operation>(
|
||||
state.setting.keymaps.map(
|
||||
(e: {key: keyUtils.Key[], op: operations.Operation}) => [e.key, e.op],
|
||||
)
|
||||
);
|
||||
|
||||
let matched = Array.from(keymaps.keys()).filter(
|
||||
(mapping: keyUtils.Key[]) => {
|
||||
return mapStartsWith(mapping, input.keys);
|
||||
});
|
||||
if (!state.addon.enabled) {
|
||||
// available keymaps are only ADDON_ENABLE and ADDON_TOGGLE_ENABLED if
|
||||
// the addon disabled
|
||||
matched = matched.filter((keys) => {
|
||||
let type = (keymaps.get(keys) as operations.Operation).type;
|
||||
return type === operations.ADDON_ENABLE ||
|
||||
type === operations.ADDON_TOGGLE_ENABLED;
|
||||
});
|
||||
}
|
||||
if (matched.length === 0) {
|
||||
this.store.dispatch(inputActions.clearKeys());
|
||||
return false;
|
||||
} else if (matched.length > 1 ||
|
||||
matched.length === 1 && input.keys.length < matched[0].length) {
|
||||
return true;
|
||||
}
|
||||
let operation = keymaps.get(matched[0]) as operations.Operation;
|
||||
let act = operationActions.exec(
|
||||
operation, state.setting, state.addon.enabled
|
||||
);
|
||||
this.store.dispatch(act);
|
||||
this.store.dispatch(inputActions.clearKeys());
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
import * as markActions from '../../actions/mark';
|
||||
import * as scrolls from '../..//scrolls';
|
||||
import * as consoleFrames from '../..//console-frames';
|
||||
import * as keyUtils from '../../../shared/utils/keys';
|
||||
import Mark from '../../Mark';
|
||||
|
||||
const cancelKey = (key: keyUtils.Key): boolean => {
|
||||
return key.key === 'Esc' || key.key === '[' && Boolean(key.ctrlKey);
|
||||
};
|
||||
|
||||
const globalKey = (key: string): boolean => {
|
||||
return (/^[A-Z0-9]$/).test(key);
|
||||
};
|
||||
|
||||
export default class MarkComponent {
|
||||
private store: any;
|
||||
|
||||
constructor(store: any) {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-statements
|
||||
key(key: keyUtils.Key) {
|
||||
let { mark: markState, setting } = this.store.getState();
|
||||
let smoothscroll = setting.properties.smoothscroll;
|
||||
|
||||
if (!markState.setMode && !markState.jumpMode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cancelKey(key)) {
|
||||
this.store.dispatch(markActions.cancel());
|
||||
return true;
|
||||
}
|
||||
|
||||
if (key.ctrlKey || key.metaKey || key.altKey) {
|
||||
consoleFrames.postError('Unknown mark');
|
||||
} else if (globalKey(key.key) && markState.setMode) {
|
||||
this.doSetGlobal(key);
|
||||
} else if (globalKey(key.key) && markState.jumpMode) {
|
||||
this.doJumpGlobal(key);
|
||||
} else if (markState.setMode) {
|
||||
this.doSet(key);
|
||||
} else if (markState.jumpMode) {
|
||||
this.doJump(markState.marks, key, smoothscroll);
|
||||
}
|
||||
|
||||
this.store.dispatch(markActions.cancel());
|
||||
return true;
|
||||
}
|
||||
|
||||
doSet(key: keyUtils.Key) {
|
||||
let { x, y } = scrolls.getScroll();
|
||||
this.store.dispatch(markActions.setLocal(key.key, x, y));
|
||||
}
|
||||
|
||||
doJump(
|
||||
marks: { [key: string]: Mark },
|
||||
key: keyUtils.Key,
|
||||
smoothscroll: boolean,
|
||||
) {
|
||||
if (!marks[key.key]) {
|
||||
consoleFrames.postError('Mark is not set');
|
||||
return;
|
||||
}
|
||||
|
||||
let { x, y } = marks[key.key];
|
||||
scrolls.scrollTo(x, y, smoothscroll);
|
||||
}
|
||||
|
||||
doSetGlobal(key: keyUtils.Key) {
|
||||
let { x, y } = scrolls.getScroll();
|
||||
this.store.dispatch(markActions.setGlobal(key.key, x, y));
|
||||
}
|
||||
|
||||
doJumpGlobal(key: keyUtils.Key) {
|
||||
this.store.dispatch(markActions.jumpGlobal(key.key));
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
import CommonComponent from './common';
|
||||
|
||||
export default CommonComponent;
|
|
@ -1,46 +0,0 @@
|
|||
import * as findActions from '../../actions/find';
|
||||
import * as messages from '../../../shared/messages';
|
||||
import MessageListener from '../../MessageListener';
|
||||
|
||||
export default class FindComponent {
|
||||
private store: any;
|
||||
|
||||
constructor(store: any) {
|
||||
this.store = store;
|
||||
|
||||
new MessageListener().onWebMessage(this.onMessage.bind(this));
|
||||
}
|
||||
|
||||
onMessage(message: messages.Message) {
|
||||
switch (message.type) {
|
||||
case messages.CONSOLE_ENTER_FIND:
|
||||
return this.start(message.text);
|
||||
case messages.FIND_NEXT:
|
||||
return this.next();
|
||||
case messages.FIND_PREV:
|
||||
return this.prev();
|
||||
}
|
||||
}
|
||||
|
||||
start(text: string) {
|
||||
let state = this.store.getState().find;
|
||||
|
||||
if (text.length === 0) {
|
||||
return this.store.dispatch(
|
||||
findActions.next(state.keyword as string, true));
|
||||
}
|
||||
return this.store.dispatch(findActions.next(text, true));
|
||||
}
|
||||
|
||||
next() {
|
||||
let state = this.store.getState().find;
|
||||
return this.store.dispatch(
|
||||
findActions.next(state.keyword as string, false));
|
||||
}
|
||||
|
||||
prev() {
|
||||
let state = this.store.getState().find;
|
||||
return this.store.dispatch(
|
||||
findActions.prev(state.keyword as string, false));
|
||||
}
|
||||
}
|
|
@ -1,166 +0,0 @@
|
|||
import * as followControllerActions from '../../actions/follow-controller';
|
||||
import * as messages from '../../../shared/messages';
|
||||
import MessageListener, { WebMessageSender } from '../../MessageListener';
|
||||
import HintKeyProducer from '../../hint-key-producer';
|
||||
|
||||
const broadcastMessage = (win: Window, message: messages.Message): void => {
|
||||
let json = JSON.stringify(message);
|
||||
let frames = [win.self].concat(Array.from(win.frames as any));
|
||||
frames.forEach(frame => frame.postMessage(json, '*'));
|
||||
};
|
||||
|
||||
export default class FollowController {
|
||||
private win: Window;
|
||||
|
||||
private store: any;
|
||||
|
||||
private state: {
|
||||
enabled?: boolean;
|
||||
newTab?: boolean;
|
||||
background?: boolean;
|
||||
keys?: string,
|
||||
};
|
||||
|
||||
private keys: string[];
|
||||
|
||||
private producer: HintKeyProducer | null;
|
||||
|
||||
constructor(win: Window, store: any) {
|
||||
this.win = win;
|
||||
this.store = store;
|
||||
this.state = {};
|
||||
this.keys = [];
|
||||
this.producer = null;
|
||||
|
||||
new MessageListener().onWebMessage(this.onMessage.bind(this));
|
||||
|
||||
store.subscribe(() => {
|
||||
this.update();
|
||||
});
|
||||
}
|
||||
|
||||
onMessage(message: messages.Message, sender: WebMessageSender) {
|
||||
switch (message.type) {
|
||||
case messages.FOLLOW_START:
|
||||
return this.store.dispatch(
|
||||
followControllerActions.enable(message.newTab, message.background));
|
||||
case messages.FOLLOW_RESPONSE_COUNT_TARGETS:
|
||||
return this.create(message.count, sender);
|
||||
case messages.FOLLOW_KEY_PRESS:
|
||||
return this.keyPress(message.key, message.ctrlKey);
|
||||
}
|
||||
}
|
||||
|
||||
update(): void {
|
||||
let prevState = this.state;
|
||||
this.state = this.store.getState().followController;
|
||||
|
||||
if (!prevState.enabled && this.state.enabled) {
|
||||
this.count();
|
||||
} else if (prevState.enabled && !this.state.enabled) {
|
||||
this.remove();
|
||||
} else if (prevState.keys !== this.state.keys) {
|
||||
this.updateHints();
|
||||
}
|
||||
}
|
||||
|
||||
updateHints(): void {
|
||||
let shown = this.keys.filter((key) => {
|
||||
return key.startsWith(this.state.keys as string);
|
||||
});
|
||||
if (shown.length === 1) {
|
||||
this.activate();
|
||||
this.store.dispatch(followControllerActions.disable());
|
||||
}
|
||||
|
||||
broadcastMessage(this.win, {
|
||||
type: messages.FOLLOW_SHOW_HINTS,
|
||||
keys: this.state.keys as string,
|
||||
});
|
||||
}
|
||||
|
||||
activate(): void {
|
||||
broadcastMessage(this.win, {
|
||||
type: messages.FOLLOW_ACTIVATE,
|
||||
keys: this.state.keys as string,
|
||||
});
|
||||
}
|
||||
|
||||
keyPress(key: string, ctrlKey: boolean): boolean {
|
||||
if (key === '[' && ctrlKey) {
|
||||
this.store.dispatch(followControllerActions.disable());
|
||||
return true;
|
||||
}
|
||||
switch (key) {
|
||||
case 'Enter':
|
||||
this.activate();
|
||||
this.store.dispatch(followControllerActions.disable());
|
||||
break;
|
||||
case 'Esc':
|
||||
this.store.dispatch(followControllerActions.disable());
|
||||
break;
|
||||
case 'Backspace':
|
||||
case 'Delete':
|
||||
this.store.dispatch(followControllerActions.backspace());
|
||||
break;
|
||||
default:
|
||||
if (this.hintchars().includes(key)) {
|
||||
this.store.dispatch(followControllerActions.keyPress(key));
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
count() {
|
||||
this.producer = new HintKeyProducer(this.hintchars());
|
||||
let doc = this.win.document;
|
||||
let viewWidth = this.win.innerWidth || doc.documentElement.clientWidth;
|
||||
let viewHeight = this.win.innerHeight || doc.documentElement.clientHeight;
|
||||
let frameElements = this.win.document.querySelectorAll('frame,iframe');
|
||||
|
||||
this.win.postMessage(JSON.stringify({
|
||||
type: messages.FOLLOW_REQUEST_COUNT_TARGETS,
|
||||
viewSize: { width: viewWidth, height: viewHeight },
|
||||
framePosition: { x: 0, y: 0 },
|
||||
}), '*');
|
||||
frameElements.forEach((ele) => {
|
||||
let { left: frameX, top: frameY } = ele.getBoundingClientRect();
|
||||
let message = JSON.stringify({
|
||||
type: messages.FOLLOW_REQUEST_COUNT_TARGETS,
|
||||
viewSize: { width: viewWidth, height: viewHeight },
|
||||
framePosition: { x: frameX, y: frameY },
|
||||
});
|
||||
if (ele instanceof HTMLFrameElement && ele.contentWindow ||
|
||||
ele instanceof HTMLIFrameElement && ele.contentWindow) {
|
||||
ele.contentWindow.postMessage(message, '*');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
create(count: number, sender: WebMessageSender) {
|
||||
let produced = [];
|
||||
for (let i = 0; i < count; ++i) {
|
||||
produced.push((this.producer as HintKeyProducer).produce());
|
||||
}
|
||||
this.keys = this.keys.concat(produced);
|
||||
|
||||
(sender as Window).postMessage(JSON.stringify({
|
||||
type: messages.FOLLOW_CREATE_HINTS,
|
||||
keysArray: produced,
|
||||
newTab: this.state.newTab,
|
||||
background: this.state.background,
|
||||
}), '*');
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.keys = [];
|
||||
broadcastMessage(this.win, {
|
||||
type: messages.FOLLOW_REMOVE_HINTS,
|
||||
});
|
||||
}
|
||||
|
||||
hintchars() {
|
||||
return this.store.getState().setting.properties.hintchars;
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
import CommonComponent from '../common';
|
||||
import FollowController from './follow-controller';
|
||||
import FindComponent from './find';
|
||||
import * as consoleFrames from '../../console-frames';
|
||||
import * as messages from '../../../shared/messages';
|
||||
import MessageListener from '../../MessageListener';
|
||||
import * as scrolls from '../../scrolls';
|
||||
|
||||
export default class TopContent {
|
||||
private win: Window;
|
||||
|
||||
private store: any;
|
||||
|
||||
constructor(win: Window, store: any) {
|
||||
this.win = win;
|
||||
this.store = store;
|
||||
|
||||
new CommonComponent(win, store); // eslint-disable-line no-new
|
||||
new FollowController(win, store); // eslint-disable-line no-new
|
||||
new FindComponent(store); // eslint-disable-line no-new
|
||||
|
||||
// TODO make component
|
||||
consoleFrames.initialize(this.win.document);
|
||||
|
||||
new MessageListener().onWebMessage(this.onWebMessage.bind(this));
|
||||
new MessageListener().onBackgroundMessage(
|
||||
this.onBackgroundMessage.bind(this));
|
||||
}
|
||||
|
||||
onWebMessage(message: messages.Message) {
|
||||
switch (message.type) {
|
||||
case messages.CONSOLE_UNFOCUS:
|
||||
this.win.focus();
|
||||
consoleFrames.blur(window.document);
|
||||
}
|
||||
}
|
||||
|
||||
onBackgroundMessage(message: messages.Message) {
|
||||
let addonState = this.store.getState().addon;
|
||||
|
||||
switch (message.type) {
|
||||
case messages.ADDON_ENABLED_QUERY:
|
||||
return Promise.resolve({
|
||||
type: messages.ADDON_ENABLED_RESPONSE,
|
||||
enabled: addonState.enabled,
|
||||
});
|
||||
case messages.TAB_SCROLL_TO:
|
||||
return scrolls.scrollTo(message.x, message.y, false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import * as messages from '../shared/messages';
|
||||
|
||||
const initialize = (doc: Document): HTMLIFrameElement => {
|
||||
let iframe = doc.createElement('iframe');
|
||||
iframe.src = browser.runtime.getURL('build/console.html');
|
||||
iframe.id = 'vimvixen-console-frame';
|
||||
iframe.className = 'vimvixen-console-frame';
|
||||
doc.body.append(iframe);
|
||||
|
||||
return iframe;
|
||||
};
|
||||
|
||||
const blur = (doc: Document) => {
|
||||
let ele = doc.getElementById('vimvixen-console-frame') as HTMLIFrameElement;
|
||||
ele.blur();
|
||||
};
|
||||
|
||||
const postError = (text: string): Promise<any> => {
|
||||
return browser.runtime.sendMessage({
|
||||
type: messages.CONSOLE_FRAME_MESSAGE,
|
||||
message: {
|
||||
type: messages.CONSOLE_SHOW_ERROR,
|
||||
text,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const postInfo = (text: string): Promise<any> => {
|
||||
return browser.runtime.sendMessage({
|
||||
type: messages.CONSOLE_FRAME_MESSAGE,
|
||||
message: {
|
||||
type: messages.CONSOLE_SHOW_INFO,
|
||||
text,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export { initialize, blur, postError, postInfo };
|
19
src/content/controllers/AddonEnabledController.ts
Normal file
19
src/content/controllers/AddonEnabledController.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
16
src/content/controllers/ConsoleFrameController.ts
Normal file
16
src/content/controllers/ConsoleFrameController.ts
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
24
src/content/controllers/FindController.ts
Normal file
24
src/content/controllers/FindController.ts
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
21
src/content/controllers/FollowKeyController.ts
Normal file
21
src/content/controllers/FollowKeyController.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
31
src/content/controllers/FollowMasterController.ts
Normal file
31
src/content/controllers/FollowMasterController.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
32
src/content/controllers/FollowSlaveController.ts
Normal file
32
src/content/controllers/FollowSlaveController.ts
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
148
src/content/controllers/KeymapController.ts
Normal file
148
src/content/controllers/KeymapController.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
16
src/content/controllers/MarkController.ts
Normal file
16
src/content/controllers/MarkController.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
31
src/content/controllers/MarkKeyController.ts
Normal file
31
src/content/controllers/MarkKeyController.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
41
src/content/controllers/SettingController.ts
Normal file
41
src/content/controllers/SettingController.ts
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
export interface Key {
|
||||
export default interface Key {
|
||||
key: string;
|
||||
shiftKey: boolean | undefined;
|
||||
ctrlKey: boolean | undefined;
|
||||
altKey: boolean | undefined;
|
||||
metaKey: boolean | undefined;
|
||||
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 };
|
64
src/content/domains/KeySequence.ts
Normal file
64
src/content/domains/KeySequence.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import Key, * as keyUtils from './Key';
|
||||
|
||||
export default class KeySequence {
|
||||
private keys: Key[];
|
||||
|
||||
private constructor(keys: Key[]) {
|
||||
this.keys = keys;
|
||||
}
|
||||
|
||||
static from(keys: Key[]): KeySequence {
|
||||
return new KeySequence(keys);
|
||||
}
|
||||
|
||||
push(key: Key): number {
|
||||
return this.keys.push(key);
|
||||
}
|
||||
|
||||
length(): number {
|
||||
return this.keys.length;
|
||||
}
|
||||
|
||||
startsWith(o: KeySequence): boolean {
|
||||
if (this.keys.length < o.keys.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < o.keys.length; ++i) {
|
||||
if (!keyUtils.equals(this.keys[i], o.keys[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
getKeyArray(): Key[] {
|
||||
return this.keys;
|
||||
}
|
||||
}
|
||||
|
||||
export const fromMapKeys = (keys: string): KeySequence => {
|
||||
const fromMapKeysRecursive = (
|
||||
remainings: string, mappedKeys: Key[],
|
||||
): Key[] => {
|
||||
if (remainings.length === 0) {
|
||||
return mappedKeys;
|
||||
}
|
||||
|
||||
let nextPos = 1;
|
||||
if (remainings.startsWith('<')) {
|
||||
let ltPos = remainings.indexOf('>');
|
||||
if (ltPos > 0) {
|
||||
nextPos = ltPos + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return fromMapKeysRecursive(
|
||||
remainings.slice(nextPos),
|
||||
mappedKeys.concat([keyUtils.fromMapKey(remainings.slice(0, nextPos))])
|
||||
);
|
||||
};
|
||||
|
||||
let data = fromMapKeysRecursive(keys, []);
|
||||
return KeySequence.from(data);
|
||||
};
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import * as doms from '../shared/utils/dom';
|
||||
|
||||
const focusInput = (): void => {
|
||||
let inputTypes = ['email', 'number', 'search', 'tel', 'text', 'url'];
|
||||
let inputSelector = inputTypes.map(type => `input[type=${type}]`).join(',');
|
||||
let targets = window.document.querySelectorAll(inputSelector + ',textarea');
|
||||
let target = Array.from(targets).find(doms.isVisible);
|
||||
if (target instanceof HTMLInputElement) {
|
||||
target.focus();
|
||||
} else if (target instanceof HTMLTextAreaElement) {
|
||||
target.focus();
|
||||
}
|
||||
};
|
||||
|
||||
export { focusInput };
|
|
@ -1,16 +1,16 @@
|
|||
import TopContentComponent from './components/top-content';
|
||||
import FrameContentComponent from './components/frame-content';
|
||||
import { ConsoleFramePresenterImpl } from './presenters/ConsoleFramePresenter';
|
||||
import consoleFrameStyle from './site-style';
|
||||
import { newStore } from './store';
|
||||
|
||||
const store = newStore();
|
||||
import * as routes from './routes';
|
||||
|
||||
if (window.self === window.top) {
|
||||
new TopContentComponent(window, store); // eslint-disable-line no-new
|
||||
} else {
|
||||
new FrameContentComponent(window, store); // eslint-disable-line no-new
|
||||
routes.routeMasterComponents();
|
||||
|
||||
new ConsoleFramePresenterImpl().initialize();
|
||||
}
|
||||
|
||||
routes.routeComponents();
|
||||
|
||||
|
||||
let style = window.document.createElement('style');
|
||||
style.textContent = consoleFrameStyle;
|
||||
window.document.head.appendChild(style);
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
const REL_PATTERN: {[key: string]: RegExp} = {
|
||||
prev: /^(?:prev(?:ious)?|older)\b|\u2039|\u2190|\xab|\u226a|<</i,
|
||||
next: /^(?:next|newer)\b|\u203a|\u2192|\xbb|\u226b|>>/i,
|
||||
};
|
||||
|
||||
// Return the last element in the document matching the supplied selector
|
||||
// and the optional filter, or null if there are no matches.
|
||||
// eslint-disable-next-line func-style
|
||||
function selectLast<E extends Element>(
|
||||
win: Window,
|
||||
selector: string,
|
||||
filter?: (e: E) => boolean,
|
||||
): E | null {
|
||||
let nodes = Array.from(
|
||||
win.document.querySelectorAll(selector) as NodeListOf<E>
|
||||
);
|
||||
|
||||
if (filter) {
|
||||
nodes = nodes.filter(filter);
|
||||
}
|
||||
return nodes.length ? nodes[nodes.length - 1] : null;
|
||||
}
|
||||
|
||||
const historyPrev = (win: Window): void => {
|
||||
win.history.back();
|
||||
};
|
||||
|
||||
const historyNext = (win: Window): void => {
|
||||
win.history.forward();
|
||||
};
|
||||
|
||||
// Code common to linkPrev and linkNext which navigates to the specified page.
|
||||
const linkRel = (win: Window, rel: string): void => {
|
||||
let link = selectLast<HTMLLinkElement>(win, `link[rel~=${rel}][href]`);
|
||||
if (link) {
|
||||
win.location.href = link.href;
|
||||
return;
|
||||
}
|
||||
|
||||
const pattern = REL_PATTERN[rel];
|
||||
|
||||
let a = selectLast<HTMLAnchorElement>(win, `a[rel~=${rel}][href]`) ||
|
||||
// `innerText` is much slower than `textContent`, but produces much better
|
||||
// (i.e. less unexpected) results
|
||||
selectLast(win, 'a[href]', lnk => pattern.test(lnk.innerText));
|
||||
|
||||
if (a) {
|
||||
a.click();
|
||||
}
|
||||
};
|
||||
|
||||
const linkPrev = (win: Window): void => {
|
||||
linkRel(win, 'prev');
|
||||
};
|
||||
|
||||
const linkNext = (win: Window): void => {
|
||||
linkRel(win, 'next');
|
||||
};
|
||||
|
||||
const parent = (win: Window): void => {
|
||||
const loc = win.location;
|
||||
if (loc.hash !== '') {
|
||||
loc.hash = '';
|
||||
return;
|
||||
} else if (loc.search !== '') {
|
||||
loc.search = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const basenamePattern = /\/[^/]+$/;
|
||||
const lastDirPattern = /\/[^/]+\/$/;
|
||||
if (basenamePattern.test(loc.pathname)) {
|
||||
loc.pathname = loc.pathname.replace(basenamePattern, '/');
|
||||
} else if (lastDirPattern.test(loc.pathname)) {
|
||||
loc.pathname = loc.pathname.replace(lastDirPattern, '/');
|
||||
}
|
||||
};
|
||||
|
||||
const root = (win: Window): void => {
|
||||
win.location.href = win.location.origin;
|
||||
};
|
||||
|
||||
export { historyPrev, historyNext, linkPrev, linkNext, parent, root };
|
25
src/content/presenters/ConsoleFramePresenter.ts
Normal file
25
src/content/presenters/ConsoleFramePresenter.ts
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
52
src/content/presenters/FindPresenter.ts
Normal file
52
src/content/presenters/FindPresenter.ts
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
25
src/content/presenters/FocusPresenter.ts
Normal file
25
src/content/presenters/FocusPresenter.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
134
src/content/presenters/FollowPresenter.ts
Normal file
134
src/content/presenters/FollowPresenter.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
127
src/content/presenters/Hint.ts
Normal file
127
src/content/presenters/Hint.ts
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
98
src/content/presenters/NavigationPresenter.ts
Normal file
98
src/content/presenters/NavigationPresenter.ts
Normal file
|
@ -0,0 +1,98 @@
|
|||
export default interface NavigationPresenter {
|
||||
openHistoryPrev(): void;
|
||||
|
||||
openHistoryNext(): void;
|
||||
|
||||
openLinkPrev(): void;
|
||||
|
||||
openLinkNext(): void;
|
||||
|
||||
openParent(): void;
|
||||
|
||||
openRoot(): void;
|
||||
|
||||
// eslint-disable-next-line semi
|
||||
}
|
||||
|
||||
const REL_PATTERN: {[key: string]: RegExp} = {
|
||||
prev: /^(?:prev(?:ious)?|older)\b|\u2039|\u2190|\xab|\u226a|<</i,
|
||||
next: /^(?:next|newer)\b|\u203a|\u2192|\xbb|\u226b|>>/i,
|
||||
};
|
||||
|
||||
// Return the last element in the document matching the supplied selector
|
||||
// and the optional filter, or null if there are no matches.
|
||||
// eslint-disable-next-line func-style
|
||||
function selectLast<E extends Element>(
|
||||
selector: string,
|
||||
filter?: (e: E) => boolean,
|
||||
): E | null {
|
||||
let nodes = Array.from(
|
||||
window.document.querySelectorAll(selector) as NodeListOf<E>
|
||||
);
|
||||
|
||||
if (filter) {
|
||||
nodes = nodes.filter(filter);
|
||||
}
|
||||
return nodes.length ? nodes[nodes.length - 1] : null;
|
||||
}
|
||||
|
||||
export class NavigationPresenterImpl implements NavigationPresenter {
|
||||
openHistoryPrev(): void {
|
||||
window.history.back();
|
||||
}
|
||||
|
||||
openHistoryNext(): void {
|
||||
window.history.forward();
|
||||
}
|
||||
|
||||
openLinkPrev(): void {
|
||||
this.linkRel('prev');
|
||||
}
|
||||
|
||||
openLinkNext(): void {
|
||||
this.linkRel('next');
|
||||
}
|
||||
|
||||
openParent(): void {
|
||||
const loc = window.location;
|
||||
if (loc.hash !== '') {
|
||||
loc.hash = '';
|
||||
return;
|
||||
} else if (loc.search !== '') {
|
||||
loc.search = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const basenamePattern = /\/[^/]+$/;
|
||||
const lastDirPattern = /\/[^/]+\/$/;
|
||||
if (basenamePattern.test(loc.pathname)) {
|
||||
loc.pathname = loc.pathname.replace(basenamePattern, '/');
|
||||
} else if (lastDirPattern.test(loc.pathname)) {
|
||||
loc.pathname = loc.pathname.replace(lastDirPattern, '/');
|
||||
}
|
||||
}
|
||||
|
||||
openRoot(): void {
|
||||
window.location.href = window.location.origin;
|
||||
}
|
||||
|
||||
// Code common to linkPrev and linkNext which navigates to the specified page.
|
||||
private linkRel(rel: 'prev' | 'next'): void {
|
||||
let link = selectLast<HTMLLinkElement>(`link[rel~=${rel}][href]`);
|
||||
if (link) {
|
||||
window.location.href = link.href;
|
||||
return;
|
||||
}
|
||||
|
||||
const pattern = REL_PATTERN[rel];
|
||||
|
||||
let a = selectLast<HTMLAnchorElement>(`a[rel~=${rel}][href]`) ||
|
||||
// `innerText` is much slower than `textContent`, but produces much better
|
||||
// (i.e. less unexpected) results
|
||||
selectLast('a[href]', lnk => pattern.test(lnk.innerText));
|
||||
|
||||
if (a) {
|
||||
a.click();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,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,30 +94,47 @@ class Scroller {
|
|||
}
|
||||
}
|
||||
|
||||
const getScroll = () => {
|
||||
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
|
||||
}
|
||||
|
||||
export class ScrollPresenterImpl {
|
||||
getScroll(): Point {
|
||||
let target = scrollTarget();
|
||||
return { x: target.scrollLeft, y: target.scrollTop };
|
||||
};
|
||||
}
|
||||
|
||||
const scrollVertically = (count: number, smooth: boolean): void => {
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
const scrollHorizonally = (count: number, smooth: boolean): void => {
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
const scrollPages = (count: number, smooth: boolean): void => {
|
||||
scrollPages(count: number, smooth: boolean): void {
|
||||
let target = scrollTarget();
|
||||
let height = target.clientHeight;
|
||||
let delta = height * count;
|
||||
|
@ -125,44 +142,38 @@ const scrollPages = (count: number, smooth: boolean): void => {
|
|||
delta = height * count;
|
||||
}
|
||||
new Scroller(target, smooth).scrollBy(0, delta);
|
||||
};
|
||||
}
|
||||
|
||||
const scrollTo = (x: number, y: number, smooth: boolean): void => {
|
||||
scrollTo(x: number, y: number, smooth: boolean): void {
|
||||
let target = scrollTarget();
|
||||
new Scroller(target, smooth).scrollTo(x, y);
|
||||
};
|
||||
}
|
||||
|
||||
const scrollToTop = (smooth: boolean): void => {
|
||||
scrollToTop(smooth: boolean): void {
|
||||
let target = scrollTarget();
|
||||
let x = target.scrollLeft;
|
||||
let y = 0;
|
||||
new Scroller(target, smooth).scrollTo(x, y);
|
||||
};
|
||||
}
|
||||
|
||||
const scrollToBottom = (smooth: boolean): void => {
|
||||
scrollToBottom(smooth: boolean): void {
|
||||
let target = scrollTarget();
|
||||
let x = target.scrollLeft;
|
||||
let y = target.scrollHeight;
|
||||
new Scroller(target, smooth).scrollTo(x, y);
|
||||
};
|
||||
}
|
||||
|
||||
const scrollToHome = (smooth: boolean): void => {
|
||||
scrollToHome(smooth: boolean): void {
|
||||
let target = scrollTarget();
|
||||
let x = 0;
|
||||
let y = target.scrollTop;
|
||||
new Scroller(target, smooth).scrollTo(x, y);
|
||||
};
|
||||
}
|
||||
|
||||
const scrollToEnd = (smooth: boolean): void => {
|
||||
scrollToEnd(smooth: boolean): void {
|
||||
let target = scrollTarget();
|
||||
let x = target.scrollWidth;
|
||||
let y = target.scrollTop;
|
||||
new Scroller(target, smooth).scrollTo(x, y);
|
||||
};
|
||||
|
||||
export {
|
||||
getScroll,
|
||||
scrollVertically, scrollHorizonally, scrollPages,
|
||||
scrollTo,
|
||||
scrollToTop, scrollToBottom, scrollToHome, scrollToEnd
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
import * as actions from '../actions';
|
||||
|
||||
export interface State {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
const defaultState: State = {
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
export default function reducer(
|
||||
state: State = defaultState,
|
||||
action: actions.AddonAction,
|
||||
): State {
|
||||
switch (action.type) {
|
||||
case actions.ADDON_SET_ENABLED:
|
||||
return { ...state,
|
||||
enabled: action.enabled, };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
import * as actions from '../actions';
|
||||
|
||||
export interface State {
|
||||
keyword: string | null;
|
||||
found: boolean;
|
||||
}
|
||||
|
||||
const defaultState: State = {
|
||||
keyword: null,
|
||||
found: false,
|
||||
};
|
||||
|
||||
export default function reducer(
|
||||
state: State = defaultState,
|
||||
action: actions.FindAction,
|
||||
): State {
|
||||
switch (action.type) {
|
||||
case actions.FIND_SET_KEYWORD:
|
||||
return { ...state,
|
||||
keyword: action.keyword,
|
||||
found: action.found, };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
import * as actions from '../actions';
|
||||
|
||||
export interface State {
|
||||
enabled: boolean;
|
||||
newTab: boolean;
|
||||
background: boolean;
|
||||
keys: string,
|
||||
}
|
||||
|
||||
const defaultState: State = {
|
||||
enabled: false,
|
||||
newTab: false,
|
||||
background: false,
|
||||
keys: '',
|
||||
};
|
||||
|
||||
export default function reducer(
|
||||
state: State = defaultState,
|
||||
action: actions.FollowAction,
|
||||
): State {
|
||||
switch (action.type) {
|
||||
case actions.FOLLOW_CONTROLLER_ENABLE:
|
||||
return { ...state,
|
||||
enabled: true,
|
||||
newTab: action.newTab,
|
||||
background: action.background,
|
||||
keys: '', };
|
||||
case actions.FOLLOW_CONTROLLER_DISABLE:
|
||||
return { ...state,
|
||||
enabled: false, };
|
||||
case actions.FOLLOW_CONTROLLER_KEY_PRESS:
|
||||
return { ...state,
|
||||
keys: state.keys + action.key, };
|
||||
case actions.FOLLOW_CONTROLLER_BACKSPACE:
|
||||
return { ...state,
|
||||
keys: state.keys.slice(0, -1), };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import { combineReducers } from 'redux';
|
||||
import addon, { State as AddonState } from './addon';
|
||||
import find, { State as FindState } from './find';
|
||||
import setting, { State as SettingState } from './setting';
|
||||
import input, { State as InputState } from './input';
|
||||
import followController, { State as FollowControllerState }
|
||||
from './follow-controller';
|
||||
import mark, { State as MarkState } from './mark';
|
||||
|
||||
export interface State {
|
||||
addon: AddonState;
|
||||
find: FindState;
|
||||
setting: SettingState;
|
||||
input: InputState;
|
||||
followController: FollowControllerState;
|
||||
mark: MarkState;
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
addon, find, setting, input, followController, mark,
|
||||
});
|
|
@ -1,26 +0,0 @@
|
|||
import * as actions from '../actions';
|
||||
import * as keyUtils from '../../shared/utils/keys';
|
||||
|
||||
export interface State {
|
||||
keys: keyUtils.Key[],
|
||||
}
|
||||
|
||||
const defaultState: State = {
|
||||
keys: []
|
||||
};
|
||||
|
||||
export default function reducer(
|
||||
state: State = defaultState,
|
||||
action: actions.InputAction,
|
||||
): State {
|
||||
switch (action.type) {
|
||||
case actions.INPUT_KEY_PRESS:
|
||||
return { ...state,
|
||||
keys: state.keys.concat([action.key]), };
|
||||
case actions.INPUT_CLEAR_KEYS:
|
||||
return { ...state,
|
||||
keys: [], };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
import Mark from '../Mark';
|
||||
import * as actions from '../actions';
|
||||
|
||||
export interface State {
|
||||
setMode: boolean;
|
||||
jumpMode: boolean;
|
||||
marks: { [key: string]: Mark };
|
||||
}
|
||||
|
||||
const defaultState: State = {
|
||||
setMode: false,
|
||||
jumpMode: false,
|
||||
marks: {},
|
||||
};
|
||||
|
||||
export default function reducer(
|
||||
state: State = defaultState,
|
||||
action: actions.MarkAction,
|
||||
): State {
|
||||
switch (action.type) {
|
||||
case actions.MARK_START_SET:
|
||||
return { ...state, setMode: true };
|
||||
case actions.MARK_START_JUMP:
|
||||
return { ...state, jumpMode: true };
|
||||
case actions.MARK_CANCEL:
|
||||
return { ...state, setMode: false, jumpMode: false };
|
||||
case actions.MARK_SET_LOCAL: {
|
||||
let marks = { ...state.marks };
|
||||
marks[action.key] = { x: action.x, y: action.y };
|
||||
return { ...state, setMode: false, marks };
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
import * as actions from '../actions';
|
||||
import * as keyUtils from '../../shared/utils/keys';
|
||||
import * as operations from '../../shared/operations';
|
||||
import { Search, Properties, DefaultSetting } from '../../shared/Settings';
|
||||
|
||||
export interface State {
|
||||
keymaps: { key: keyUtils.Key[], op: operations.Operation }[];
|
||||
search: Search;
|
||||
properties: Properties;
|
||||
}
|
||||
|
||||
// defaultState does not refer due to the state is load from
|
||||
// background on load.
|
||||
const defaultState: State = {
|
||||
keymaps: [],
|
||||
search: DefaultSetting.search,
|
||||
properties: DefaultSetting.properties,
|
||||
};
|
||||
|
||||
export default function reducer(
|
||||
state: State = defaultState,
|
||||
action: actions.SettingAction,
|
||||
): State {
|
||||
switch (action.type) {
|
||||
case actions.SETTING_SET:
|
||||
return {
|
||||
keymaps: Object.entries(action.settings.keymaps).map((entry) => {
|
||||
return {
|
||||
key: keyUtils.fromMapKeys(entry[0]),
|
||||
op: entry[1],
|
||||
};
|
||||
}),
|
||||
properties: action.settings.properties,
|
||||
search: action.settings.search,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
19
src/content/repositories/AddonEnabledRepository.ts
Normal file
19
src/content/repositories/AddonEnabledRepository.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
46
src/content/repositories/ClipboardRepository.ts
Normal file
46
src/content/repositories/ClipboardRepository.ts
Normal file
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
19
src/content/repositories/FindRepository.ts
Normal file
19
src/content/repositories/FindRepository.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
35
src/content/repositories/FollowKeyRepository.ts
Normal file
35
src/content/repositories/FollowKeyRepository.ts
Normal file
|
@ -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 = [];
|
||||
}
|
||||
}
|
59
src/content/repositories/FollowMasterRepository.ts
Normal file
59
src/content/repositories/FollowMasterRepository.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
31
src/content/repositories/FollowSlaveRepository.ts
Normal file
31
src/content/repositories/FollowSlaveRepository.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
24
src/content/repositories/KeymapRepository.ts
Normal file
24
src/content/repositories/KeymapRepository.ts
Normal file
|
@ -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([]);
|
||||
}
|
||||
}
|
52
src/content/repositories/MarkKeyRepository.ts
Normal file
52
src/content/repositories/MarkKeyRepository.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
25
src/content/repositories/MarkRepository.ts
Normal file
25
src/content/repositories/MarkRepository.ts
Normal file
|
@ -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 };
|
||||
}
|
||||
}
|
21
src/content/repositories/SettingRepository.ts
Normal file
21
src/content/repositories/SettingRepository.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
97
src/content/routes.ts
Normal file
97
src/content/routes.ts
Normal file
|
@ -0,0 +1,97 @@
|
|||
import MessageListener from './MessageListener';
|
||||
import FindController from './controllers/FindController';
|
||||
import MarkController from './controllers/MarkController';
|
||||
import FollowMasterController from './controllers/FollowMasterController';
|
||||
import FollowSlaveController from './controllers/FollowSlaveController';
|
||||
import FollowKeyController from './controllers/FollowKeyController';
|
||||
import InputDriver from './InputDriver';
|
||||
import KeymapController from './controllers/KeymapController';
|
||||
import AddonEnabledUseCase from './usecases/AddonEnabledUseCase';
|
||||
import MarkKeyController from './controllers/MarkKeyController';
|
||||
import AddonEnabledController from './controllers/AddonEnabledController';
|
||||
import SettingController from './controllers/SettingController';
|
||||
import ConsoleFrameController from './controllers/ConsoleFrameController';
|
||||
import * as messages from '../shared/messages';
|
||||
|
||||
export const routeComponents = () => {
|
||||
let listener = new MessageListener();
|
||||
|
||||
let followSlaveController = new FollowSlaveController();
|
||||
listener.onWebMessage((message: messages.Message) => {
|
||||
switch (message.type) {
|
||||
case messages.FOLLOW_REQUEST_COUNT_TARGETS:
|
||||
return followSlaveController.countTargets(message);
|
||||
case messages.FOLLOW_CREATE_HINTS:
|
||||
return followSlaveController.createHints(message);
|
||||
case messages.FOLLOW_SHOW_HINTS:
|
||||
return followSlaveController.showHints(message);
|
||||
case messages.FOLLOW_ACTIVATE:
|
||||
return followSlaveController.activate(message);
|
||||
case messages.FOLLOW_REMOVE_HINTS:
|
||||
return followSlaveController.clear(message);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
let keymapController = new KeymapController();
|
||||
let markKeyController = new MarkKeyController();
|
||||
let followKeyController = new FollowKeyController();
|
||||
let inputDriver = new InputDriver(document.body);
|
||||
inputDriver.onKey(key => followKeyController.press(key));
|
||||
inputDriver.onKey(key => markKeyController.press(key));
|
||||
inputDriver.onKey(key => keymapController.press(key));
|
||||
|
||||
let settingController = new SettingController();
|
||||
settingController.initSettings();
|
||||
|
||||
listener.onBackgroundMessage((message: messages.Message): any => {
|
||||
let addonEnabledUseCase = new AddonEnabledUseCase();
|
||||
|
||||
switch (message.type) {
|
||||
case messages.SETTINGS_CHANGED:
|
||||
return settingController.reloadSettings(message);
|
||||
case messages.ADDON_TOGGLE_ENABLED:
|
||||
return addonEnabledUseCase.toggle();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const routeMasterComponents = () => {
|
||||
let listener = new MessageListener();
|
||||
|
||||
let findController = new FindController();
|
||||
let followMasterController = new FollowMasterController();
|
||||
let markController = new MarkController();
|
||||
let addonEnabledController = new AddonEnabledController();
|
||||
let consoleFrameController = new ConsoleFrameController();
|
||||
|
||||
listener.onWebMessage((message: messages.Message, sender: Window) => {
|
||||
switch (message.type) {
|
||||
case messages.CONSOLE_ENTER_FIND:
|
||||
return findController.start(message);
|
||||
case messages.FIND_NEXT:
|
||||
return findController.next(message);
|
||||
case messages.FIND_PREV:
|
||||
return findController.prev(message);
|
||||
case messages.CONSOLE_UNFOCUS:
|
||||
return consoleFrameController.unfocus(message);
|
||||
case messages.FOLLOW_START:
|
||||
return followMasterController.followStart(message);
|
||||
case messages.FOLLOW_RESPONSE_COUNT_TARGETS:
|
||||
return followMasterController.responseCountTargets(message, sender);
|
||||
case messages.FOLLOW_KEY_PRESS:
|
||||
return followMasterController.keyPress(message);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
listener.onBackgroundMessage((message: messages.Message) => {
|
||||
switch (message.type) {
|
||||
case messages.ADDON_ENABLED_QUERY:
|
||||
return addonEnabledController.getAddonEnabled(message);
|
||||
case messages.TAB_SCROLL_TO:
|
||||
return markController.scrollTo(message);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
};
|
|
@ -1,8 +0,0 @@
|
|||
import promise from 'redux-promise';
|
||||
import reducers from '../reducers';
|
||||
import { createStore, applyMiddleware } from 'redux';
|
||||
|
||||
export const newStore = () => createStore(
|
||||
reducers,
|
||||
applyMiddleware(promise),
|
||||
);
|
|
@ -1,41 +0,0 @@
|
|||
import * as messages from '../shared/messages';
|
||||
import * as urls from '../shared/urls';
|
||||
import { Search } from '../shared/Settings';
|
||||
|
||||
const yank = (win: Window) => {
|
||||
let input = win.document.createElement('input');
|
||||
win.document.body.append(input);
|
||||
|
||||
input.style.position = 'fixed';
|
||||
input.style.top = '-100px';
|
||||
input.value = win.location.href;
|
||||
input.select();
|
||||
|
||||
win.document.execCommand('copy');
|
||||
|
||||
input.remove();
|
||||
};
|
||||
|
||||
const paste = (win: Window, newTab: boolean, search: Search) => {
|
||||
let textarea = win.document.createElement('textarea');
|
||||
win.document.body.append(textarea);
|
||||
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.top = '-100px';
|
||||
textarea.contentEditable = 'true';
|
||||
textarea.focus();
|
||||
|
||||
if (win.document.execCommand('paste')) {
|
||||
let value = textarea.textContent as string;
|
||||
let url = urls.searchUrl(value, search);
|
||||
browser.runtime.sendMessage({
|
||||
type: messages.OPEN_URL,
|
||||
url,
|
||||
newTab,
|
||||
});
|
||||
}
|
||||
|
||||
textarea.remove();
|
||||
};
|
||||
|
||||
export { yank, paste };
|
40
src/content/usecases/AddonEnabledUseCase.ts
Normal file
40
src/content/usecases/AddonEnabledUseCase.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
44
src/content/usecases/ClipboardUseCase.ts
Normal file
44
src/content/usecases/ClipboardUseCase.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
17
src/content/usecases/ConsoleFrameUseCase.ts
Normal file
17
src/content/usecases/ConsoleFrameUseCase.ts
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
20
src/content/usecases/FindSlaveUseCase.ts
Normal file
20
src/content/usecases/FindSlaveUseCase.ts
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
81
src/content/usecases/FindUseCase.ts
Normal file
81
src/content/usecases/FindUseCase.ts
Normal file
|
@ -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');
|
||||
}
|
||||
}
|
16
src/content/usecases/FocusUseCase.ts
Normal file
16
src/content/usecases/FocusUseCase.ts
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
150
src/content/usecases/FollowMasterUseCase.ts
Normal file
150
src/content/usecases/FollowMasterUseCase.ts
Normal file
|
@ -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('');
|
||||
}
|
||||
}
|
91
src/content/usecases/FollowSlaveUseCase.ts
Normal file
91
src/content/usecases/FollowSlaveUseCase.ts
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
38
src/content/usecases/HintKeyProducer.ts
Normal file
38
src/content/usecases/HintKeyProducer.ts
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
87
src/content/usecases/KeymapUseCase.ts
Normal file
87
src/content/usecases/KeymapUseCase.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
36
src/content/usecases/MarkKeyUseCase.ts
Normal file
36
src/content/usecases/MarkKeyUseCase.ts
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
66
src/content/usecases/MarkUseCase.ts
Normal file
66
src/content/usecases/MarkUseCase.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
36
src/content/usecases/NavigateUseCase.ts
Normal file
36
src/content/usecases/NavigateUseCase.ts
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
58
src/content/usecases/ScrollUseCase.ts
Normal file
58
src/content/usecases/ScrollUseCase.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
24
src/content/usecases/SettingUseCase.ts
Normal file
24
src/content/usecases/SettingUseCase.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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[];
|
||||
tags: string[];
|
||||
viewSize: { width: number, height: number };
|
||||
framePosition: { x: number, y: number };
|
||||
}
|
||||
|
||||
export interface FollowShowHintsMessage {
|
||||
type: typeof FOLLOW_SHOW_HINTS;
|
||||
prefix: string;
|
||||
}
|
||||
|
||||
export interface FollowRemoveHintsMessage {
|
||||
type: typeof FOLLOW_REMOVE_HINTS;
|
||||
}
|
||||
|
||||
export interface FollowActivateMessage {
|
||||
type: typeof FOLLOW_ACTIVATE;
|
||||
tag: string;
|
||||
newTab: boolean;
|
||||
background: boolean;
|
||||
}
|
||||
|
||||
interface FollowShowHintsMessage {
|
||||
type: typeof FOLLOW_SHOW_HINTS;
|
||||
keys: string;
|
||||
}
|
||||
|
||||
interface FollowRemoveHintsMessage {
|
||||
type: typeof FOLLOW_REMOVE_HINTS;
|
||||
}
|
||||
|
||||
interface FollowActivateMessage {
|
||||
type: typeof FOLLOW_ACTIVATE;
|
||||
keys: string;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
129
test/content/InputDriver.test.ts
Normal file
129
test/content/InputDriver.test.ts
Normal file
|
@ -0,0 +1,129 @@
|
|||
import InputDriver from '../../src/content/InputDriver';
|
||||
import { expect } from 'chai';
|
||||
import Key from '../../src/content/domains/Key';
|
||||
|
||||
describe('InputDriver', () => {
|
||||
let target: HTMLElement;
|
||||
let driver: InputDriver;
|
||||
|
||||
beforeEach(() => {
|
||||
target = document.createElement('div');
|
||||
document.body.appendChild(target);
|
||||
driver = new InputDriver(target);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
target.remove();
|
||||
target = null;
|
||||
driver = null;
|
||||
});
|
||||
|
||||
it('register callbacks', (done) => {
|
||||
driver.onKey((key: Key): boolean => {
|
||||
expect(key.key).to.equal('a');
|
||||
expect(key.ctrlKey).to.be.true;
|
||||
expect(key.shiftKey).to.be.false;
|
||||
expect(key.altKey).to.be.false;
|
||||
expect(key.metaKey).to.be.false;
|
||||
done();
|
||||
return true;
|
||||
});
|
||||
|
||||
target.dispatchEvent(new KeyboardEvent('keydown', {
|
||||
key: 'a',
|
||||
ctrlKey: true,
|
||||
shiftKey: false,
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
}));
|
||||
});
|
||||
|
||||
it('invoke callback once', () => {
|
||||
let a = 0, b = 0;
|
||||
driver.onKey((key: Key): boolean => {
|
||||
if (key.key == 'a') {
|
||||
++a;
|
||||
} else {
|
||||
key.key == 'b'
|
||||
++b;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
let events = [
|
||||
new KeyboardEvent('keydown', { key: 'a' }),
|
||||
new KeyboardEvent('keydown', { key: 'b' }),
|
||||
new KeyboardEvent('keypress', { key: 'a' }),
|
||||
new KeyboardEvent('keyup', { key: 'a' }),
|
||||
new KeyboardEvent('keypress', { key: 'b' }),
|
||||
new KeyboardEvent('keyup', { key: 'b' }),
|
||||
];
|
||||
for (let e of events) {
|
||||
target.dispatchEvent(e);
|
||||
}
|
||||
|
||||
expect(a).to.equal(1);
|
||||
expect(b).to.equal(1);
|
||||
})
|
||||
|
||||
it('propagates and stop handler chain', () => {
|
||||
let a = 0, b = 0, c = 0;
|
||||
driver.onKey((key: Key): boolean => {
|
||||
a++;
|
||||
return false;
|
||||
});
|
||||
driver.onKey((key: Key): boolean => {
|
||||
b++;
|
||||
return true;
|
||||
});
|
||||
driver.onKey((key: Key): boolean => {
|
||||
c++;
|
||||
return true;
|
||||
});
|
||||
|
||||
target.dispatchEvent(new KeyboardEvent('keydown', { key: 'b' }));
|
||||
|
||||
expect(a).to.equal(1);
|
||||
expect(b).to.equal(1);
|
||||
expect(c).to.equal(0);
|
||||
})
|
||||
|
||||
it('does not invoke only meta keys', () => {
|
||||
driver.onKey((key: Key): boolean=> {
|
||||
expect.fail();
|
||||
return false;
|
||||
});
|
||||
|
||||
target.dispatchEvent(new KeyboardEvent('keydown', { key: 'Shift' }));
|
||||
target.dispatchEvent(new KeyboardEvent('keydown', { key: 'Control' }));
|
||||
target.dispatchEvent(new KeyboardEvent('keydown', { key: 'Alt' }));
|
||||
target.dispatchEvent(new KeyboardEvent('keydown', { key: 'OS' }));
|
||||
})
|
||||
|
||||
it('ignores events from input elements', () => {
|
||||
['input', 'textarea', 'select'].forEach((name) => {
|
||||
let input = window.document.createElement(name);
|
||||
let driver = new InputDriver(input);
|
||||
driver.onKey((key: Key): boolean => {
|
||||
expect.fail();
|
||||
return false;
|
||||
});
|
||||
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'x' }));
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores events from contenteditable elements', () => {
|
||||
let div = window.document.createElement('div');
|
||||
let driver = new InputDriver(div);
|
||||
driver.onKey((key: Key): boolean => {
|
||||
expect.fail();
|
||||
return false;
|
||||
});
|
||||
|
||||
div.setAttribute('contenteditable', '');
|
||||
div.dispatchEvent(new KeyboardEvent('keydown', { key: 'x' }));
|
||||
|
||||
div.setAttribute('contenteditable', 'true');
|
||||
div.dispatchEvent(new KeyboardEvent('keydown', { key: 'x' }));
|
||||
});
|
||||
});
|
|
@ -1,34 +0,0 @@
|
|||
import * as actions from 'content/actions';
|
||||
import * as followControllerActions from 'content/actions/follow-controller';
|
||||
|
||||
describe('follow-controller actions', () => {
|
||||
describe('enable', () => {
|
||||
it('creates FOLLOW_CONTROLLER_ENABLE action', () => {
|
||||
let action = followControllerActions.enable(true);
|
||||
expect(action.type).to.equal(actions.FOLLOW_CONTROLLER_ENABLE);
|
||||
expect(action.newTab).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disable', () => {
|
||||
it('creates FOLLOW_CONTROLLER_DISABLE action', () => {
|
||||
let action = followControllerActions.disable(true);
|
||||
expect(action.type).to.equal(actions.FOLLOW_CONTROLLER_DISABLE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyPress', () => {
|
||||
it('creates FOLLOW_CONTROLLER_KEY_PRESS action', () => {
|
||||
let action = followControllerActions.keyPress(100);
|
||||
expect(action.type).to.equal(actions.FOLLOW_CONTROLLER_KEY_PRESS);
|
||||
expect(action.key).to.equal(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('backspace', () => {
|
||||
it('creates FOLLOW_CONTROLLER_BACKSPACE action', () => {
|
||||
let action = followControllerActions.backspace(100);
|
||||
expect(action.type).to.equal(actions.FOLLOW_CONTROLLER_BACKSPACE);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,19 +0,0 @@
|
|||
import * as actions from 'content/actions';
|
||||
import * as inputActions from 'content/actions/input';
|
||||
|
||||
describe("input actions", () => {
|
||||
describe("keyPress", () => {
|
||||
it('create INPUT_KEY_PRESS action', () => {
|
||||
let action = inputActions.keyPress('a');
|
||||
expect(action.type).to.equal(actions.INPUT_KEY_PRESS);
|
||||
expect(action.key).to.equal('a');
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearKeys", () => {
|
||||
it('create INPUT_CLEAR_KEYSaction', () => {
|
||||
let action = inputActions.clearKeys();
|
||||
expect(action.type).to.equal(actions.INPUT_CLEAR_KEYS);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,35 +0,0 @@
|
|||
import * as actions from 'content/actions';
|
||||
import * as markActions from 'content/actions/mark';
|
||||
|
||||
describe('mark actions', () => {
|
||||
describe('startSet', () => {
|
||||
it('create MARK_START_SET action', () => {
|
||||
let action = markActions.startSet();
|
||||
expect(action.type).to.equal(actions.MARK_START_SET);
|
||||
});
|
||||
});
|
||||
|
||||
describe('startJump', () => {
|
||||
it('create MARK_START_JUMP action', () => {
|
||||
let action = markActions.startJump();
|
||||
expect(action.type).to.equal(actions.MARK_START_JUMP);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancel', () => {
|
||||
it('create MARK_CANCEL action', () => {
|
||||
let action = markActions.cancel();
|
||||
expect(action.type).to.equal(actions.MARK_CANCEL);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setLocal', () => {
|
||||
it('create setLocal action', () => {
|
||||
let action = markActions.setLocal('a', 20, 30);
|
||||
expect(action.type).to.equal(actions.MARK_SET_LOCAL);
|
||||
expect(action.key).to.equal('a');
|
||||
expect(action.x).to.equal(20);
|
||||
expect(action.y).to.equal(30);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,43 +0,0 @@
|
|||
import * as actions from 'content/actions';
|
||||
import * as settingActions from 'content/actions/setting';
|
||||
|
||||
describe("setting actions", () => {
|
||||
describe("set", () => {
|
||||
it('create SETTING_SET action', () => {
|
||||
let action = settingActions.set({
|
||||
keymaps: {
|
||||
'dd': 'remove current tab',
|
||||
'z<C-A>': 'increment',
|
||||
},
|
||||
search: {
|
||||
default: "google",
|
||||
engines: {
|
||||
google: 'https://google.com/search?q={}',
|
||||
}
|
||||
},
|
||||
properties: {
|
||||
hintchars: 'abcd1234',
|
||||
},
|
||||
blacklist: [],
|
||||
});
|
||||
expect(action.type).to.equal(actions.SETTING_SET);
|
||||
expect(action.settings.properties.hintchars).to.equal('abcd1234');
|
||||
});
|
||||
|
||||
it('overrides cancel keys', () => {
|
||||
let action = settingActions.set({
|
||||
keymaps: {
|
||||
"k": { "type": "scroll.vertically", "count": -1 },
|
||||
"j": { "type": "scroll.vertically", "count": 1 },
|
||||
}
|
||||
});
|
||||
let keymaps = action.settings.keymaps;
|
||||
expect(action.settings.keymaps).to.deep.equals({
|
||||
"k": { type: "scroll.vertically", count: -1 },
|
||||
"j": { type: "scroll.vertically", count: 1 },
|
||||
'<Esc>': { type: 'cancel' },
|
||||
'<C-[>': { type: 'cancel' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,17 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<a id='visible_a' href='#' >link</a>
|
||||
<a href='#' style='display:none'>invisible 1</a>
|
||||
<a href='#' style='visibility:hidden'>invisible 2</a>
|
||||
<i>not link<i>
|
||||
<div id='editable_div_1' contenteditable>link</div>
|
||||
<div id='editable_div_2' contenteditable='true'>link</div>
|
||||
<div id='x' contenteditable='false'>link</div>
|
||||
<details>
|
||||
<summary id='summary_1'>summary link</summary>
|
||||
Some details
|
||||
<a href='#'>not visible</a>
|
||||
</details>
|
||||
</body>
|
||||
</html>
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue