Types src/content
This commit is contained in:
parent
992b3ac65d
commit
d01db82c0d
62 changed files with 1411 additions and 468 deletions
32
src/content/MessageListener.ts
Normal file
32
src/content/MessageListener.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
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,
|
||||
) {
|
||||
window.addEventListener('message', (event: MessageEvent) => {
|
||||
let sender = event.source;
|
||||
let message = null;
|
||||
try {
|
||||
message = JSON.parse(event.data);
|
||||
} catch (e) {
|
||||
// ignore unexpected message
|
||||
return;
|
||||
}
|
||||
listener(message, sender);
|
||||
});
|
||||
}
|
||||
|
||||
onBackgroundMessage(
|
||||
listener: (msg: Message, sender: WebExtMessageSender) => any,
|
||||
) {
|
||||
browser.runtime.onMessage.addListener(
|
||||
(msg: any, sender: WebExtMessageSender) => {
|
||||
listener(valueOf(msg), sender);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
import messages from 'shared/messages';
|
||||
import actions from 'content/actions';
|
||||
import * as messages from '../../shared/messages';
|
||||
import * as actions from './index';
|
||||
|
||||
const enable = () => setEnabled(true);
|
||||
const enable = (): Promise<actions.AddonAction> => setEnabled(true);
|
||||
|
||||
const disable = () => setEnabled(false);
|
||||
const disable = (): Promise<actions.AddonAction> => setEnabled(false);
|
||||
|
||||
const setEnabled = async(enabled) => {
|
||||
const setEnabled = async(enabled: boolean): Promise<actions.AddonAction> => {
|
||||
await browser.runtime.sendMessage({
|
||||
type: messages.ADDON_ENABLED_RESPONSE,
|
||||
enabled,
|
||||
|
|
|
@ -5,28 +5,41 @@
|
|||
// NOTE: window.find is not standard API
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Window/find
|
||||
|
||||
import messages from 'shared/messages';
|
||||
import actions from 'content/actions';
|
||||
import * as messages from '../../shared/messages';
|
||||
import * as actions from './index';
|
||||
import * as consoleFrames from '../console-frames';
|
||||
|
||||
const find = (string, backwards) => {
|
||||
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
|
||||
let found = window.find(string, caseSensitive, backwards, wrapScan);
|
||||
|
||||
// eslint-disable-next-line no-extra-parens
|
||||
let found = (<any>window).find(str, caseSensitive, backwards, wrapScan);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
window.getSelection().removeAllRanges();
|
||||
return window.find(string, caseSensitive, backwards, wrapScan);
|
||||
let sel = window.getSelection();
|
||||
if (sel) {
|
||||
sel.removeAllRanges();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-extra-parens
|
||||
return (<any>window).find(str, caseSensitive, backwards, wrapScan);
|
||||
};
|
||||
|
||||
const findNext = async(currentKeyword, reset, backwards) => {
|
||||
// eslint-disable-next-line max-statements
|
||||
const findNext = async(
|
||||
currentKeyword: string, reset: boolean, backwards: boolean,
|
||||
): Promise<actions.FindAction> => {
|
||||
if (reset) {
|
||||
window.getSelection().removeAllRanges();
|
||||
let sel = window.getSelection();
|
||||
if (sel) {
|
||||
sel.removeAllRanges();
|
||||
}
|
||||
}
|
||||
|
||||
let keyword = currentKeyword;
|
||||
|
@ -41,7 +54,8 @@ const findNext = async(currentKeyword, reset, backwards) => {
|
|||
});
|
||||
}
|
||||
if (!keyword) {
|
||||
return consoleFrames.postError('No previous search keywords');
|
||||
await consoleFrames.postError('No previous search keywords');
|
||||
return { type: actions.NOOP };
|
||||
}
|
||||
let found = find(keyword, backwards);
|
||||
if (found) {
|
||||
|
@ -57,11 +71,15 @@ const findNext = async(currentKeyword, reset, backwards) => {
|
|||
};
|
||||
};
|
||||
|
||||
const next = (currentKeyword, reset) => {
|
||||
const next = (
|
||||
currentKeyword: string, reset: boolean,
|
||||
): Promise<actions.FindAction> => {
|
||||
return findNext(currentKeyword, reset, false);
|
||||
};
|
||||
|
||||
const prev = (currentKeyword, reset) => {
|
||||
const prev = (
|
||||
currentKeyword: string, reset: boolean,
|
||||
): Promise<actions.FindAction> => {
|
||||
return findNext(currentKeyword, reset, true);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import actions from 'content/actions';
|
||||
import * as actions from './index';
|
||||
|
||||
const enable = (newTab, background) => {
|
||||
const enable = (
|
||||
newTab: boolean, background: boolean,
|
||||
): actions.FollowAction => {
|
||||
return {
|
||||
type: actions.FOLLOW_CONTROLLER_ENABLE,
|
||||
newTab,
|
||||
|
@ -8,20 +10,20 @@ const enable = (newTab, background) => {
|
|||
};
|
||||
};
|
||||
|
||||
const disable = () => {
|
||||
const disable = (): actions.FollowAction => {
|
||||
return {
|
||||
type: actions.FOLLOW_CONTROLLER_DISABLE,
|
||||
};
|
||||
};
|
||||
|
||||
const keyPress = (key) => {
|
||||
const keyPress = (key: string): actions.FollowAction => {
|
||||
return {
|
||||
type: actions.FOLLOW_CONTROLLER_KEY_PRESS,
|
||||
key: key
|
||||
};
|
||||
};
|
||||
|
||||
const backspace = () => {
|
||||
const backspace = (): actions.FollowAction => {
|
||||
return {
|
||||
type: actions.FOLLOW_CONTROLLER_BACKSPACE,
|
||||
};
|
||||
|
|
|
@ -1,31 +1,120 @@
|
|||
export default {
|
||||
// Enable/disable
|
||||
ADDON_SET_ENABLED: 'addon.set.enabled',
|
||||
import Redux from 'redux';
|
||||
|
||||
// Settings
|
||||
SETTING_SET: 'setting.set',
|
||||
// Enable/disable
|
||||
export const ADDON_SET_ENABLED = 'addon.set.enabled';
|
||||
|
||||
// User input
|
||||
INPUT_KEY_PRESS: 'input.key.press',
|
||||
INPUT_CLEAR_KEYS: 'input.clear.keys',
|
||||
// Find
|
||||
export const FIND_SET_KEYWORD = 'find.set.keyword';
|
||||
|
||||
// Completion
|
||||
COMPLETION_SET_ITEMS: 'completion.set.items',
|
||||
COMPLETION_SELECT_NEXT: 'completions.select.next',
|
||||
COMPLETION_SELECT_PREV: 'completions.select.prev',
|
||||
// Settings
|
||||
export const SETTING_SET = 'setting.set';
|
||||
|
||||
// Follow
|
||||
FOLLOW_CONTROLLER_ENABLE: 'follow.controller.enable',
|
||||
FOLLOW_CONTROLLER_DISABLE: 'follow.controller.disable',
|
||||
FOLLOW_CONTROLLER_KEY_PRESS: 'follow.controller.key.press',
|
||||
FOLLOW_CONTROLLER_BACKSPACE: 'follow.controller.backspace',
|
||||
// User input
|
||||
export const INPUT_KEY_PRESS = 'input.key.press';
|
||||
export const INPUT_CLEAR_KEYS = 'input.clear.keys';
|
||||
|
||||
// Find
|
||||
FIND_SET_KEYWORD: 'find.set.keyword',
|
||||
// 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';
|
||||
|
||||
// Mark
|
||||
MARK_START_SET: 'mark.start.set',
|
||||
MARK_START_JUMP: 'mark.start.jump',
|
||||
MARK_CANCEL: 'mark.cancel',
|
||||
MARK_SET_LOCAL: 'mark.set.local',
|
||||
};
|
||||
// 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;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export interface InputKeyPressAction extends Redux.Action {
|
||||
type: typeof INPUT_KEY_PRESS;
|
||||
key: string;
|
||||
}
|
||||
|
||||
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,13 +1,13 @@
|
|||
import actions from 'content/actions';
|
||||
import * as actions from './index';
|
||||
|
||||
const keyPress = (key) => {
|
||||
const keyPress = (key: string): actions.InputAction => {
|
||||
return {
|
||||
type: actions.INPUT_KEY_PRESS,
|
||||
key,
|
||||
};
|
||||
};
|
||||
|
||||
const clearKeys = () => {
|
||||
const clearKeys = (): actions.InputAction => {
|
||||
return {
|
||||
type: actions.INPUT_CLEAR_KEYS
|
||||
};
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import actions from 'content/actions';
|
||||
import messages from 'shared/messages';
|
||||
import * as actions from './index';
|
||||
import * as messages from '../../shared/messages';
|
||||
|
||||
const startSet = () => {
|
||||
const startSet = (): actions.MarkAction => {
|
||||
return { type: actions.MARK_START_SET };
|
||||
};
|
||||
|
||||
const startJump = () => {
|
||||
const startJump = (): actions.MarkAction => {
|
||||
return { type: actions.MARK_START_JUMP };
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
const cancel = (): actions.MarkAction => {
|
||||
return { type: actions.MARK_CANCEL };
|
||||
};
|
||||
|
||||
const setLocal = (key, x, y) => {
|
||||
const setLocal = (key: string, x: number, y: number): actions.MarkAction => {
|
||||
return {
|
||||
type: actions.MARK_SET_LOCAL,
|
||||
key,
|
||||
|
@ -22,22 +22,22 @@ const setLocal = (key, x, y) => {
|
|||
};
|
||||
};
|
||||
|
||||
const setGlobal = (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: '' };
|
||||
return { type: actions.NOOP };
|
||||
};
|
||||
|
||||
const jumpGlobal = (key) => {
|
||||
const jumpGlobal = (key: string): actions.MarkAction => {
|
||||
browser.runtime.sendMessage({
|
||||
type: messages.MARK_JUMP_GLOBAL,
|
||||
key,
|
||||
});
|
||||
return { type: '' };
|
||||
return { type: actions.NOOP };
|
||||
};
|
||||
|
||||
export {
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
import operations from 'shared/operations';
|
||||
import messages from 'shared/messages';
|
||||
import * as scrolls from 'content/scrolls';
|
||||
import * as navigates from 'content/navigates';
|
||||
import * as focuses from 'content/focuses';
|
||||
import * as urls from 'content/urls';
|
||||
import * as consoleFrames from 'content/console-frames';
|
||||
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';
|
||||
import * as properties from 'shared/settings/properties';
|
||||
import * as properties from '../../shared/settings/properties';
|
||||
|
||||
// eslint-disable-next-line complexity, max-lines-per-function
|
||||
const exec = (operation, settings, addonEnabled) => {
|
||||
const exec = (
|
||||
operation: operations.Operation,
|
||||
settings: any,
|
||||
addonEnabled: boolean,
|
||||
): Promise<actions.Action> | actions.Action => {
|
||||
let smoothscroll = settings.properties.smoothscroll ||
|
||||
properties.defaults.smoothscroll;
|
||||
switch (operation.type) {
|
||||
|
@ -98,7 +103,7 @@ const exec = (operation, settings, addonEnabled) => {
|
|||
operation,
|
||||
});
|
||||
}
|
||||
return { type: '' };
|
||||
return { type: actions.NOOP };
|
||||
};
|
||||
|
||||
export { exec };
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import actions from 'content/actions';
|
||||
import * as keyUtils from 'shared/utils/keys';
|
||||
import operations from 'shared/operations';
|
||||
import messages from 'shared/messages';
|
||||
import * as actions from './index';
|
||||
import * as keyUtils from '../../shared/utils/keys';
|
||||
import * as operations from '../../shared/operations';
|
||||
import * as messages from '../../shared/messages';
|
||||
|
||||
const reservedKeymaps = {
|
||||
'<Esc>': { type: operations.CANCEL },
|
||||
'<C-[>': { type: operations.CANCEL },
|
||||
};
|
||||
|
||||
const set = (value) => {
|
||||
let entries = [];
|
||||
const set = (value: any): actions.SettingAction => {
|
||||
let entries: any[] = [];
|
||||
if (value.keymaps) {
|
||||
let keymaps = { ...value.keymaps, ...reservedKeymaps };
|
||||
entries = Object.entries(keymaps).map((entry) => {
|
||||
|
@ -27,7 +27,7 @@ const set = (value) => {
|
|||
};
|
||||
};
|
||||
|
||||
const load = async() => {
|
||||
const load = async(): Promise<actions.SettingAction> => {
|
||||
let settings = await browser.runtime.sendMessage({
|
||||
type: messages.SETTINGS_QUERY,
|
||||
});
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import messages from 'shared/messages';
|
||||
import MessageListener from '../../MessageListener';
|
||||
import Hint from './hint';
|
||||
import * as dom from 'shared/utils/dom';
|
||||
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',
|
||||
|
@ -8,8 +10,22 @@ const TARGET_SELECTOR = [
|
|||
'[role="button"]', 'summary'
|
||||
].join(',');
|
||||
|
||||
interface Size {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
const inViewport = (win, element, viewSize, framePosition) => {
|
||||
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);
|
||||
|
@ -30,34 +46,44 @@ const inViewport = (win, element, viewSize, framePosition) => {
|
|||
return true;
|
||||
};
|
||||
|
||||
const isAriaHiddenOrAriaDisabled = (win, element) => {
|
||||
const isAriaHiddenOrAriaDisabled = (win: Window, element: Element): boolean => {
|
||||
if (!element || win.document.documentElement === element) {
|
||||
return false;
|
||||
}
|
||||
for (let attr of ['aria-hidden', 'aria-disabled']) {
|
||||
if (element.hasAttribute(attr)) {
|
||||
let hidden = element.getAttribute(attr).toLowerCase();
|
||||
let value = element.getAttribute(attr);
|
||||
if (value !== null) {
|
||||
let hidden = value.toLowerCase();
|
||||
if (hidden === '' || hidden === 'true') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return isAriaHiddenOrAriaDisabled(win, element.parentNode);
|
||||
return isAriaHiddenOrAriaDisabled(win, element.parentElement as Element);
|
||||
};
|
||||
|
||||
export default class Follow {
|
||||
constructor(win, store) {
|
||||
private win: Window;
|
||||
|
||||
private newTab: boolean;
|
||||
|
||||
private background: boolean;
|
||||
|
||||
private hints: {[key: string]: Hint };
|
||||
|
||||
private targets: HTMLElement[] = [];
|
||||
|
||||
constructor(win: Window) {
|
||||
this.win = win;
|
||||
this.store = store;
|
||||
this.newTab = false;
|
||||
this.background = false;
|
||||
this.hints = {};
|
||||
this.targets = [];
|
||||
|
||||
messages.onMessage(this.onMessage.bind(this));
|
||||
new MessageListener().onWebMessage(this.onMessage.bind(this));
|
||||
}
|
||||
|
||||
key(key) {
|
||||
key(key: keyUtils.Key): boolean {
|
||||
if (Object.keys(this.hints).length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
@ -69,7 +95,7 @@ export default class Follow {
|
|||
return true;
|
||||
}
|
||||
|
||||
openLink(element) {
|
||||
openLink(element: HTMLAreaElement|HTMLAnchorElement) {
|
||||
// Browser prevent new tab by link with target='_blank'
|
||||
if (!this.newTab && element.getAttribute('target') !== '_blank') {
|
||||
element.click();
|
||||
|
@ -90,7 +116,7 @@ export default class Follow {
|
|||
});
|
||||
}
|
||||
|
||||
countHints(sender, viewSize, framePosition) {
|
||||
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,
|
||||
|
@ -98,7 +124,7 @@ export default class Follow {
|
|||
}), '*');
|
||||
}
|
||||
|
||||
createHints(keysArray, newTab, background) {
|
||||
createHints(keysArray: string[], newTab: boolean, background: boolean) {
|
||||
if (keysArray.length !== this.targets.length) {
|
||||
throw new Error('illegal hint count');
|
||||
}
|
||||
|
@ -113,7 +139,7 @@ export default class Follow {
|
|||
}
|
||||
}
|
||||
|
||||
showHints(keys) {
|
||||
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))
|
||||
|
@ -128,18 +154,19 @@ export default class Follow {
|
|||
this.targets = [];
|
||||
}
|
||||
|
||||
activateHints(keys) {
|
||||
activateHints(keys: string) {
|
||||
let hint = this.hints[keys];
|
||||
if (!hint) {
|
||||
return;
|
||||
}
|
||||
let element = hint.target;
|
||||
let element = hint.getTarget();
|
||||
switch (element.tagName.toLowerCase()) {
|
||||
case 'a':
|
||||
return this.openLink(element as HTMLAnchorElement);
|
||||
case 'area':
|
||||
return this.openLink(element);
|
||||
return this.openLink(element as HTMLAreaElement);
|
||||
case 'input':
|
||||
switch (element.type) {
|
||||
switch ((element as HTMLInputElement).type) {
|
||||
case 'file':
|
||||
case 'checkbox':
|
||||
case 'radio':
|
||||
|
@ -166,7 +193,7 @@ export default class Follow {
|
|||
}
|
||||
}
|
||||
|
||||
onMessage(message, sender) {
|
||||
onMessage(message: messages.Message, sender: any) {
|
||||
switch (message.type) {
|
||||
case messages.FOLLOW_REQUEST_COUNT_TARGETS:
|
||||
return this.countHints(sender, message.viewSize, message.framePosition);
|
||||
|
@ -178,19 +205,23 @@ export default class Follow {
|
|||
case messages.FOLLOW_ACTIVATE:
|
||||
return this.activateHints(message.keys);
|
||||
case messages.FOLLOW_REMOVE_HINTS:
|
||||
return this.removeHints(message.keys);
|
||||
return this.removeHints();
|
||||
}
|
||||
}
|
||||
|
||||
static getTargetElements(win, viewSize, framePosition) {
|
||||
static getTargetElements(
|
||||
win: Window,
|
||||
viewSize:
|
||||
Size, framePosition: Point,
|
||||
): HTMLElement[] {
|
||||
let all = win.document.querySelectorAll(TARGET_SELECTOR);
|
||||
let filtered = Array.prototype.filter.call(all, (element) => {
|
||||
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.type !== 'hidden' &&
|
||||
(element as HTMLInputElement).type !== 'hidden' &&
|
||||
element.offsetHeight > 0 &&
|
||||
!isAriaHiddenOrAriaDisabled(win, element) &&
|
||||
inViewport(win, element, viewSize, framePosition);
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import * as dom from 'shared/utils/dom';
|
||||
import * as dom from '../../../shared/utils/dom';
|
||||
|
||||
const hintPosition = (element) => {
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
const hintPosition = (element: Element): Point => {
|
||||
let { left, top, right, bottom } = dom.viewportRect(element);
|
||||
|
||||
if (element.tagName !== 'AREA') {
|
||||
|
@ -14,17 +19,21 @@ const hintPosition = (element) => {
|
|||
};
|
||||
|
||||
export default class Hint {
|
||||
constructor(target, tag) {
|
||||
if (!(document.body instanceof HTMLElement)) {
|
||||
throw new TypeError('target is not an HTMLElement');
|
||||
private target: HTMLElement;
|
||||
|
||||
private element: HTMLElement;
|
||||
|
||||
constructor(target: HTMLElement, tag: string) {
|
||||
let doc = target.ownerDocument;
|
||||
if (doc === null) {
|
||||
throw new TypeError('ownerDocument is null');
|
||||
}
|
||||
|
||||
this.target = target;
|
||||
|
||||
let doc = target.ownerDocument;
|
||||
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;
|
||||
|
@ -35,15 +44,19 @@ export default class Hint {
|
|||
doc.body.append(this.element);
|
||||
}
|
||||
|
||||
show() {
|
||||
show(): void {
|
||||
this.element.style.display = 'inline';
|
||||
}
|
||||
|
||||
hide() {
|
||||
hide(): void {
|
||||
this.element.style.display = 'none';
|
||||
}
|
||||
|
||||
remove() {
|
||||
remove(): void {
|
||||
this.element.remove();
|
||||
}
|
||||
|
||||
getTarget(): HTMLElement {
|
||||
return this.target;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,33 +2,37 @@ import InputComponent from './input';
|
|||
import FollowComponent from './follow';
|
||||
import MarkComponent from './mark';
|
||||
import KeymapperComponent from './keymapper';
|
||||
import * as settingActions from 'content/actions/setting';
|
||||
import messages from 'shared/messages';
|
||||
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 blacklists from '../../../shared/blacklists';
|
||||
import * as keys from '../../../shared/utils/keys';
|
||||
|
||||
export default class Common {
|
||||
constructor(win, store) {
|
||||
const input = new InputComponent(win.document.body, store);
|
||||
const follow = new FollowComponent(win, store);
|
||||
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(win.document.body, store);
|
||||
const keymapper = new KeymapperComponent(store);
|
||||
|
||||
input.onKey(key => follow.key(key));
|
||||
input.onKey(key => mark.key(key));
|
||||
input.onKey(key => keymapper.key(key));
|
||||
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.prevEnabled = undefined;
|
||||
this.prevBlacklist = undefined;
|
||||
|
||||
this.reloadSettings();
|
||||
|
||||
messages.onMessage(this.onMessage.bind(this));
|
||||
new MessageListener().onBackgroundMessage(this.onMessage.bind(this));
|
||||
}
|
||||
|
||||
onMessage(message) {
|
||||
onMessage(message: messages.Message) {
|
||||
let { enabled } = this.store.getState().addon;
|
||||
switch (message.type) {
|
||||
case messages.SETTINGS_CHANGED:
|
||||
|
@ -40,12 +44,13 @@ export default class Common {
|
|||
|
||||
reloadSettings() {
|
||||
try {
|
||||
this.store.dispatch(settingActions.load()).then(({ value: settings }) => {
|
||||
let enabled = !blacklists.includes(
|
||||
settings.blacklist, this.win.location.href
|
||||
);
|
||||
this.store.dispatch(addonActions.setEnabled(enabled));
|
||||
});
|
||||
this.store.dispatch(settingActions.load())
|
||||
.then(({ value: settings }: any) => {
|
||||
let enabled = !blacklists.includes(
|
||||
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);
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import * as dom from 'shared/utils/dom';
|
||||
import * as keys from 'shared/utils/keys';
|
||||
import * as dom from '../../../shared/utils/dom';
|
||||
import * as keys from '../../../shared/utils/keys';
|
||||
|
||||
const cancelKey = (e) => {
|
||||
const cancelKey = (e: KeyboardEvent): boolean => {
|
||||
return e.key === 'Escape' || e.key === '[' && e.ctrlKey;
|
||||
};
|
||||
|
||||
export default class InputComponent {
|
||||
constructor(target) {
|
||||
private pressed: {[key: string]: string} = {};
|
||||
|
||||
private onKeyListeners: ((key: keys.Key) => boolean)[] = [];
|
||||
|
||||
constructor(target: HTMLElement) {
|
||||
this.pressed = {};
|
||||
this.onKeyListeners = [];
|
||||
|
||||
|
@ -15,11 +19,11 @@ export default class InputComponent {
|
|||
target.addEventListener('keyup', this.onKeyUp.bind(this));
|
||||
}
|
||||
|
||||
onKey(cb) {
|
||||
onKey(cb: (key: keys.Key) => boolean) {
|
||||
this.onKeyListeners.push(cb);
|
||||
}
|
||||
|
||||
onKeyPress(e) {
|
||||
onKeyPress(e: KeyboardEvent) {
|
||||
if (this.pressed[e.key] && this.pressed[e.key] !== 'keypress') {
|
||||
return;
|
||||
}
|
||||
|
@ -27,7 +31,7 @@ export default class InputComponent {
|
|||
this.capture(e);
|
||||
}
|
||||
|
||||
onKeyDown(e) {
|
||||
onKeyDown(e: KeyboardEvent) {
|
||||
if (this.pressed[e.key] && this.pressed[e.key] !== 'keydown') {
|
||||
return;
|
||||
}
|
||||
|
@ -35,14 +39,19 @@ export default class InputComponent {
|
|||
this.capture(e);
|
||||
}
|
||||
|
||||
onKeyUp(e) {
|
||||
onKeyUp(e: KeyboardEvent) {
|
||||
delete this.pressed[e.key];
|
||||
}
|
||||
|
||||
capture(e) {
|
||||
if (this.fromInput(e)) {
|
||||
if (cancelKey(e) && e.target.blur) {
|
||||
e.target.blur();
|
||||
// eslint-disable-next-line max-statements
|
||||
capture(e: KeyboardEvent) {
|
||||
let target = e.target;
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
if (this.fromInput(target)) {
|
||||
if (cancelKey(e) && target.blur) {
|
||||
target.blur();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -63,13 +72,10 @@ export default class InputComponent {
|
|||
}
|
||||
}
|
||||
|
||||
fromInput(e) {
|
||||
if (!e.target) {
|
||||
return false;
|
||||
}
|
||||
return e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
e.target instanceof HTMLSelectElement ||
|
||||
dom.isContentEditable(e.target);
|
||||
fromInput(e: Element) {
|
||||
return e instanceof HTMLInputElement ||
|
||||
e instanceof HTMLTextAreaElement ||
|
||||
e instanceof HTMLSelectElement ||
|
||||
dom.isContentEditable(e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as inputActions from 'content/actions/input';
|
||||
import * as operationActions from 'content/actions/operation';
|
||||
import operations from 'shared/operations';
|
||||
import * as keyUtils from 'shared/utils/keys';
|
||||
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, keys) => {
|
||||
if (mapping.length < keys.length) {
|
||||
|
|
|
@ -3,7 +3,7 @@ import * as scrolls from 'content/scrolls';
|
|||
import * as consoleFrames from 'content/console-frames';
|
||||
import * as properties from 'shared/settings/properties';
|
||||
|
||||
const cancelKey = (key) => {
|
||||
const cancelKey = (key): boolean => {
|
||||
return key.key === 'Esc' || key.key === '[' && key.ctrlKey;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import * as findActions from 'content/actions/find';
|
||||
import messages from 'shared/messages';
|
||||
import * as findActions from '../../actions/find';
|
||||
import * as messages from '../../../shared/messages';
|
||||
import MessageListener from '../../MessageListener';
|
||||
|
||||
export default class FindComponent {
|
||||
constructor(win, store) {
|
||||
this.win = win;
|
||||
private store: any;
|
||||
|
||||
constructor(store: any) {
|
||||
this.store = store;
|
||||
|
||||
messages.onMessage(this.onMessage.bind(this));
|
||||
new MessageListener().onWebMessage(this.onMessage.bind(this));
|
||||
}
|
||||
|
||||
onMessage(message) {
|
||||
onMessage(message: messages.Message) {
|
||||
switch (message.type) {
|
||||
case messages.CONSOLE_ENTER_FIND:
|
||||
return this.start(message.text);
|
||||
|
@ -20,22 +22,25 @@ export default class FindComponent {
|
|||
}
|
||||
}
|
||||
|
||||
start(text) {
|
||||
start(text: string) {
|
||||
let state = this.store.getState().find;
|
||||
|
||||
if (text.length === 0) {
|
||||
return this.store.dispatch(findActions.next(state.keyword, true));
|
||||
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, false));
|
||||
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, false));
|
||||
return this.store.dispatch(
|
||||
findActions.prev(state.keyword as string, false));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +1,46 @@
|
|||
import * as followControllerActions from 'content/actions/follow-controller';
|
||||
import messages from 'shared/messages';
|
||||
import HintKeyProducer from 'content/hint-key-producer';
|
||||
import * as properties from 'shared/settings/properties';
|
||||
import * as followControllerActions from '../../actions/follow-controller';
|
||||
import * as messages from '../../../shared/messages';
|
||||
import MessageListener, { WebMessageSender } from '../../MessageListener';
|
||||
import HintKeyProducer from '../../hint-key-producer';
|
||||
import * as properties from '../../../shared/settings/properties';
|
||||
|
||||
const broadcastMessage = (win, message) => {
|
||||
const broadcastMessage = (win: Window, message: messages.Message): void => {
|
||||
let json = JSON.stringify(message);
|
||||
let frames = [window.self].concat(Array.from(window.frames));
|
||||
let frames = [win.self].concat(Array.from(win.frames as any));
|
||||
frames.forEach(frame => frame.postMessage(json, '*'));
|
||||
};
|
||||
|
||||
export default class FollowController {
|
||||
constructor(win, store) {
|
||||
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;
|
||||
|
||||
messages.onMessage(this.onMessage.bind(this));
|
||||
new MessageListener().onWebMessage(this.onMessage.bind(this));
|
||||
|
||||
store.subscribe(() => {
|
||||
this.update();
|
||||
});
|
||||
}
|
||||
|
||||
onMessage(message, sender) {
|
||||
onMessage(message: messages.Message, sender: WebMessageSender) {
|
||||
switch (message.type) {
|
||||
case messages.FOLLOW_START:
|
||||
return this.store.dispatch(
|
||||
|
@ -36,7 +52,7 @@ export default class FollowController {
|
|||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
update(): void {
|
||||
let prevState = this.state;
|
||||
this.state = this.store.getState().followController;
|
||||
|
||||
|
@ -49,8 +65,10 @@ export default class FollowController {
|
|||
}
|
||||
}
|
||||
|
||||
updateHints() {
|
||||
let shown = this.keys.filter(key => key.startsWith(this.state.keys));
|
||||
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());
|
||||
|
@ -58,18 +76,18 @@ export default class FollowController {
|
|||
|
||||
broadcastMessage(this.win, {
|
||||
type: messages.FOLLOW_SHOW_HINTS,
|
||||
keys: this.state.keys,
|
||||
keys: this.state.keys as string,
|
||||
});
|
||||
}
|
||||
|
||||
activate() {
|
||||
activate(): void {
|
||||
broadcastMessage(this.win, {
|
||||
type: messages.FOLLOW_ACTIVATE,
|
||||
keys: this.state.keys,
|
||||
keys: this.state.keys as string,
|
||||
});
|
||||
}
|
||||
|
||||
keyPress(key, ctrlKey) {
|
||||
keyPress(key: string, ctrlKey: boolean): boolean {
|
||||
if (key === '[' && ctrlKey) {
|
||||
this.store.dispatch(followControllerActions.disable());
|
||||
return true;
|
||||
|
@ -107,25 +125,28 @@ export default class FollowController {
|
|||
viewSize: { width: viewWidth, height: viewHeight },
|
||||
framePosition: { x: 0, y: 0 },
|
||||
}), '*');
|
||||
frameElements.forEach((element) => {
|
||||
let { left: frameX, top: frameY } = element.getBoundingClientRect();
|
||||
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 },
|
||||
});
|
||||
element.contentWindow.postMessage(message, '*');
|
||||
if (ele instanceof HTMLFrameElement && ele.contentWindow ||
|
||||
ele instanceof HTMLIFrameElement && ele.contentWindow) {
|
||||
ele.contentWindow.postMessage(message, '*');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
create(count, sender) {
|
||||
create(count: number, sender: WebMessageSender) {
|
||||
let produced = [];
|
||||
for (let i = 0; i < count; ++i) {
|
||||
produced.push(this.producer.produce());
|
||||
produced.push((this.producer as HintKeyProducer).produce());
|
||||
}
|
||||
this.keys = this.keys.concat(produced);
|
||||
|
||||
sender.postMessage(JSON.stringify({
|
||||
(sender as Window).postMessage(JSON.stringify({
|
||||
type: messages.FOLLOW_CREATE_HINTS,
|
||||
keysArray: produced,
|
||||
newTab: this.state.newTab,
|
||||
|
|
|
@ -2,33 +2,43 @@ import CommonComponent from '../common';
|
|||
import FollowController from './follow-controller';
|
||||
import FindComponent from './find';
|
||||
import * as consoleFrames from '../../console-frames';
|
||||
import messages from 'shared/messages';
|
||||
import * as scrolls from 'content/scrolls';
|
||||
import * as messages from '../../../shared/messages';
|
||||
import MessageListener from '../../MessageListener';
|
||||
import * as scrolls from '../../scrolls';
|
||||
|
||||
export default class TopContent {
|
||||
private win: Window;
|
||||
|
||||
constructor(win, store) {
|
||||
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(win, store); // eslint-disable-line no-new
|
||||
new FindComponent(store); // eslint-disable-line no-new
|
||||
|
||||
// TODO make component
|
||||
consoleFrames.initialize(this.win.document);
|
||||
|
||||
messages.onMessage(this.onMessage.bind(this));
|
||||
new MessageListener().onWebMessage(this.onWebMessage.bind(this));
|
||||
new MessageListener().onBackgroundMessage(
|
||||
this.onBackgroundMessage.bind(this));
|
||||
}
|
||||
|
||||
onMessage(message) {
|
||||
let addonState = this.store.getState().addon;
|
||||
|
||||
onWebMessage(message: messages.Message) {
|
||||
switch (message.type) {
|
||||
case messages.CONSOLE_UNFOCUS:
|
||||
this.win.focus();
|
||||
consoleFrames.blur(window.document);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import messages from 'shared/messages';
|
||||
import * as messages from '../shared/messages';
|
||||
|
||||
const initialize = (doc) => {
|
||||
const initialize = (doc: Document): HTMLIFrameElement => {
|
||||
let iframe = doc.createElement('iframe');
|
||||
iframe.src = browser.runtime.getURL('build/console.html');
|
||||
iframe.id = 'vimvixen-console-frame';
|
||||
|
@ -10,13 +10,13 @@ const initialize = (doc) => {
|
|||
return iframe;
|
||||
};
|
||||
|
||||
const blur = (doc) => {
|
||||
let iframe = doc.getElementById('vimvixen-console-frame');
|
||||
iframe.blur();
|
||||
const blur = (doc: Document) => {
|
||||
let ele = doc.getElementById('vimvixen-console-frame') as HTMLIFrameElement;
|
||||
ele.blur();
|
||||
};
|
||||
|
||||
const postError = (text) => {
|
||||
browser.runtime.sendMessage({
|
||||
const postError = (text: string): Promise<any> => {
|
||||
return browser.runtime.sendMessage({
|
||||
type: messages.CONSOLE_FRAME_MESSAGE,
|
||||
message: {
|
||||
type: messages.CONSOLE_SHOW_ERROR,
|
||||
|
@ -25,8 +25,8 @@ const postError = (text) => {
|
|||
});
|
||||
};
|
||||
|
||||
const postInfo = (text) => {
|
||||
browser.runtime.sendMessage({
|
||||
const postInfo = (text: string): Promise<any> => {
|
||||
return browser.runtime.sendMessage({
|
||||
type: messages.CONSOLE_FRAME_MESSAGE,
|
||||
message: {
|
||||
type: messages.CONSOLE_SHOW_INFO,
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import * as doms from 'shared/utils/dom';
|
||||
import * as doms from '../shared/utils/dom';
|
||||
|
||||
const focusInput = () => {
|
||||
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) {
|
||||
if (target instanceof HTMLInputElement) {
|
||||
target.focus();
|
||||
} else if (target instanceof HTMLTextAreaElement) {
|
||||
target.focus();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
export default class HintKeyProducer {
|
||||
constructor(charset) {
|
||||
private charset: string;
|
||||
|
||||
private counter: number[];
|
||||
|
||||
constructor(charset: string) {
|
||||
if (charset.length === 0) {
|
||||
throw new TypeError('charset is empty');
|
||||
}
|
||||
|
@ -8,13 +12,13 @@ export default class HintKeyProducer {
|
|||
this.counter = [];
|
||||
}
|
||||
|
||||
produce() {
|
||||
produce(): string {
|
||||
this.increment();
|
||||
|
||||
return this.counter.map(x => this.charset[x]).join('');
|
||||
}
|
||||
|
||||
increment() {
|
||||
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);
|
||||
|
|
|
@ -1,14 +1,9 @@
|
|||
import { createStore, applyMiddleware } from 'redux';
|
||||
import promise from 'redux-promise';
|
||||
import reducers from 'content/reducers';
|
||||
import TopContentComponent from './components/top-content';
|
||||
import FrameContentComponent from './components/frame-content';
|
||||
import consoleFrameStyle from './site-style';
|
||||
import { newStore } from './store';
|
||||
|
||||
const store = createStore(
|
||||
reducers,
|
||||
applyMiddleware(promise),
|
||||
);
|
||||
const store = newStore();
|
||||
|
||||
if (window.self === window.top) {
|
||||
new TopContentComponent(window, store); // eslint-disable-line no-new
|
||||
|
|
|
@ -1,58 +1,63 @@
|
|||
const REL_PATTERN = {
|
||||
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.
|
||||
const selectLast = (win, selector, filter) => {
|
||||
let nodes = win.document.querySelectorAll(selector);
|
||||
// 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 = Array.from(nodes).filter(filter);
|
||||
nodes = nodes.filter(filter);
|
||||
}
|
||||
|
||||
return nodes.length ? nodes[nodes.length - 1] : null;
|
||||
};
|
||||
}
|
||||
|
||||
const historyPrev = (win) => {
|
||||
const historyPrev = (win: Window): void => {
|
||||
win.history.back();
|
||||
};
|
||||
|
||||
const historyNext = (win) => {
|
||||
const historyNext = (win: Window): void => {
|
||||
win.history.forward();
|
||||
};
|
||||
|
||||
// Code common to linkPrev and linkNext which navigates to the specified page.
|
||||
const linkRel = (win, rel) => {
|
||||
let link = selectLast(win, `link[rel~=${rel}][href]`);
|
||||
|
||||
const linkRel = (win: Window, rel: string): void => {
|
||||
let link = selectLast<HTMLLinkElement>(win, `link[rel~=${rel}][href]`);
|
||||
if (link) {
|
||||
win.location = link.href;
|
||||
win.location.href = link.href;
|
||||
return;
|
||||
}
|
||||
|
||||
const pattern = REL_PATTERN[rel];
|
||||
|
||||
link = selectLast(win, `a[rel~=${rel}][href]`) ||
|
||||
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 (link) {
|
||||
link.click();
|
||||
if (a) {
|
||||
a.click();
|
||||
}
|
||||
};
|
||||
|
||||
const linkPrev = (win) => {
|
||||
const linkPrev = (win: Window): void => {
|
||||
linkRel(win, 'prev');
|
||||
};
|
||||
|
||||
const linkNext = (win) => {
|
||||
const linkNext = (win: Window): void => {
|
||||
linkRel(win, 'next');
|
||||
};
|
||||
|
||||
const parent = (win) => {
|
||||
const parent = (win: Window): void => {
|
||||
const loc = win.location;
|
||||
if (loc.hash !== '') {
|
||||
loc.hash = '';
|
||||
|
@ -71,8 +76,8 @@ const parent = (win) => {
|
|||
}
|
||||
};
|
||||
|
||||
const root = (win) => {
|
||||
win.location = win.location.origin;
|
||||
const root = (win: Window): void => {
|
||||
win.location.href = win.location.origin;
|
||||
};
|
||||
|
||||
export { historyPrev, historyNext, linkPrev, linkNext, parent, root };
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
import actions from 'content/actions';
|
||||
import * as actions from '../actions';
|
||||
|
||||
const defaultState = {
|
||||
export interface State {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
const defaultState: State = {
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
export default function reducer(state = defaultState, action = {}) {
|
||||
export default function reducer(
|
||||
state: State = defaultState,
|
||||
action: actions.AddonAction,
|
||||
): State {
|
||||
switch (action.type) {
|
||||
case actions.ADDON_SET_ENABLED:
|
||||
return { ...state,
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
import actions from 'content/actions';
|
||||
import * as actions from '../actions';
|
||||
|
||||
const defaultState = {
|
||||
export interface State {
|
||||
keyword: string | null;
|
||||
found: boolean;
|
||||
}
|
||||
|
||||
const defaultState: State = {
|
||||
keyword: null,
|
||||
found: false,
|
||||
};
|
||||
|
||||
export default function reducer(state = defaultState, action = {}) {
|
||||
export default function reducer(
|
||||
state: State = defaultState,
|
||||
action: actions.FindAction,
|
||||
): State {
|
||||
switch (action.type) {
|
||||
case actions.FIND_SET_KEYWORD:
|
||||
return { ...state,
|
||||
|
|
|
@ -1,13 +1,23 @@
|
|||
import actions from 'content/actions';
|
||||
import * as actions from '../actions';
|
||||
|
||||
const defaultState = {
|
||||
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 = defaultState, action = {}) {
|
||||
export default function reducer(
|
||||
state: State = defaultState,
|
||||
action: actions.FollowAction,
|
||||
): State {
|
||||
switch (action.type) {
|
||||
case actions.FOLLOW_CONTROLLER_ENABLE:
|
||||
return { ...state,
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
import { combineReducers } from 'redux';
|
||||
import addon from './addon';
|
||||
import find from './find';
|
||||
import setting from './setting';
|
||||
import input from './input';
|
||||
import followController from './follow-controller';
|
||||
import mark from './mark';
|
||||
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,10 +1,17 @@
|
|||
import actions from 'content/actions';
|
||||
import * as actions from '../actions';
|
||||
|
||||
const defaultState = {
|
||||
export interface State {
|
||||
keys: string[];
|
||||
}
|
||||
|
||||
const defaultState: State = {
|
||||
keys: []
|
||||
};
|
||||
|
||||
export default function reducer(state = defaultState, action = {}) {
|
||||
export default function reducer(
|
||||
state: State = defaultState,
|
||||
action: actions.InputAction,
|
||||
): State {
|
||||
switch (action.type) {
|
||||
case actions.INPUT_KEY_PRESS:
|
||||
return { ...state,
|
||||
|
|
|
@ -1,12 +1,26 @@
|
|||
import actions from 'content/actions';
|
||||
import * as actions from '../actions';
|
||||
|
||||
const defaultState = {
|
||||
interface Mark {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
setMode: boolean;
|
||||
jumpMode: boolean;
|
||||
marks: { [key: string]: Mark };
|
||||
}
|
||||
|
||||
const defaultState: State = {
|
||||
setMode: false,
|
||||
jumpMode: false,
|
||||
marks: {},
|
||||
};
|
||||
|
||||
export default function reducer(state = defaultState, action = {}) {
|
||||
export default function reducer(
|
||||
state: State = defaultState,
|
||||
action: actions.MarkAction,
|
||||
): State {
|
||||
switch (action.type) {
|
||||
case actions.MARK_START_SET:
|
||||
return { ...state, setMode: true };
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
import actions from 'content/actions';
|
||||
import * as actions from '../actions';
|
||||
|
||||
export interface State {
|
||||
keymaps: any[];
|
||||
}
|
||||
|
||||
const defaultState = {
|
||||
// keymaps is and arrays of key-binding pairs, which is entries of Map
|
||||
keymaps: [],
|
||||
};
|
||||
|
||||
export default function reducer(state = defaultState, action = {}) {
|
||||
export default function reducer(
|
||||
state: State = defaultState,
|
||||
action: actions.SettingAction,
|
||||
): State {
|
||||
switch (action.type) {
|
||||
case actions.SETTING_SET:
|
||||
return { ...action.value };
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import * as doms from 'shared/utils/dom';
|
||||
import * as doms from '../shared/utils/dom';
|
||||
|
||||
const SCROLL_DELTA_X = 64;
|
||||
const SCROLL_DELTA_Y = 64;
|
||||
|
||||
// dirty way to store scrolling state on globally
|
||||
let scrolling = false;
|
||||
let lastTimeoutId = null;
|
||||
let lastTimeoutId: number | null = null;
|
||||
|
||||
const isScrollableStyle = (element) => {
|
||||
const isScrollableStyle = (element: Element): boolean => {
|
||||
let { overflowX, overflowY } = window.getComputedStyle(element);
|
||||
return !(overflowX !== 'scroll' && overflowX !== 'auto' &&
|
||||
overflowY !== 'scroll' && overflowY !== 'auto');
|
||||
};
|
||||
|
||||
const isOverflowed = (element) => {
|
||||
const isOverflowed = (element: Element): boolean => {
|
||||
return element.scrollWidth > element.clientWidth ||
|
||||
element.scrollHeight > element.clientHeight;
|
||||
};
|
||||
|
@ -22,7 +22,7 @@ const isOverflowed = (element) => {
|
|||
// this method is called by each scrolling, and the returned value of this
|
||||
// method is not cached. That does not cause performance issue because in the
|
||||
// most pages, the window is root element i,e, documentElement.
|
||||
const findScrollable = (element) => {
|
||||
const findScrollable = (element: Element): Element | null => {
|
||||
if (isScrollableStyle(element) && isOverflowed(element)) {
|
||||
return element;
|
||||
}
|
||||
|
@ -56,12 +56,16 @@ const resetScrolling = () => {
|
|||
};
|
||||
|
||||
class Scroller {
|
||||
constructor(element, smooth) {
|
||||
private element: Element;
|
||||
|
||||
private smooth: boolean;
|
||||
|
||||
constructor(element: Element, smooth: boolean) {
|
||||
this.element = element;
|
||||
this.smooth = smooth;
|
||||
}
|
||||
|
||||
scrollTo(x, y) {
|
||||
scrollTo(x: number, y: number): void {
|
||||
if (!this.smooth) {
|
||||
this.element.scrollTo(x, y);
|
||||
return;
|
||||
|
@ -74,13 +78,13 @@ class Scroller {
|
|||
this.prepareReset();
|
||||
}
|
||||
|
||||
scrollBy(x, y) {
|
||||
scrollBy(x: number, y: number): void {
|
||||
let left = this.element.scrollLeft + x;
|
||||
let top = this.element.scrollTop + y;
|
||||
this.scrollTo(left, top);
|
||||
}
|
||||
|
||||
prepareReset() {
|
||||
prepareReset(): void {
|
||||
scrolling = true;
|
||||
if (lastTimeoutId) {
|
||||
clearTimeout(lastTimeoutId);
|
||||
|
@ -95,7 +99,7 @@ const getScroll = () => {
|
|||
return { x: target.scrollLeft, y: target.scrollTop };
|
||||
};
|
||||
|
||||
const scrollVertically = (count, smooth) => {
|
||||
const scrollVertically = (count: number, smooth: boolean): void => {
|
||||
let target = scrollTarget();
|
||||
let delta = SCROLL_DELTA_Y * count;
|
||||
if (scrolling) {
|
||||
|
@ -104,7 +108,7 @@ const scrollVertically = (count, smooth) => {
|
|||
new Scroller(target, smooth).scrollBy(0, delta);
|
||||
};
|
||||
|
||||
const scrollHorizonally = (count, smooth) => {
|
||||
const scrollHorizonally = (count: number, smooth: boolean): void => {
|
||||
let target = scrollTarget();
|
||||
let delta = SCROLL_DELTA_X * count;
|
||||
if (scrolling) {
|
||||
|
@ -113,7 +117,7 @@ const scrollHorizonally = (count, smooth) => {
|
|||
new Scroller(target, smooth).scrollBy(delta, 0);
|
||||
};
|
||||
|
||||
const scrollPages = (count, smooth) => {
|
||||
const scrollPages = (count: number, smooth: boolean): void => {
|
||||
let target = scrollTarget();
|
||||
let height = target.clientHeight;
|
||||
let delta = height * count;
|
||||
|
@ -123,33 +127,33 @@ const scrollPages = (count, smooth) => {
|
|||
new Scroller(target, smooth).scrollBy(0, delta);
|
||||
};
|
||||
|
||||
const scrollTo = (x, y, smooth) => {
|
||||
const scrollTo = (x: number, y: number, smooth: boolean): void => {
|
||||
let target = scrollTarget();
|
||||
new Scroller(target, smooth).scrollTo(x, y);
|
||||
};
|
||||
|
||||
const scrollToTop = (smooth) => {
|
||||
const scrollToTop = (smooth: boolean): void => {
|
||||
let target = scrollTarget();
|
||||
let x = target.scrollLeft;
|
||||
let y = 0;
|
||||
new Scroller(target, smooth).scrollTo(x, y);
|
||||
};
|
||||
|
||||
const scrollToBottom = (smooth) => {
|
||||
const 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) => {
|
||||
const scrollToHome = (smooth: boolean): void => {
|
||||
let target = scrollTarget();
|
||||
let x = 0;
|
||||
let y = target.scrollTop;
|
||||
new Scroller(target, smooth).scrollTo(x, y);
|
||||
};
|
||||
|
||||
const scrollToEnd = (smooth) => {
|
||||
const scrollToEnd = (smooth: boolean): void => {
|
||||
let target = scrollTarget();
|
||||
let x = target.scrollWidth;
|
||||
let y = target.scrollTop;
|
||||
|
|
8
src/content/store/index.ts
Normal file
8
src/content/store/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import promise from 'redux-promise';
|
||||
import reducers from '../reducers';
|
||||
import { createStore, applyMiddleware } from 'redux';
|
||||
|
||||
export const newStore = () => createStore(
|
||||
reducers,
|
||||
applyMiddleware(promise),
|
||||
);
|
|
@ -1,7 +1,7 @@
|
|||
import messages from 'shared/messages';
|
||||
import * as messages from '../shared/messages';
|
||||
import * as urls from '../shared/urls';
|
||||
|
||||
const yank = (win) => {
|
||||
const yank = (win: Window) => {
|
||||
let input = win.document.createElement('input');
|
||||
win.document.body.append(input);
|
||||
|
||||
|
@ -15,7 +15,7 @@ const yank = (win) => {
|
|||
input.remove();
|
||||
};
|
||||
|
||||
const paste = (win, newTab, searchSettings) => {
|
||||
const paste = (win: Window, newTab: boolean, searchSettings: any) => {
|
||||
let textarea = win.document.createElement('textarea');
|
||||
win.document.body.append(textarea);
|
||||
|
||||
|
@ -25,7 +25,7 @@ const paste = (win, newTab, searchSettings) => {
|
|||
textarea.focus();
|
||||
|
||||
if (win.document.execCommand('paste')) {
|
||||
let value = textarea.textContent;
|
||||
let value = textarea.textContent as string;
|
||||
let url = urls.searchUrl(value, searchSettings);
|
||||
browser.runtime.sendMessage({
|
||||
type: messages.OPEN_URL,
|
||||
|
|
Reference in a new issue