Types src/content

This commit is contained in:
Shin'ya Ueoka 2019-05-02 14:08:51 +09:00
parent 992b3ac65d
commit d01db82c0d
62 changed files with 1411 additions and 468 deletions

View 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);
},
);
}
}

View file

@ -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,

View file

@ -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);
};

View file

@ -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,
};

View file

@ -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;

View file

@ -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
};

View file

@ -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 {

View file

@ -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 };

View file

@ -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,
});

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -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;
};

View file

@ -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));
}
}

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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();
}
};

View file

@ -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);

View file

@ -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

View file

@ -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 };

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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 };

View file

@ -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 };

View file

@ -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;

View 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),
);

View file

@ -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,