From c60d0e7392fc708e961614d6b756a045de74f458 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Tue, 30 Apr 2019 14:00:07 +0900 Subject: Rename .js/.jsx to .ts/.tsx --- src/console/index.tsx | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/console/index.tsx (limited to 'src/console/index.tsx') diff --git a/src/console/index.tsx b/src/console/index.tsx new file mode 100644 index 0000000..3190a9a --- /dev/null +++ b/src/console/index.tsx @@ -0,0 +1,42 @@ +import messages from 'shared/messages'; +import reducers from 'console/reducers'; +import { createStore, applyMiddleware } from 'redux'; +import promise from 'redux-promise'; +import * as consoleActions from 'console/actions/console'; +import { Provider } from 'react-redux'; +import Console from './components/Console'; +import React from 'react'; +import ReactDOM from 'react-dom'; + +const store = createStore( + reducers, + applyMiddleware(promise), +); + +window.addEventListener('load', () => { + let wrapper = document.getElementById('vimvixen-console'); + ReactDOM.render( + + + , + wrapper); +}); + +const onMessage = (message) => { + switch (message.type) { + case messages.CONSOLE_SHOW_COMMAND: + return store.dispatch(consoleActions.showCommand(message.command)); + case messages.CONSOLE_SHOW_FIND: + return store.dispatch(consoleActions.showFind()); + case messages.CONSOLE_SHOW_ERROR: + return store.dispatch(consoleActions.showError(message.text)); + case messages.CONSOLE_SHOW_INFO: + return store.dispatch(consoleActions.showInfo(message.text)); + case messages.CONSOLE_HIDE: + return store.dispatch(consoleActions.hide()); + } +}; + +browser.runtime.onMessage.addListener(onMessage); +let port = browser.runtime.connect({ name: 'vimvixen-console' }); +port.onMessage.addListener(onMessage); -- cgit v1.2.3 From 0452370df43cc4263f268e7064f824d7e6e489b3 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Wed, 1 May 2019 23:10:37 +0900 Subject: Types on src/console --- src/background/usecases/CompletionsUseCase.ts | 12 ++-- src/console/actions/console.ts | 34 +++++----- src/console/actions/index.ts | 76 ++++++++++++++++++---- src/console/components/Console.tsx | 43 +++++++----- src/console/components/console/Completion.tsx | 45 ++++++++----- src/console/components/console/CompletionItem.tsx | 9 ++- src/console/components/console/CompletionTitle.tsx | 11 ++-- src/console/components/console/Input.tsx | 31 +++++---- src/console/components/console/Message.tsx | 13 ++-- src/console/index.tsx | 10 +-- src/console/reducers/index.ts | 23 +++++-- test/console/actions/console.test.ts | 2 +- test/console/reducers/console.test.ts | 2 +- 13 files changed, 206 insertions(+), 105 deletions(-) (limited to 'src/console/index.tsx') diff --git a/src/background/usecases/CompletionsUseCase.ts b/src/background/usecases/CompletionsUseCase.ts index 037d6eb..f3a808b 100644 --- a/src/background/usecases/CompletionsUseCase.ts +++ b/src/background/usecases/CompletionsUseCase.ts @@ -1,4 +1,3 @@ -import Completions from '../domains/Completions'; import CompletionGroup from '../domains/CompletionGroup'; import CommandDocs from '../domains/CommandDocs'; import CompletionsRepository from '../repositories/CompletionsRepository'; @@ -25,7 +24,7 @@ export default class CompletionsUseCase { this.settingRepository = new SettingRepository(); } - queryConsoleCommand(prefix: string): Promise { + queryConsoleCommand(prefix: string): Promise { let keys = Object.keys(CommandDocs); let items = keys .filter(name => name.startsWith(prefix)) @@ -38,10 +37,10 @@ export default class CompletionsUseCase { if (items.length === 0) { return Promise.resolve([]); } - return Promise.resolve([{ name: 'Console CompletionGroup', items }]); + return Promise.resolve([{ name: 'Console Command', items }]); } - async queryOpen(name: string, keywords: string): Promise { + async queryOpen(name: string, keywords: string): Promise { let settings = await this.settingRepository.get(); let groups: CompletionGroup[] = []; @@ -71,7 +70,10 @@ export default class CompletionsUseCase { } // eslint-disable-next-line max-statements - async queryBuffer(name: string, keywords: string): Promise { + async queryBuffer( + name: string, + keywords: string, + ): Promise { let lastId = await this.tabPresenter.getLastSelectedId(); let trimmed = keywords.trim(); let tabs: Tab[] = []; diff --git a/src/console/actions/console.ts b/src/console/actions/console.ts index 3713a76..ceb419c 100644 --- a/src/console/actions/console.ts +++ b/src/console/actions/console.ts @@ -1,40 +1,40 @@ -import messages from 'shared/messages'; -import actions from 'console/actions'; +import messages from '../../shared/messages'; +import * as actions from './index'; -const hide = () => { +const hide = (): actions.ConsoleAction => { return { type: actions.CONSOLE_HIDE, }; }; -const showCommand = (text) => { +const showCommand = (text: string): actions.ConsoleAction => { return { type: actions.CONSOLE_SHOW_COMMAND, text: text }; }; -const showFind = () => { +const showFind = (): actions.ConsoleAction => { return { type: actions.CONSOLE_SHOW_FIND, }; }; -const showError = (text) => { +const showError = (text: string): actions.ConsoleAction => { return { type: actions.CONSOLE_SHOW_ERROR, text: text }; }; -const showInfo = (text) => { +const showInfo = (text: string): actions.ConsoleAction => { return { type: actions.CONSOLE_SHOW_INFO, text: text }; }; -const hideCommand = () => { +const hideCommand = (): actions.ConsoleAction => { window.top.postMessage(JSON.stringify({ type: messages.CONSOLE_UNFOCUS, }), '*'); @@ -43,15 +43,17 @@ const hideCommand = () => { }; }; -const enterCommand = async(text) => { +const enterCommand = async( + text: string, +): Promise => { await browser.runtime.sendMessage({ type: messages.CONSOLE_ENTER_COMMAND, text, }); - return hideCommand(text); + return hideCommand(); }; -const enterFind = (text) => { +const enterFind = (text: string): actions.ConsoleAction => { window.top.postMessage(JSON.stringify({ type: messages.CONSOLE_ENTER_FIND, text, @@ -59,14 +61,14 @@ const enterFind = (text) => { return hideCommand(); }; -const setConsoleText = (consoleText) => { +const setConsoleText = (consoleText: string): actions.ConsoleAction => { return { type: actions.CONSOLE_SET_CONSOLE_TEXT, consoleText, }; }; -const getCompletions = async(text) => { +const getCompletions = async(text: string): Promise => { let completions = await browser.runtime.sendMessage({ type: messages.CONSOLE_QUERY_COMPLETIONS, text, @@ -78,13 +80,13 @@ const getCompletions = async(text) => { }; }; -const completionNext = () => { +const completionNext = (): actions.ConsoleAction => { return { type: actions.CONSOLE_COMPLETION_NEXT, }; }; -const completionPrev = () => { +const completionPrev = (): actions.ConsoleAction => { return { type: actions.CONSOLE_COMPLETION_PREV, }; @@ -92,5 +94,5 @@ const completionPrev = () => { export { hide, showCommand, showFind, showError, showInfo, hideCommand, setConsoleText, - enterCommand, enterFind, getCompletions, completionNext, completionPrev + enterCommand, enterFind, getCompletions, completionNext, completionPrev, }; diff --git a/src/console/actions/index.ts b/src/console/actions/index.ts index b394179..3770496 100644 --- a/src/console/actions/index.ts +++ b/src/console/actions/index.ts @@ -1,13 +1,63 @@ -export default { - // console commands - CONSOLE_HIDE: 'console.hide', - CONSOLE_SHOW_COMMAND: 'console.show.command', - CONSOLE_SHOW_ERROR: 'console.show.error', - CONSOLE_SHOW_INFO: 'console.show.info', - CONSOLE_HIDE_COMMAND: 'console.hide.command', - CONSOLE_SET_CONSOLE_TEXT: 'console.set.command', - CONSOLE_SET_COMPLETIONS: 'console.set.completions', - CONSOLE_COMPLETION_NEXT: 'console.completion.next', - CONSOLE_COMPLETION_PREV: 'console.completion.prev', - CONSOLE_SHOW_FIND: 'console.show.find', -}; +// console commands +export const CONSOLE_HIDE = 'console.hide'; +export const CONSOLE_SHOW_COMMAND = 'console.show.command'; +export const CONSOLE_SHOW_ERROR = 'console.show.error'; +export const CONSOLE_SHOW_INFO = 'console.show.info'; +export const CONSOLE_HIDE_COMMAND = 'console.hide.command'; +export const CONSOLE_SET_CONSOLE_TEXT = 'console.set.command'; +export const CONSOLE_SET_COMPLETIONS = 'console.set.completions'; +export const CONSOLE_COMPLETION_NEXT = 'console.completion.next'; +export const CONSOLE_COMPLETION_PREV = 'console.completion.prev'; +export const CONSOLE_SHOW_FIND = 'console.show.find'; + +interface HideAction { + type: typeof CONSOLE_HIDE; +} + +interface ShowCommand { + type: typeof CONSOLE_SHOW_COMMAND; + text: string; +} + +interface ShowFindAction { + type: typeof CONSOLE_SHOW_FIND; +} + +interface ShowErrorAction { + type: typeof CONSOLE_SHOW_ERROR; + text: string; +} + +interface ShowInfoAction { + type: typeof CONSOLE_SHOW_INFO; + text: string; +} + +interface HideCommandAction { + type: typeof CONSOLE_HIDE_COMMAND; +} + +interface SetConsoleTextAction { + type: typeof CONSOLE_SET_CONSOLE_TEXT; + consoleText: string; +} + +interface SetCompletionsAction { + type: typeof CONSOLE_SET_COMPLETIONS; + completions: any[]; + completionSource: string; +} + +interface CompletionNextAction { + type: typeof CONSOLE_COMPLETION_NEXT; +} + +interface CompletionPrevAction { + type: typeof CONSOLE_COMPLETION_PREV; +} + +export type ConsoleAction = + HideAction | ShowCommand | ShowFindAction | ShowErrorAction | + ShowInfoAction | HideCommandAction | SetConsoleTextAction | + SetCompletionsAction | CompletionNextAction | CompletionPrevAction; + diff --git a/src/console/components/Console.tsx b/src/console/components/Console.tsx index 5427e43..09c0f50 100644 --- a/src/console/components/Console.tsx +++ b/src/console/components/Console.tsx @@ -1,7 +1,6 @@ import './console.scss'; import { connect } from 'react-redux'; import React from 'react'; -import PropTypes from 'prop-types'; import Input from './console/Input'; import Completion from './console/Completion'; import Message from './console/Message'; @@ -9,14 +8,29 @@ import * as consoleActions from '../../console/actions/console'; const COMPLETION_MAX_ITEMS = 33; -class Console extends React.Component { +interface Props { + mode?: string; + consoleText?: string; + messageText?: string; + children?: string; +} + +class Console extends React.Component { + private input: HTMLInputElement | null; + + constructor(props: Props) { + super(props); + + this.input = null; + } + onBlur() { if (this.props.mode === 'command' || this.props.mode === 'find') { return this.props.dispatch(consoleActions.hideCommand()); } } - doEnter(e) { + doEnter(e: React.KeyboardEvent) { e.stopPropagation(); e.preventDefault(); @@ -28,19 +42,19 @@ class Console extends React.Component { } } - selectNext(e) { + selectNext(e: React.KeyboardEvent) { this.props.dispatch(consoleActions.completionNext()); e.stopPropagation(); e.preventDefault(); } - selectPrev(e) { + selectPrev(e: React.KeyboardEvent) { this.props.dispatch(consoleActions.completionPrev()); e.stopPropagation(); e.preventDefault(); } - onKeyDown(e) { + onKeyDown(e: React.KeyboardEvent) { if (e.keyCode === KeyboardEvent.DOM_VK_ESCAPE && e.ctrlKey) { this.props.dispatch(consoleActions.hideCommand()); } @@ -81,7 +95,7 @@ class Console extends React.Component { } } - onChange(e) { + onChange(e: React.ChangeEvent) { let text = e.target.value; this.props.dispatch(consoleActions.setConsoleText(text)); if (this.props.mode === 'command') { @@ -90,7 +104,7 @@ class Console extends React.Component { } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: Props) { if (!this.input) { return; } @@ -134,16 +148,11 @@ class Console extends React.Component { focus() { window.focus(); - this.input.focus(); + if (this.input) { + this.input.focus(); + } } } -Console.propTypes = { - mode: PropTypes.string, - consoleText: PropTypes.string, - messageText: PropTypes.string, - children: PropTypes.string, -}; - -const mapStateToProps = state => state; +const mapStateToProps = (state: any) => state; export default connect(mapStateToProps)(Console); diff --git a/src/console/components/console/Completion.tsx b/src/console/components/console/Completion.tsx index 5477cb6..169a39c 100644 --- a/src/console/components/console/Completion.tsx +++ b/src/console/components/console/Completion.tsx @@ -1,15 +1,36 @@ import React from 'react'; -import PropTypes from 'prop-types'; import CompletionItem from './CompletionItem'; import CompletionTitle from './CompletionTitle'; -class Completion extends React.Component { - constructor() { - super(); +interface Item { + icon?: string; + caption?: string; + url?: string; +} + +interface Group { + name: string; + items: Item[]; +} + +interface Props { + select: number; + size: number; + completions: Group[]; +} + +interface State { + viewOffset: number; + select: number; +} + +class Completion extends React.Component { + constructor(props: Props) { + super(props); this.state = { viewOffset: 0, select: -1 }; } - static getDerivedStateFromProps(nextProps, prevState) { + static getDerivedStateFromProps(nextProps: Props, prevState: State) { if (prevState.select === nextProps.select) { return null; } @@ -24,6 +45,7 @@ class Completion extends React.Component { } index += g.items.length; } + return -1; })(); let viewOffset = 0; @@ -70,17 +92,4 @@ class Completion extends React.Component { } } -Completion.propTypes = { - select: PropTypes.number, - size: PropTypes.number, - completions: PropTypes.arrayOf(PropTypes.shape({ - name: PropTypes.string, - items: PropTypes.arrayOf(PropTypes.shape({ - icon: PropTypes.string, - caption: PropTypes.string, - url: PropTypes.string, - })), - })), -}; - export default Completion; diff --git a/src/console/components/console/CompletionItem.tsx b/src/console/components/console/CompletionItem.tsx index 3dc552b..1cbf3de 100644 --- a/src/console/components/console/CompletionItem.tsx +++ b/src/console/components/console/CompletionItem.tsx @@ -1,7 +1,14 @@ import React from 'react'; import PropTypes from 'prop-types'; -const CompletionItem = (props) => { +interface Props { + highlight: boolean; + caption?: string; + url?: string; + icon?: string; +} + +const CompletionItem = (props: Props) => { let className = 'vimvixen-console-completion-item'; if (props.highlight) { className += ' vimvixen-completion-selected'; diff --git a/src/console/components/console/CompletionTitle.tsx b/src/console/components/console/CompletionTitle.tsx index 4fcba3f..2543619 100644 --- a/src/console/components/console/CompletionTitle.tsx +++ b/src/console/components/console/CompletionTitle.tsx @@ -1,14 +1,13 @@ import React from 'react'; -import PropTypes from 'prop-types'; -const CompletionTitle = (props) => { +interface Props { + title: string; +} + +const CompletionTitle = (props: Props) => { return
  • {props.title}
  • ; }; -CompletionTitle.propTypes = { - title: PropTypes.string, -}; - export default CompletionTitle; diff --git a/src/console/components/console/Input.tsx b/src/console/components/console/Input.tsx index cbd3348..d0348bd 100644 --- a/src/console/components/console/Input.tsx +++ b/src/console/components/console/Input.tsx @@ -1,9 +1,26 @@ import React from 'react'; -import PropTypes from 'prop-types'; -class Input extends React.Component { +interface Props { + mode: string; + value: string; + onBlur: (e: React.FocusEvent) => void; + onKeyDown: (e: React.KeyboardEvent) => void; + onChange: (e: React.ChangeEvent) => void; +} + +class Input extends React.Component { + private input: HTMLInputElement | null; + + constructor(props: Props) { + super(props); + + this.input = null; + } + focus() { - this.input.focus(); + if (this.input) { + this.input.focus(); + } } render() { @@ -32,12 +49,4 @@ class Input extends React.Component { } } -Input.propTypes = { - mode: PropTypes.string, - value: PropTypes.string, - onBlur: PropTypes.func, - onKeyDown: PropTypes.func, - onChange: PropTypes.func, -}; - export default Input; diff --git a/src/console/components/console/Message.tsx b/src/console/components/console/Message.tsx index dd96248..07a929e 100644 --- a/src/console/components/console/Message.tsx +++ b/src/console/components/console/Message.tsx @@ -1,7 +1,11 @@ import React from 'react'; -import PropTypes from 'prop-types'; -const Message = (props) => { +interface Props { + mode: string; + children: string[]; +} + +const Message = (props: Props) => { switch (props.mode) { case 'error': return ( @@ -16,10 +20,7 @@ const Message = (props) => {

    ); } -}; - -Message.propTypes = { - children: PropTypes.string, + return null; }; export default Message; diff --git a/src/console/index.tsx b/src/console/index.tsx index 3190a9a..ee3a8ee 100644 --- a/src/console/index.tsx +++ b/src/console/index.tsx @@ -1,8 +1,8 @@ -import messages from 'shared/messages'; -import reducers from 'console/reducers'; +import messages from '../shared/messages'; +import reducers from './reducers'; import { createStore, applyMiddleware } from 'redux'; import promise from 'redux-promise'; -import * as consoleActions from 'console/actions/console'; +import * as consoleActions from './actions/console'; import { Provider } from 'react-redux'; import Console from './components/Console'; import React from 'react'; @@ -22,7 +22,7 @@ window.addEventListener('load', () => { wrapper); }); -const onMessage = (message) => { +const onMessage = (message: any): any => { switch (message.type) { case messages.CONSOLE_SHOW_COMMAND: return store.dispatch(consoleActions.showCommand(message.command)); @@ -38,5 +38,5 @@ const onMessage = (message) => { }; browser.runtime.onMessage.addListener(onMessage); -let port = browser.runtime.connect({ name: 'vimvixen-console' }); +let port = browser.runtime.connect(undefined, { name: 'vimvixen-console' }); port.onMessage.addListener(onMessage); diff --git a/src/console/reducers/index.ts b/src/console/reducers/index.ts index 614a72f..37ed715 100644 --- a/src/console/reducers/index.ts +++ b/src/console/reducers/index.ts @@ -1,4 +1,14 @@ -import actions from 'console/actions'; +import * as actions from '../actions'; + +interface State { + mode: string; + messageText: string; + consoleText: string; + completionSource: string; + completions: any[], + select: number; + viewIndex: number; +} const defaultState = { mode: '', @@ -10,7 +20,7 @@ const defaultState = { viewIndex: 0, }; -const nextSelection = (state) => { +const nextSelection = (state: State): number => { if (state.completions.length === 0) { return -1; } @@ -27,7 +37,7 @@ const nextSelection = (state) => { return -1; }; -const prevSelection = (state) => { +const prevSelection = (state: State): number => { let length = state.completions .map(g => g.items.length) .reduce((x, y) => x + y); @@ -37,7 +47,7 @@ const prevSelection = (state) => { return state.select - 1; }; -const nextConsoleText = (completions, select, defaults) => { +const nextConsoleText = (completions: any[], select: number, defaults: any) => { if (select < 0) { return defaults; } @@ -46,7 +56,10 @@ const nextConsoleText = (completions, select, defaults) => { }; // eslint-disable-next-line max-lines-per-function -export default function reducer(state = defaultState, action = {}) { +export default function reducer( + state: State = defaultState, + action: actions.ConsoleAction, +): State { switch (action.type) { case actions.CONSOLE_HIDE: return { ...state, diff --git a/test/console/actions/console.test.ts b/test/console/actions/console.test.ts index 10cd9fe..e45d008 100644 --- a/test/console/actions/console.test.ts +++ b/test/console/actions/console.test.ts @@ -1,4 +1,4 @@ -import actions from 'console/actions'; +import * as actions from 'console/actions'; import * as consoleActions from 'console/actions/console'; describe("console actions", () => { diff --git a/test/console/reducers/console.test.ts b/test/console/reducers/console.test.ts index d5a38cf..47e7daf 100644 --- a/test/console/reducers/console.test.ts +++ b/test/console/reducers/console.test.ts @@ -1,4 +1,4 @@ -import actions from 'console/actions'; +import * as actions from 'console/actions'; import reducer from 'console/reducers'; describe("console reducer", () => { -- cgit v1.2.3 From d01db82c0dca352de2d7644c383d388fc3ec0366 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Thu, 2 May 2019 14:08:51 +0900 Subject: Types src/content --- .eslintrc | 2 + package.json | 1 + src/background/controllers/OperationController.ts | 4 +- src/background/controllers/VersionController.ts | 2 +- src/background/domains/Setting.ts | 20 +- src/background/infrastructures/ConsoleClient.ts | 2 +- .../infrastructures/ContentMessageClient.ts | 2 +- .../infrastructures/ContentMessageListener.ts | 6 +- src/background/presenters/NotifyPresenter.ts | 6 +- src/background/usecases/VersionUseCase.ts | 2 +- src/console/actions/console.ts | 2 +- src/console/index.tsx | 11 +- src/content/MessageListener.ts | 32 ++ src/content/actions/addon.ts | 10 +- src/content/actions/find.ts | 40 +- src/content/actions/follow-controller.ts | 12 +- src/content/actions/index.ts | 151 ++++-- src/content/actions/input.ts | 6 +- src/content/actions/mark.ts | 20 +- src/content/actions/operation.ts | 25 +- src/content/actions/setting.ts | 14 +- src/content/components/common/follow.ts | 79 +++- src/content/components/common/hint.ts | 33 +- src/content/components/common/index.ts | 43 +- src/content/components/common/input.ts | 46 +- src/content/components/common/keymapper.ts | 8 +- src/content/components/common/mark.ts | 2 +- src/content/components/top-content/find.ts | 25 +- .../components/top-content/follow-controller.ts | 65 ++- src/content/components/top-content/index.ts | 28 +- src/content/console-frames.ts | 18 +- src/content/focuses.ts | 8 +- src/content/hint-key-producer.ts | 10 +- src/content/index.ts | 9 +- src/content/navigates.ts | 45 +- src/content/reducers/addon.ts | 13 +- src/content/reducers/find.ts | 14 +- src/content/reducers/follow-controller.ts | 16 +- src/content/reducers/index.ts | 22 +- src/content/reducers/input.ts | 13 +- src/content/reducers/mark.ts | 20 +- src/content/reducers/setting.ts | 11 +- src/content/scrolls.ts | 38 +- src/content/store/index.ts | 8 + src/content/urls.ts | 8 +- src/shared/messages.ts | 346 +++++++++++--- src/shared/operations.ts | 523 ++++++++++++++++++--- src/shared/settings/validator.ts | 2 +- src/shared/utils/keys.ts | 2 +- test/content/actions/follow-controller.test.ts | 2 +- test/content/actions/input.test.ts | 2 +- test/content/actions/mark.test.ts | 2 +- test/content/actions/setting.test.ts | 2 +- test/content/components/common/input.test.ts | 14 +- test/content/reducers/addon.test.ts | 2 +- test/content/reducers/find.test.ts | 2 +- test/content/reducers/follow-controller.test.ts | 2 +- test/content/reducers/input.test.ts | 2 +- test/content/reducers/mark.test.ts | 2 +- test/content/reducers/setting.test.ts | 2 +- test/shared/operations.test.ts | 41 ++ tsconfig.json | 9 +- 62 files changed, 1426 insertions(+), 483 deletions(-) create mode 100644 src/content/MessageListener.ts create mode 100644 src/content/store/index.ts create mode 100644 test/shared/operations.test.ts (limited to 'src/console/index.tsx') diff --git a/.eslintrc b/.eslintrc index fb60bc2..7845ca5 100644 --- a/.eslintrc +++ b/.eslintrc @@ -35,6 +35,7 @@ "indent": ["error", 2], "jsx-quotes": ["error", "prefer-single"], "max-classes-per-file": "off", + "max-lines": "off", "max-params": ["error", 5], "max-statements": ["error", 15], "multiline-comment-style": "off", @@ -47,6 +48,7 @@ "no-console": ["error", { "allow": ["warn", "error"] }], "no-continue": "off", "no-empty-function": "off", + "no-extra-parens": "off", "no-magic-numbers": "off", "no-mixed-operators": "off", "no-plusplus": "off", diff --git a/package.json b/package.json index 5d44a1b..a799554 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "build": "NODE_ENV=production webpack --mode production --progress --display-error-details", "package": "npm run build && script/package", "lint": "eslint --ext .js,.jsx,.ts,.tsx src", + "type-checks": "tsc", "test": "karma start", "test:e2e": "mocha --timeout 8000 e2e" }, diff --git a/src/background/controllers/OperationController.ts b/src/background/controllers/OperationController.ts index 4e9c106..fa09512 100644 --- a/src/background/controllers/OperationController.ts +++ b/src/background/controllers/OperationController.ts @@ -1,4 +1,4 @@ -import operations from '../../shared/operations'; +import * as operations from '../../shared/operations'; import FindUseCase from '../usecases/FindUseCase'; import ConsoleUseCase from '../usecases/ConsoleUseCase'; import TabUseCase from '../usecases/TabUseCase'; @@ -25,7 +25,7 @@ export default class OperationController { } // eslint-disable-next-line complexity, max-lines-per-function - exec(operation: any): Promise { + exec(operation: operations.Operation): Promise { switch (operation.type) { case operations.TAB_CLOSE: return this.tabUseCase.close(false); diff --git a/src/background/controllers/VersionController.ts b/src/background/controllers/VersionController.ts index f402ed0..2e2a197 100644 --- a/src/background/controllers/VersionController.ts +++ b/src/background/controllers/VersionController.ts @@ -7,7 +7,7 @@ export default class VersionController { this.versionUseCase = new VersionUseCase(); } - notify(): void { + notify(): Promise { return this.versionUseCase.notify(); } } diff --git a/src/background/domains/Setting.ts b/src/background/domains/Setting.ts index 106ec0f..b2b1ff2 100644 --- a/src/background/domains/Setting.ts +++ b/src/background/domains/Setting.ts @@ -1,22 +1,30 @@ import DefaultSettings from '../../shared/settings/default'; import * as settingsValues from '../../shared/settings/values'; +type SettingValue = { + source: string, + json: string, + form: any +} + export default class Setting { - constructor({ source, json, form }) { + private obj: SettingValue; + + constructor({ source, json, form }: SettingValue) { this.obj = { source, json, form }; } - get source() { + get source(): string { return this.obj.source; } - get json() { + get json(): string { return this.obj.json; } - get form() { + get form(): any { return this.obj.form; } @@ -33,11 +41,11 @@ export default class Setting { return { ...settingsValues.valueFromJson(DefaultSettings.json), ...value }; } - serialize() { + serialize(): SettingValue { return this.obj; } - static deserialize(obj) { + static deserialize(obj: SettingValue): Setting { return new Setting({ source: obj.source, json: obj.json, form: obj.form }); } diff --git a/src/background/infrastructures/ConsoleClient.ts b/src/background/infrastructures/ConsoleClient.ts index 7ad5d24..c162634 100644 --- a/src/background/infrastructures/ConsoleClient.ts +++ b/src/background/infrastructures/ConsoleClient.ts @@ -1,4 +1,4 @@ -import messages from '../../shared/messages'; +import * as messages from '../../shared/messages'; export default class ConsoleClient { showCommand(tabId: number, command: string): Promise { diff --git a/src/background/infrastructures/ContentMessageClient.ts b/src/background/infrastructures/ContentMessageClient.ts index 20057c7..d4bc476 100644 --- a/src/background/infrastructures/ContentMessageClient.ts +++ b/src/background/infrastructures/ContentMessageClient.ts @@ -1,4 +1,4 @@ -import messages from '../../shared/messages'; +import * as messages from '../../shared/messages'; export default class ContentMessageClient { async broadcastSettingsChanged(): Promise { diff --git a/src/background/infrastructures/ContentMessageListener.ts b/src/background/infrastructures/ContentMessageListener.ts index 81d3232..1cc2696 100644 --- a/src/background/infrastructures/ContentMessageListener.ts +++ b/src/background/infrastructures/ContentMessageListener.ts @@ -1,4 +1,4 @@ -import messages from '../../shared/messages'; +import * as messages from '../../shared/messages'; import CompletionGroup from '../domains/CompletionGroup'; import CommandController from '../controllers/CommandController'; import SettingController from '../controllers/SettingController'; @@ -68,7 +68,9 @@ export default class ContentMessageListener { browser.runtime.onConnect.addListener(this.onConnected.bind(this)); } - onMessage(message: any, senderTab: browser.tabs.Tab): Promise | any { + onMessage( + message: messages.Message, senderTab: browser.tabs.Tab, + ): Promise | any { switch (message.type) { case messages.CONSOLE_QUERY_COMPLETIONS: return this.onConsoleQueryCompletions(message.text); diff --git a/src/background/presenters/NotifyPresenter.ts b/src/background/presenters/NotifyPresenter.ts index c83c205..23932f7 100644 --- a/src/background/presenters/NotifyPresenter.ts +++ b/src/background/presenters/NotifyPresenter.ts @@ -1,11 +1,11 @@ const NOTIFICATION_ID = 'vimvixen-update'; export default class NotifyPresenter { - notify( + async notify( title: string, message: string, onclick: () => void, - ): Promise { + ): Promise { const listener = (id: string) => { if (id !== NOTIFICATION_ID) { return; @@ -17,7 +17,7 @@ export default class NotifyPresenter { }; browser.notifications.onClicked.addListener(listener); - return browser.notifications.create(NOTIFICATION_ID, { + await browser.notifications.create(NOTIFICATION_ID, { 'type': 'basic', 'iconUrl': browser.extension.getURL('resources/icon_48x48.png'), title, diff --git a/src/background/usecases/VersionUseCase.ts b/src/background/usecases/VersionUseCase.ts index 207f9e2..3a3cc2e 100644 --- a/src/background/usecases/VersionUseCase.ts +++ b/src/background/usecases/VersionUseCase.ts @@ -12,7 +12,7 @@ export default class VersionUseCase { this.notifyPresenter = new NotifyPresenter(); } - notify(): Promise { + notify(): Promise { let title = `Vim Vixen ${manifest.version} has been installed`; let message = 'Click here to see release notes'; let url = this.releaseNoteUrl(manifest.version); diff --git a/src/console/actions/console.ts b/src/console/actions/console.ts index ceb419c..b1494b0 100644 --- a/src/console/actions/console.ts +++ b/src/console/actions/console.ts @@ -1,4 +1,4 @@ -import messages from '../../shared/messages'; +import * as messages from '../../shared/messages'; import * as actions from './index'; const hide = (): actions.ConsoleAction => { diff --git a/src/console/index.tsx b/src/console/index.tsx index ee3a8ee..b655154 100644 --- a/src/console/index.tsx +++ b/src/console/index.tsx @@ -1,4 +1,4 @@ -import messages from '../shared/messages'; +import * as messages from '../shared/messages'; import reducers from './reducers'; import { createStore, applyMiddleware } from 'redux'; import promise from 'redux-promise'; @@ -23,15 +23,16 @@ window.addEventListener('load', () => { }); const onMessage = (message: any): any => { - switch (message.type) { + let msg = messages.valueOf(message); + switch (msg.type) { case messages.CONSOLE_SHOW_COMMAND: - return store.dispatch(consoleActions.showCommand(message.command)); + return store.dispatch(consoleActions.showCommand(msg.command)); case messages.CONSOLE_SHOW_FIND: return store.dispatch(consoleActions.showFind()); case messages.CONSOLE_SHOW_ERROR: - return store.dispatch(consoleActions.showError(message.text)); + return store.dispatch(consoleActions.showError(msg.text)); case messages.CONSOLE_SHOW_INFO: - return store.dispatch(consoleActions.showInfo(message.text)); + return store.dispatch(consoleActions.showInfo(msg.text)); case messages.CONSOLE_HIDE: return store.dispatch(consoleActions.hide()); } diff --git a/src/content/MessageListener.ts b/src/content/MessageListener.ts new file mode 100644 index 0000000..105d028 --- /dev/null +++ b/src/content/MessageListener.ts @@ -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); + }, + ); + } +} diff --git a/src/content/actions/addon.ts b/src/content/actions/addon.ts index b30cf16..8dedae0 100644 --- a/src/content/actions/addon.ts +++ b/src/content/actions/addon.ts @@ -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 => setEnabled(true); -const disable = () => setEnabled(false); +const disable = (): Promise => setEnabled(false); -const setEnabled = async(enabled) => { +const setEnabled = async(enabled: boolean): Promise => { await browser.runtime.sendMessage({ type: messages.ADDON_ENABLED_RESPONSE, enabled, diff --git a/src/content/actions/find.ts b/src/content/actions/find.ts index e08d7e5..6dd2ae6 100644 --- a/src/content/actions/find.ts +++ b/src/content/actions/find.ts @@ -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 = (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 (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 => { 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 => { return findNext(currentKeyword, reset, false); }; -const prev = (currentKeyword, reset) => { +const prev = ( + currentKeyword: string, reset: boolean, +): Promise => { return findNext(currentKeyword, reset, true); }; diff --git a/src/content/actions/follow-controller.ts b/src/content/actions/follow-controller.ts index 006b248..115b3b6 100644 --- a/src/content/actions/follow-controller.ts +++ b/src/content/actions/follow-controller.ts @@ -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, }; diff --git a/src/content/actions/index.ts b/src/content/actions/index.ts index 0a16fdf..18d0a69 100644 --- a/src/content/actions/index.ts +++ b/src/content/actions/index.ts @@ -1,31 +1,120 @@ -export default { - // Enable/disable - ADDON_SET_ENABLED: 'addon.set.enabled', - - // Settings - SETTING_SET: 'setting.set', - - // User input - INPUT_KEY_PRESS: 'input.key.press', - INPUT_CLEAR_KEYS: 'input.clear.keys', - - // Completion - COMPLETION_SET_ITEMS: 'completion.set.items', - COMPLETION_SELECT_NEXT: 'completions.select.next', - COMPLETION_SELECT_PREV: 'completions.select.prev', - - // 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', - - // Find - FIND_SET_KEYWORD: 'find.set.keyword', - - // Mark - MARK_START_SET: 'mark.start.set', - MARK_START_JUMP: 'mark.start.jump', - MARK_CANCEL: 'mark.cancel', - MARK_SET_LOCAL: 'mark.set.local', -}; +import Redux from 'redux'; + +// Enable/disable +export const ADDON_SET_ENABLED = 'addon.set.enabled'; + +// Find +export const FIND_SET_KEYWORD = 'find.set.keyword'; + +// Settings +export const SETTING_SET = 'setting.set'; + +// User input +export const INPUT_KEY_PRESS = 'input.key.press'; +export const INPUT_CLEAR_KEYS = 'input.clear.keys'; + +// Completion +export const COMPLETION_SET_ITEMS = 'completion.set.items'; +export const COMPLETION_SELECT_NEXT = 'completions.select.next'; +export const COMPLETION_SELECT_PREV = 'completions.select.prev'; + +// Follow +export const FOLLOW_CONTROLLER_ENABLE = 'follow.controller.enable'; +export const FOLLOW_CONTROLLER_DISABLE = 'follow.controller.disable'; +export const FOLLOW_CONTROLLER_KEY_PRESS = 'follow.controller.key.press'; +export const FOLLOW_CONTROLLER_BACKSPACE = 'follow.controller.backspace'; + +// Mark +export const MARK_START_SET = 'mark.start.set'; +export const MARK_START_JUMP = 'mark.start.jump'; +export const MARK_CANCEL = 'mark.cancel'; +export const MARK_SET_LOCAL = 'mark.set.local'; + +export const NOOP = 'noop'; + +export interface AddonSetEnabledAction extends Redux.Action { + type: typeof ADDON_SET_ENABLED; + enabled: boolean; +} + +export interface FindSetKeywordAction extends Redux.Action { + type: typeof FIND_SET_KEYWORD; + keyword: string; + found: boolean; +} + +export interface SettingSetAction extends Redux.Action { + type: typeof SETTING_SET; + 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; diff --git a/src/content/actions/input.ts b/src/content/actions/input.ts index 465a486..21c912e 100644 --- a/src/content/actions/input.ts +++ b/src/content/actions/input.ts @@ -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 }; diff --git a/src/content/actions/mark.ts b/src/content/actions/mark.ts index 712a811..5eb9554 100644 --- a/src/content/actions/mark.ts +++ b/src/content/actions/mark.ts @@ -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 { diff --git a/src/content/actions/operation.ts b/src/content/actions/operation.ts index ed9b2cf..6acb407 100644 --- a/src/content/actions/operation.ts +++ b/src/content/actions/operation.ts @@ -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 => { 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 }; diff --git a/src/content/actions/setting.ts b/src/content/actions/setting.ts index 1c15dd7..a8f049a 100644 --- a/src/content/actions/setting.ts +++ b/src/content/actions/setting.ts @@ -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 = { '': { type: operations.CANCEL }, '': { 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 => { let settings = await browser.runtime.sendMessage({ type: messages.SETTINGS_QUERY, }); diff --git a/src/content/components/common/follow.ts b/src/content/components/common/follow.ts index 63ce603..67f2dd9 100644 --- a/src/content/components/common/follow.ts +++ b/src/content/components/common/follow.ts @@ -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; +} + +interface Point { + x: number; + y: number; +} -const inViewport = (win, element, viewSize, framePosition) => { +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); diff --git a/src/content/components/common/hint.ts b/src/content/components/common/hint.ts index 1472587..2fcbb0f 100644 --- a/src/content/components/common/hint.ts +++ b/src/content/components/common/hint.ts @@ -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; - this.target = target; + private element: HTMLElement; + constructor(target: HTMLElement, tag: string) { let doc = target.ownerDocument; + if (doc === null) { + throw new TypeError('ownerDocument is null'); + } + let { x, y } = hintPosition(target); let { scrollX, scrollY } = window; + this.target = target; + this.element = doc.createElement('span'); this.element.className = 'vimvixen-hint'; this.element.textContent = tag; @@ -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; + } } diff --git a/src/content/components/common/index.ts b/src/content/components/common/index.ts index bcab4fa..9b5164e 100644 --- a/src/content/components/common/index.ts +++ b/src/content/components/common/index.ts @@ -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); diff --git a/src/content/components/common/input.ts b/src/content/components/common/input.ts index eefaf10..64eb5f3 100644 --- a/src/content/components/common/input.ts +++ b/src/content/components/common/input.ts @@ -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); } } diff --git a/src/content/components/common/keymapper.ts b/src/content/components/common/keymapper.ts index ec0d093..d9c9834 100644 --- a/src/content/components/common/keymapper.ts +++ b/src/content/components/common/keymapper.ts @@ -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) { diff --git a/src/content/components/common/mark.ts b/src/content/components/common/mark.ts index 0f838a9..500d03b 100644 --- a/src/content/components/common/mark.ts +++ b/src/content/components/common/mark.ts @@ -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; }; diff --git a/src/content/components/top-content/find.ts b/src/content/components/top-content/find.ts index 4d46d79..74b95bc 100644 --- a/src/content/components/top-content/find.ts +++ b/src/content/components/top-content/find.ts @@ -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)); } } diff --git a/src/content/components/top-content/follow-controller.ts b/src/content/components/top-content/follow-controller.ts index 7f36604..be71f6e 100644 --- a/src/content/components/top-content/follow-controller.ts +++ b/src/content/components/top-content/follow-controller.ts @@ -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, diff --git a/src/content/components/top-content/index.ts b/src/content/components/top-content/index.ts index 1aaef1b..ac95ea9 100644 --- a/src/content/components/top-content/index.ts +++ b/src/content/components/top-content/index.ts @@ -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, diff --git a/src/content/console-frames.ts b/src/content/console-frames.ts index ecb5a87..bd6b835 100644 --- a/src/content/console-frames.ts +++ b/src/content/console-frames.ts @@ -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 => { + 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 => { + return browser.runtime.sendMessage({ type: messages.CONSOLE_FRAME_MESSAGE, message: { type: messages.CONSOLE_SHOW_INFO, diff --git a/src/content/focuses.ts b/src/content/focuses.ts index a6f6cc8..8f53881 100644 --- a/src/content/focuses.ts +++ b/src/content/focuses.ts @@ -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(); } }; diff --git a/src/content/hint-key-producer.ts b/src/content/hint-key-producer.ts index 14b23b6..935394e 100644 --- a/src/content/hint-key-producer.ts +++ b/src/content/hint-key-producer.ts @@ -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); diff --git a/src/content/index.ts b/src/content/index.ts index 9edb712..309f27f 100644 --- a/src/content/index.ts +++ b/src/content/index.ts @@ -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 diff --git a/src/content/navigates.ts b/src/content/navigates.ts index c9baa30..a2007a6 100644 --- a/src/content/navigates.ts +++ b/src/content/navigates.ts @@ -1,58 +1,63 @@ -const REL_PATTERN = { +const REL_PATTERN: {[key: string]: RegExp} = { prev: /^(?:prev(?:ious)?|older)\b|\u2039|\u2190|\xab|\u226a|<>/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( + win: Window, + selector: string, + filter?: (e: E) => boolean, +): E | null { + let nodes = Array.from( + win.document.querySelectorAll(selector) as NodeListOf + ); 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(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(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 }; diff --git a/src/content/reducers/addon.ts b/src/content/reducers/addon.ts index 0def55a..2131228 100644 --- a/src/content/reducers/addon.ts +++ b/src/content/reducers/addon.ts @@ -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, diff --git a/src/content/reducers/find.ts b/src/content/reducers/find.ts index 4560e2c..8c3e637 100644 --- a/src/content/reducers/find.ts +++ b/src/content/reducers/find.ts @@ -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, diff --git a/src/content/reducers/follow-controller.ts b/src/content/reducers/follow-controller.ts index 5869c47..6965704 100644 --- a/src/content/reducers/follow-controller.ts +++ b/src/content/reducers/follow-controller.ts @@ -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, diff --git a/src/content/reducers/index.ts b/src/content/reducers/index.ts index bf612a3..fb5eb84 100644 --- a/src/content/reducers/index.ts +++ b/src/content/reducers/index.ts @@ -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, diff --git a/src/content/reducers/input.ts b/src/content/reducers/input.ts index 23e7dd2..6257e49 100644 --- a/src/content/reducers/input.ts +++ b/src/content/reducers/input.ts @@ -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, diff --git a/src/content/reducers/mark.ts b/src/content/reducers/mark.ts index 2c96cc5..e78b7b9 100644 --- a/src/content/reducers/mark.ts +++ b/src/content/reducers/mark.ts @@ -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 }; diff --git a/src/content/reducers/setting.ts b/src/content/reducers/setting.ts index a49db6d..fa8e8ee 100644 --- a/src/content/reducers/setting.ts +++ b/src/content/reducers/setting.ts @@ -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 }; diff --git a/src/content/scrolls.ts b/src/content/scrolls.ts index bbf2491..6a35315 100644 --- a/src/content/scrolls.ts +++ b/src/content/scrolls.ts @@ -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; diff --git a/src/content/store/index.ts b/src/content/store/index.ts new file mode 100644 index 0000000..5c41744 --- /dev/null +++ b/src/content/store/index.ts @@ -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), +); diff --git a/src/content/urls.ts b/src/content/urls.ts index 6e7ea31..390efde 100644 --- a/src/content/urls.ts +++ b/src/content/urls.ts @@ -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, diff --git a/src/shared/messages.ts b/src/shared/messages.ts index 2bc12d8..41b0f0b 100644 --- a/src/shared/messages.ts +++ b/src/shared/messages.ts @@ -1,78 +1,276 @@ -type WebMessageSender = Window | MessagePort | ServiceWorker | null; -type WebMessageListener = (msg: any, sender: WebMessageSender | null) => void; - -const onWebMessage = (listener: WebMessageListener) => { - 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); - }); -}; +import * as operations from './operations'; -const onBackgroundMessage = ( - listener: (msg: any, sender: browser.runtime.MessageSender, -) => void) => { - browser.runtime.onMessage.addListener(listener); -}; +export const BACKGROUND_OPERATION = 'background.operation'; -const onMessage = ( - listener: (msg: any, sender: WebMessageSender | browser.runtime.MessageSender, -) => void) => { - onWebMessage(listener); - onBackgroundMessage(listener); -}; +export const CONSOLE_UNFOCUS = 'console.unfocus'; +export const CONSOLE_ENTER_COMMAND = 'console.enter.command'; +export const CONSOLE_ENTER_FIND = 'console.enter.find'; +export const CONSOLE_QUERY_COMPLETIONS = 'console.query.completions'; +export const CONSOLE_SHOW_COMMAND = 'console.show.command'; +export const CONSOLE_SHOW_ERROR = 'console.show.error'; +export const CONSOLE_SHOW_INFO = 'console.show.info'; +export const CONSOLE_SHOW_FIND = 'console.show.find'; +export const CONSOLE_HIDE = 'console.hide'; + +export const FOLLOW_START = 'follow.start'; +export const FOLLOW_REQUEST_COUNT_TARGETS = 'follow.request.count.targets'; +export const FOLLOW_RESPONSE_COUNT_TARGETS = 'follow.response.count.targets'; +export const FOLLOW_CREATE_HINTS = 'follow.create.hints'; +export const FOLLOW_SHOW_HINTS = 'follow.update.hints'; +export const FOLLOW_REMOVE_HINTS = 'follow.remove.hints'; +export const FOLLOW_ACTIVATE = 'follow.activate'; +export const FOLLOW_KEY_PRESS = 'follow.key.press'; + +export const MARK_SET_GLOBAL = 'mark.set.global'; +export const MARK_JUMP_GLOBAL = 'mark.jump.global'; + +export const TAB_SCROLL_TO = 'tab.scroll.to'; + +export const FIND_NEXT = 'find.next'; +export const FIND_PREV = 'find.prev'; +export const FIND_GET_KEYWORD = 'find.get.keyword'; +export const FIND_SET_KEYWORD = 'find.set.keyword'; + +export const ADDON_ENABLED_QUERY = 'addon.enabled.query'; +export const ADDON_ENABLED_RESPONSE = 'addon.enabled.response'; +export const ADDON_TOGGLE_ENABLED = 'addon.toggle.enabled'; + +export const OPEN_URL = 'open.url'; + +export const SETTINGS_CHANGED = 'settings.changed'; +export const SETTINGS_QUERY = 'settings.query'; + +export const CONSOLE_FRAME_MESSAGE = 'console.frame.message'; + +interface BackgroundOperationMessage { + type: typeof BACKGROUND_OPERATION; + operation: operations.Operation; +} + +interface ConsoleUnfocusMessage { + type: typeof CONSOLE_UNFOCUS; +} + +interface ConsoleEnterCommandMessage { + type: typeof CONSOLE_ENTER_COMMAND; + text: string; +} + +interface ConsoleEnterFindMessage { + type: typeof CONSOLE_ENTER_FIND; + text: string; +} + +interface ConsoleQueryCompletionsMessage { + type: typeof CONSOLE_QUERY_COMPLETIONS; + text: string; +} + +interface ConsoleShowCommandMessage { + type: typeof CONSOLE_SHOW_COMMAND; + command: string; +} + +interface ConsoleShowErrorMessage { + type: typeof CONSOLE_SHOW_ERROR; + text: string; +} + +interface ConsoleShowInfoMessage { + type: typeof CONSOLE_SHOW_INFO; + text: string; +} + +interface ConsoleShowFindMessage { + type: typeof CONSOLE_SHOW_FIND; +} + +interface ConsoleHideMessage { + type: typeof CONSOLE_HIDE; +} + +interface FollowStartMessage { + type: typeof FOLLOW_START; + newTab: boolean; + background: boolean; +} + +interface FollowRequestCountTargetsMessage { + type: typeof FOLLOW_REQUEST_COUNT_TARGETS; + viewSize: { width: number, height: number }; + framePosition: { x: number, y: number }; +} + +interface FollowResponseCountTargetsMessage { + type: typeof FOLLOW_RESPONSE_COUNT_TARGETS; + count: number; +} + +interface FollowCreateHintsMessage { + type: typeof FOLLOW_CREATE_HINTS; + keysArray: string[]; + newTab: boolean; + background: boolean; +} + +interface FollowShowHintsMessage { + type: typeof FOLLOW_SHOW_HINTS; + keys: string; +} + +interface FollowRemoveHintsMessage { + type: typeof FOLLOW_REMOVE_HINTS; +} + +interface FollowActivateMessage { + type: typeof FOLLOW_ACTIVATE; + keys: string; +} + +interface FollowKeyPressMessage { + type: typeof FOLLOW_KEY_PRESS; + key: string; + ctrlKey: boolean; +} + +interface MarkSetGlobalMessage { + type: typeof MARK_SET_GLOBAL; + key: string; + x: number; + y: number; +} + +interface MarkJumpGlobalMessage { + type: typeof MARK_JUMP_GLOBAL; + key: string; +} + +interface TabScrollToMessage { + type: typeof TAB_SCROLL_TO; + x: number; + y: number; +} + +interface FindNextMessage { + type: typeof FIND_NEXT; +} + +interface FindPrevMessage { + type: typeof FIND_PREV; +} + +interface FindGetKeywordMessage { + type: typeof FIND_GET_KEYWORD; +} + +interface FindSetKeywordMessage { + type: typeof FIND_SET_KEYWORD; + keyword: string; + found: boolean; +} + +interface AddonEnabledQueryMessage { + type: typeof ADDON_ENABLED_QUERY; +} + +interface AddonEnabledResponseMessage { + type: typeof ADDON_ENABLED_RESPONSE; + enabled: boolean; +} + +interface AddonToggleEnabledMessage { + type: typeof ADDON_TOGGLE_ENABLED; +} + +interface OpenUrlMessage { + type: typeof OPEN_URL; + url: string; + newTab: boolean; + background: boolean; +} + +interface SettingsChangedMessage { + type: typeof SETTINGS_CHANGED; +} + +interface SettingsQueryMessage { + type: typeof SETTINGS_QUERY; +} + +interface ConsoleFrameMessageMessage { + type: typeof CONSOLE_FRAME_MESSAGE; + message: any; +} + +export type Message = + BackgroundOperationMessage | + ConsoleUnfocusMessage | + ConsoleEnterCommandMessage | + ConsoleEnterFindMessage | + ConsoleQueryCompletionsMessage | + ConsoleShowCommandMessage | + ConsoleShowErrorMessage | + ConsoleShowInfoMessage | + ConsoleShowFindMessage | + ConsoleHideMessage | + FollowStartMessage | + FollowRequestCountTargetsMessage | + FollowResponseCountTargetsMessage | + FollowCreateHintsMessage | + FollowShowHintsMessage | + FollowRemoveHintsMessage | + FollowActivateMessage | + FollowKeyPressMessage | + MarkSetGlobalMessage | + MarkJumpGlobalMessage | + TabScrollToMessage | + FindNextMessage | + FindPrevMessage | + FindGetKeywordMessage | + FindSetKeywordMessage | + AddonEnabledQueryMessage | + AddonEnabledResponseMessage | + AddonToggleEnabledMessage | + OpenUrlMessage | + SettingsChangedMessage | + SettingsQueryMessage | + ConsoleFrameMessageMessage; -export default { - BACKGROUND_OPERATION: 'background.operation', - - CONSOLE_UNFOCUS: 'console.unfocus', - CONSOLE_ENTER_COMMAND: 'console.enter.command', - CONSOLE_ENTER_FIND: 'console.enter.find', - CONSOLE_QUERY_COMPLETIONS: 'console.query.completions', - CONSOLE_SHOW_COMMAND: 'console.show.command', - CONSOLE_SHOW_ERROR: 'console.show.error', - CONSOLE_SHOW_INFO: 'console.show.info', - CONSOLE_SHOW_FIND: 'console.show.find', - CONSOLE_HIDE: 'console.hide', - - FOLLOW_START: 'follow.start', - FOLLOW_REQUEST_COUNT_TARGETS: 'follow.request.count.targets', - FOLLOW_RESPONSE_COUNT_TARGETS: 'follow.response.count.targets', - FOLLOW_CREATE_HINTS: 'follow.create.hints', - FOLLOW_SHOW_HINTS: 'follow.update.hints', - FOLLOW_REMOVE_HINTS: 'follow.remove.hints', - FOLLOW_ACTIVATE: 'follow.activate', - FOLLOW_KEY_PRESS: 'follow.key.press', - - MARK_SET_GLOBAL: 'mark.set.global', - MARK_JUMP_GLOBAL: 'mark.jump.global', - - TAB_SCROLL_TO: 'tab.scroll.to', - - FIND_NEXT: 'find.next', - FIND_PREV: 'find.prev', - FIND_GET_KEYWORD: 'find.get.keyword', - FIND_SET_KEYWORD: 'find.set.keyword', - - ADDON_ENABLED_QUERY: 'addon.enabled.query', - ADDON_ENABLED_RESPONSE: 'addon.enabled.response', - ADDON_TOGGLE_ENABLED: 'addon.toggle.enabled', - - OPEN_URL: 'open.url', - - SETTINGS_CHANGED: 'settings.changed', - SETTINGS_QUERY: 'settings.query', - - WINDOW_TOP_MESSAGE: 'window.top.message', - CONSOLE_FRAME_MESSAGE: 'console.frame.message', - - onWebMessage, - onBackgroundMessage, - onMessage, +// eslint-disable-next-line complexity +export const valueOf = (o: any): Message => { + switch (o.type) { + case CONSOLE_UNFOCUS: + case CONSOLE_ENTER_COMMAND: + case CONSOLE_ENTER_FIND: + case CONSOLE_QUERY_COMPLETIONS: + case CONSOLE_SHOW_COMMAND: + case CONSOLE_SHOW_ERROR: + case CONSOLE_SHOW_INFO: + case CONSOLE_SHOW_FIND: + case CONSOLE_HIDE: + case FOLLOW_START: + case FOLLOW_REQUEST_COUNT_TARGETS: + case FOLLOW_RESPONSE_COUNT_TARGETS: + case FOLLOW_CREATE_HINTS: + case FOLLOW_SHOW_HINTS: + case FOLLOW_REMOVE_HINTS: + case FOLLOW_ACTIVATE: + case FOLLOW_KEY_PRESS: + case MARK_SET_GLOBAL: + case MARK_JUMP_GLOBAL: + case TAB_SCROLL_TO: + case FIND_NEXT: + case FIND_PREV: + case FIND_GET_KEYWORD: + case FIND_SET_KEYWORD: + case ADDON_ENABLED_QUERY: + case ADDON_ENABLED_RESPONSE: + case ADDON_TOGGLE_ENABLED: + case OPEN_URL: + case SETTINGS_CHANGED: + case SETTINGS_QUERY: + case CONSOLE_FRAME_MESSAGE: + return o; + } + throw new Error('unknown operation type: ' + o.type); }; diff --git a/src/shared/operations.ts b/src/shared/operations.ts index d59723e..cc22f75 100644 --- a/src/shared/operations.ts +++ b/src/shared/operations.ts @@ -1,80 +1,447 @@ -const operations: { [key: string]: string } = { - // Hide console, or cancel some user actions - CANCEL: 'cancel', - - // Addons - ADDON_ENABLE: 'addon.enable', - ADDON_DISABLE: 'addon.disable', - ADDON_TOGGLE_ENABLED: 'addon.toggle.enabled', - - // Command - COMMAND_SHOW: 'command.show', - COMMAND_SHOW_OPEN: 'command.show.open', - COMMAND_SHOW_TABOPEN: 'command.show.tabopen', - COMMAND_SHOW_WINOPEN: 'command.show.winopen', - COMMAND_SHOW_BUFFER: 'command.show.buffer', - COMMAND_SHOW_ADDBOOKMARK: 'command.show.addbookmark', - - // Scrolls - SCROLL_VERTICALLY: 'scroll.vertically', - SCROLL_HORIZONALLY: 'scroll.horizonally', - SCROLL_PAGES: 'scroll.pages', - SCROLL_TOP: 'scroll.top', - SCROLL_BOTTOM: 'scroll.bottom', - SCROLL_HOME: 'scroll.home', - SCROLL_END: 'scroll.end', - - // Follows - FOLLOW_START: 'follow.start', - - // Navigations - NAVIGATE_HISTORY_PREV: 'navigate.history.prev', - NAVIGATE_HISTORY_NEXT: 'navigate.history.next', - NAVIGATE_LINK_PREV: 'navigate.link.prev', - NAVIGATE_LINK_NEXT: 'navigate.link.next', - NAVIGATE_PARENT: 'navigate.parent', - NAVIGATE_ROOT: 'navigate.root', - - // Focus - FOCUS_INPUT: 'focus.input', - - // Page - PAGE_SOURCE: 'page.source', - PAGE_HOME: 'page.home', - - // Tabs - TAB_CLOSE: 'tabs.close', - TAB_CLOSE_FORCE: 'tabs.close.force', - TAB_CLOSE_RIGHT: 'tabs.close.right', - TAB_REOPEN: 'tabs.reopen', - TAB_PREV: 'tabs.prev', - TAB_NEXT: 'tabs.next', - TAB_FIRST: 'tabs.first', - TAB_LAST: 'tabs.last', - TAB_PREV_SEL: 'tabs.prevsel', - TAB_RELOAD: 'tabs.reload', - TAB_PIN: 'tabs.pin', - TAB_UNPIN: 'tabs.unpin', - TAB_TOGGLE_PINNED: 'tabs.pin.toggle', - TAB_DUPLICATE: 'tabs.duplicate', - - // Zooms - ZOOM_IN: 'zoom.in', - ZOOM_OUT: 'zoom.out', - ZOOM_NEUTRAL: 'zoom.neutral', - - // Url yank/paste - URLS_YANK: 'urls.yank', - URLS_PASTE: 'urls.paste', - - // Find - FIND_START: 'find.start', - FIND_NEXT: 'find.next', - FIND_PREV: 'find.prev', - - // Mark - MARK_SET_PREFIX: 'mark.set.prefix', - MARK_JUMP_PREFIX: 'mark.jump.prefix', +// Hide console; or cancel some user actions +export const CANCEL = 'cancel'; + +// Addons +export const ADDON_ENABLE = 'addon.enable'; +export const ADDON_DISABLE = 'addon.disable'; +export const ADDON_TOGGLE_ENABLED = 'addon.toggle.enabled'; + +// Command +export const COMMAND_SHOW = 'command.show'; +export const COMMAND_SHOW_OPEN = 'command.show.open'; +export const COMMAND_SHOW_TABOPEN = 'command.show.tabopen'; +export const COMMAND_SHOW_WINOPEN = 'command.show.winopen'; +export const COMMAND_SHOW_BUFFER = 'command.show.buffer'; +export const COMMAND_SHOW_ADDBOOKMARK = 'command.show.addbookmark'; + +// Scrolls +export const SCROLL_VERTICALLY = 'scroll.vertically'; +export const SCROLL_HORIZONALLY = 'scroll.horizonally'; +export const SCROLL_PAGES = 'scroll.pages'; +export const SCROLL_TOP = 'scroll.top'; +export const SCROLL_BOTTOM = 'scroll.bottom'; +export const SCROLL_HOME = 'scroll.home'; +export const SCROLL_END = 'scroll.end'; + +// Follows +export const FOLLOW_START = 'follow.start'; + +// Navigations +export const NAVIGATE_HISTORY_PREV = 'navigate.history.prev'; +export const NAVIGATE_HISTORY_NEXT = 'navigate.history.next'; +export const NAVIGATE_LINK_PREV = 'navigate.link.prev'; +export const NAVIGATE_LINK_NEXT = 'navigate.link.next'; +export const NAVIGATE_PARENT = 'navigate.parent'; +export const NAVIGATE_ROOT = 'navigate.root'; + +// Focus +export const FOCUS_INPUT = 'focus.input'; + +// Page +export const PAGE_SOURCE = 'page.source'; +export const PAGE_HOME = 'page.home'; + +// Tabs +export const TAB_CLOSE = 'tabs.close'; +export const TAB_CLOSE_FORCE = 'tabs.close.force'; +export const TAB_CLOSE_RIGHT = 'tabs.close.right'; +export const TAB_REOPEN = 'tabs.reopen'; +export const TAB_PREV = 'tabs.prev'; +export const TAB_NEXT = 'tabs.next'; +export const TAB_FIRST = 'tabs.first'; +export const TAB_LAST = 'tabs.last'; +export const TAB_PREV_SEL = 'tabs.prevsel'; +export const TAB_RELOAD = 'tabs.reload'; +export const TAB_PIN = 'tabs.pin'; +export const TAB_UNPIN = 'tabs.unpin'; +export const TAB_TOGGLE_PINNED = 'tabs.pin.toggle'; +export const TAB_DUPLICATE = 'tabs.duplicate'; + +// Zooms +export const ZOOM_IN = 'zoom.in'; +export const ZOOM_OUT = 'zoom.out'; +export const ZOOM_NEUTRAL = 'zoom.neutral'; + +// Url yank/paste +export const URLS_YANK = 'urls.yank'; +export const URLS_PASTE = 'urls.paste'; + +// Find +export const FIND_START = 'find.start'; +export const FIND_NEXT = 'find.next'; +export const FIND_PREV = 'find.prev'; + +// Mark +export const MARK_SET_PREFIX = 'mark.set.prefix'; +export const MARK_JUMP_PREFIX = 'mark.jump.prefix'; + +export interface CancelOperation { + type: typeof CANCEL; +} + +export interface AddonEnableOperation { + type: typeof ADDON_ENABLE; +} + +export interface AddonDisableOperation { + type: typeof ADDON_DISABLE; +} + +export interface AddonToggleEnabledOperation { + type: typeof ADDON_TOGGLE_ENABLED; +} + +export interface CommandShowOperation { + type: typeof COMMAND_SHOW; +} + +export interface CommandShowOpenOperation { + type: typeof COMMAND_SHOW_OPEN; + alter: boolean; +} + +export interface CommandShowTabopenOperation { + type: typeof COMMAND_SHOW_TABOPEN; + alter: boolean; +} + +export interface CommandShowWinopenOperation { + type: typeof COMMAND_SHOW_WINOPEN; + alter: boolean; +} + +export interface CommandShowBufferOperation { + type: typeof COMMAND_SHOW_BUFFER; +} + +export interface CommandShowAddbookmarkOperation { + type: typeof COMMAND_SHOW_ADDBOOKMARK; + alter: boolean; +} + +export interface ScrollVerticallyOperation { + type: typeof SCROLL_VERTICALLY; + count: number; +} + +export interface ScrollHorizonallyOperation { + type: typeof SCROLL_HORIZONALLY; + count: number; +} + +export interface ScrollPagesOperation { + type: typeof SCROLL_PAGES; + count: number; +} + +export interface ScrollTopOperation { + type: typeof SCROLL_TOP; +} + +export interface ScrollBottomOperation { + type: typeof SCROLL_BOTTOM; +} + +export interface ScrollHomeOperation { + type: typeof SCROLL_HOME; +} + +export interface ScrollEndOperation { + type: typeof SCROLL_END; +} + +export interface FollowStartOperation { + type: typeof FOLLOW_START; + newTab: boolean; + background: boolean; +} + +export interface NavigateHistoryPrevOperation { + type: typeof NAVIGATE_HISTORY_PREV; +} + +export interface NavigateHistoryNextOperation { + type: typeof NAVIGATE_HISTORY_NEXT; +} + +export interface NavigateLinkPrevOperation { + type: typeof NAVIGATE_LINK_PREV; +} + +export interface NavigateLinkNextOperation { + type: typeof NAVIGATE_LINK_NEXT; +} + +export interface NavigateParentOperation { + type: typeof NAVIGATE_PARENT; +} + +export interface NavigateRootOperation { + type: typeof NAVIGATE_ROOT; +} + +export interface FocusInputOperation { + type: typeof FOCUS_INPUT; +} + +export interface PageSourceOperation { + type: typeof PAGE_SOURCE; +} + +export interface PageHomeOperation { + type: typeof PAGE_HOME; + newTab: boolean; +} + +export interface TabCloseOperation { + type: typeof TAB_CLOSE; +} + +export interface TabCloseForceOperation { + type: typeof TAB_CLOSE_FORCE; +} + +export interface TabCloseRightOperation { + type: typeof TAB_CLOSE_RIGHT; +} + +export interface TabReopenOperation { + type: typeof TAB_REOPEN; +} + +export interface TabPrevOperation { + type: typeof TAB_PREV; +} + +export interface TabNextOperation { + type: typeof TAB_NEXT; +} + +export interface TabFirstOperation { + type: typeof TAB_FIRST; +} + +export interface TabLastOperation { + type: typeof TAB_LAST; +} + +export interface TabPrevSelOperation { + type: typeof TAB_PREV_SEL; +} + +export interface TabReloadOperation { + type: typeof TAB_RELOAD; + cache: boolean; +} + +export interface TabPinOperation { + type: typeof TAB_PIN; +} + +export interface TabUnpinOperation { + type: typeof TAB_UNPIN; +} + +export interface TabTogglePinnedOperation { + type: typeof TAB_TOGGLE_PINNED; +} + +export interface TabDuplicateOperation { + type: typeof TAB_DUPLICATE; +} + +export interface ZoomInOperation { + type: typeof ZOOM_IN; +} + +export interface ZoomOutOperation { + type: typeof ZOOM_OUT; +} + +export interface ZoomNeutralOperation { + type: typeof ZOOM_NEUTRAL; +} + +export interface UrlsYankOperation { + type: typeof URLS_YANK; +} + +export interface UrlsPasteOperation { + type: typeof URLS_PASTE; + newTab: boolean; +} + +export interface FindStartOperation { + type: typeof FIND_START; +} + +export interface FindNextOperation { + type: typeof FIND_NEXT; +} + +export interface FindPrevOperation { + type: typeof FIND_PREV; +} + +export interface MarkSetPrefixOperation { + type: typeof MARK_SET_PREFIX; +} + +export interface MarkJumpPrefixOperation { + type: typeof MARK_JUMP_PREFIX; +} + +export type Operation = + CancelOperation | + AddonEnableOperation | + AddonDisableOperation | + AddonToggleEnabledOperation | + CommandShowOperation | + CommandShowOpenOperation | + CommandShowTabopenOperation | + CommandShowWinopenOperation | + CommandShowBufferOperation | + CommandShowAddbookmarkOperation | + ScrollVerticallyOperation | + ScrollHorizonallyOperation | + ScrollPagesOperation | + ScrollTopOperation | + ScrollBottomOperation | + ScrollHomeOperation | + ScrollEndOperation | + FollowStartOperation | + NavigateHistoryPrevOperation | + NavigateHistoryNextOperation | + NavigateLinkPrevOperation | + NavigateLinkNextOperation | + NavigateParentOperation | + NavigateRootOperation | + FocusInputOperation | + PageSourceOperation | + PageHomeOperation | + TabCloseOperation | + TabCloseForceOperation | + TabCloseRightOperation | + TabReopenOperation | + TabPrevOperation | + TabNextOperation | + TabFirstOperation | + TabLastOperation | + TabPrevSelOperation | + TabReloadOperation | + TabPinOperation | + TabUnpinOperation | + TabTogglePinnedOperation | + TabDuplicateOperation | + ZoomInOperation | + ZoomOutOperation | + ZoomNeutralOperation | + UrlsYankOperation | + UrlsPasteOperation | + FindStartOperation | + FindNextOperation | + FindPrevOperation | + MarkSetPrefixOperation | + MarkJumpPrefixOperation; + +const assertOptionalBoolean = (obj: any, name: string) => { + if (Object.prototype.hasOwnProperty.call(obj, name) && + typeof obj[name] !== 'boolean') { + throw new TypeError(`Not a boolean parameter '${name}'`); + } +}; + +const assertRequiredNumber = (obj: any, name: string) => { + if (!Object.prototype.hasOwnProperty.call(obj, name) || + typeof obj[name] !== 'number') { + throw new TypeError(`Missing number parameter '${name}`); + } }; -export default operations; +// eslint-disable-next-line complexity, max-lines-per-function +export const valueOf = (o: any): Operation => { + if (!Object.prototype.hasOwnProperty.call(o, 'type')) { + throw new TypeError(`missing 'type' field`); + } + switch (o.type) { + case COMMAND_SHOW_OPEN: + case COMMAND_SHOW_TABOPEN: + case COMMAND_SHOW_WINOPEN: + case COMMAND_SHOW_ADDBOOKMARK: + assertOptionalBoolean(o, 'alter'); + return { type: o.type, alter: Boolean(o.alter) }; + case SCROLL_VERTICALLY: + case SCROLL_HORIZONALLY: + case SCROLL_PAGES: + assertRequiredNumber(o, 'count'); + return { type: o.type, count: Number(o.count) }; + case FOLLOW_START: + assertOptionalBoolean(o, 'newTab'); + assertOptionalBoolean(o, 'background'); + return { + type: FOLLOW_START, + newTab: Boolean(typeof o.newTab === undefined ? false : o.newTab), + background: Boolean(typeof o.background === undefined ? true : o.background), // eslint-disable-line max-len + }; + case PAGE_HOME: + assertOptionalBoolean(o, 'newTab'); + return { + type: PAGE_HOME, + newTab: Boolean(typeof o.newTab === undefined ? false : o.newTab), + }; + case TAB_RELOAD: + assertOptionalBoolean(o, 'cache'); + return { + type: TAB_RELOAD, + cache: Boolean(typeof o.cache === undefined ? false : o.cache), + }; + case URLS_PASTE: + assertOptionalBoolean(o, 'newTab'); + return { + type: URLS_PASTE, + newTab: Boolean(typeof o.newTab === undefined ? false : o.newTab), + }; + case CANCEL: + case ADDON_ENABLE: + case ADDON_DISABLE: + case ADDON_TOGGLE_ENABLED: + case COMMAND_SHOW: + case COMMAND_SHOW_BUFFER: + case SCROLL_TOP: + case SCROLL_BOTTOM: + case SCROLL_HOME: + case SCROLL_END: + case NAVIGATE_HISTORY_PREV: + case NAVIGATE_HISTORY_NEXT: + case NAVIGATE_LINK_PREV: + case NAVIGATE_LINK_NEXT: + case NAVIGATE_PARENT: + case NAVIGATE_ROOT: + case FOCUS_INPUT: + case PAGE_SOURCE: + case TAB_CLOSE: + case TAB_CLOSE_FORCE: + case TAB_CLOSE_RIGHT: + case TAB_REOPEN: + case TAB_PREV: + case TAB_NEXT: + case TAB_FIRST: + case TAB_LAST: + case TAB_PREV_SEL: + case TAB_PIN: + case TAB_UNPIN: + case TAB_TOGGLE_PINNED: + case TAB_DUPLICATE: + case ZOOM_IN: + case ZOOM_OUT: + case ZOOM_NEUTRAL: + case URLS_YANK: + case FIND_START: + case FIND_NEXT: + case FIND_PREV: + case MARK_SET_PREFIX: + case MARK_JUMP_PREFIX: + return { type: o.type }; + } + throw new Error('unknown operation type: ' + o.type); +}; diff --git a/src/shared/settings/validator.ts b/src/shared/settings/validator.ts index 0483931..71cc466 100644 --- a/src/shared/settings/validator.ts +++ b/src/shared/settings/validator.ts @@ -1,4 +1,4 @@ -import operations from '../operations'; +import * as operations from '../operations'; import * as properties from './properties'; const VALID_TOP_KEYS = ['keymaps', 'search', 'blacklist', 'properties']; diff --git a/src/shared/utils/keys.ts b/src/shared/utils/keys.ts index d9abef7..e9b0365 100644 --- a/src/shared/utils/keys.ts +++ b/src/shared/utils/keys.ts @@ -1,4 +1,4 @@ -interface Key { +export interface Key { key: string; shiftKey: boolean | undefined; ctrlKey: boolean | undefined; diff --git a/test/content/actions/follow-controller.test.ts b/test/content/actions/follow-controller.test.ts index 718a90a..a4b1710 100644 --- a/test/content/actions/follow-controller.test.ts +++ b/test/content/actions/follow-controller.test.ts @@ -1,4 +1,4 @@ -import actions from 'content/actions'; +import * as actions from 'content/actions'; import * as followControllerActions from 'content/actions/follow-controller'; describe('follow-controller actions', () => { diff --git a/test/content/actions/input.test.ts b/test/content/actions/input.test.ts index fe9db5f..33238a5 100644 --- a/test/content/actions/input.test.ts +++ b/test/content/actions/input.test.ts @@ -1,4 +1,4 @@ -import actions from 'content/actions'; +import * as actions from 'content/actions'; import * as inputActions from 'content/actions/input'; describe("input actions", () => { diff --git a/test/content/actions/mark.test.ts b/test/content/actions/mark.test.ts index adbf06b..6c6d59e 100644 --- a/test/content/actions/mark.test.ts +++ b/test/content/actions/mark.test.ts @@ -1,4 +1,4 @@ -import actions from 'content/actions'; +import * as actions from 'content/actions'; import * as markActions from 'content/actions/mark'; describe('mark actions', () => { diff --git a/test/content/actions/setting.test.ts b/test/content/actions/setting.test.ts index 10f6807..0721d5d 100644 --- a/test/content/actions/setting.test.ts +++ b/test/content/actions/setting.test.ts @@ -1,4 +1,4 @@ -import actions from 'content/actions'; +import * as actions from 'content/actions'; import * as settingActions from 'content/actions/setting'; describe("setting actions", () => { diff --git a/test/content/components/common/input.test.ts b/test/content/components/common/input.test.ts index 2ba5507..f3a943c 100644 --- a/test/content/components/common/input.test.ts +++ b/test/content/components/common/input.test.ts @@ -21,12 +21,14 @@ describe('InputComponent', () => { ++b; } }); - component.onKeyDown({ key: 'a' }); - component.onKeyDown({ key: 'b' }); - component.onKeyPress({ key: 'a' }); - component.onKeyUp({ key: 'a' }); - component.onKeyPress({ key: 'b' }); - component.onKeyUp({ key: 'b' }); + + let elem = document.body; + component.onKeyDown({ key: 'a', target: elem }); + component.onKeyDown({ key: 'b', target: elem }); + component.onKeyPress({ key: 'a', target: elem }); + component.onKeyUp({ key: 'a', target: elem }); + component.onKeyPress({ key: 'b', target: elem }); + component.onKeyUp({ key: 'b', target: elem }); expect(a).is.equals(1); expect(b).is.equals(1); diff --git a/test/content/reducers/addon.test.ts b/test/content/reducers/addon.test.ts index d4eb845..fb05244 100644 --- a/test/content/reducers/addon.test.ts +++ b/test/content/reducers/addon.test.ts @@ -1,4 +1,4 @@ -import actions from 'content/actions'; +import * as actions from 'content/actions'; import addonReducer from 'content/reducers/addon'; describe("addon reducer", () => { diff --git a/test/content/reducers/find.test.ts b/test/content/reducers/find.test.ts index a8c30d7..66a2c67 100644 --- a/test/content/reducers/find.test.ts +++ b/test/content/reducers/find.test.ts @@ -1,4 +1,4 @@ -import actions from 'content/actions'; +import * as actions from 'content/actions'; import findReducer from 'content/reducers/find'; describe("find reducer", () => { diff --git a/test/content/reducers/follow-controller.test.ts b/test/content/reducers/follow-controller.test.ts index 8a4c2d4..39f326c 100644 --- a/test/content/reducers/follow-controller.test.ts +++ b/test/content/reducers/follow-controller.test.ts @@ -1,4 +1,4 @@ -import actions from 'content/actions'; +import * as actions from 'content/actions'; import followControllerReducer from 'content/reducers/follow-controller'; describe('follow-controller reducer', () => { diff --git a/test/content/reducers/input.test.ts b/test/content/reducers/input.test.ts index 0011943..f892201 100644 --- a/test/content/reducers/input.test.ts +++ b/test/content/reducers/input.test.ts @@ -1,4 +1,4 @@ -import actions from 'content/actions'; +import * as actions from 'content/actions'; import inputReducer from 'content/reducers/input'; describe("input reducer", () => { diff --git a/test/content/reducers/mark.test.ts b/test/content/reducers/mark.test.ts index 76efbf7..1a51c3e 100644 --- a/test/content/reducers/mark.test.ts +++ b/test/content/reducers/mark.test.ts @@ -1,4 +1,4 @@ -import actions from 'content/actions'; +import * as actions from 'content/actions'; import reducer from 'content/reducers/mark'; describe("mark reducer", () => { diff --git a/test/content/reducers/setting.test.ts b/test/content/reducers/setting.test.ts index 4e4c095..226fc58 100644 --- a/test/content/reducers/setting.test.ts +++ b/test/content/reducers/setting.test.ts @@ -1,4 +1,4 @@ -import actions from 'content/actions'; +import * as actions from 'content/actions'; import settingReducer from 'content/reducers/setting'; describe("content setting reducer", () => { diff --git a/test/shared/operations.test.ts b/test/shared/operations.test.ts new file mode 100644 index 0000000..42a3eed --- /dev/null +++ b/test/shared/operations.test.ts @@ -0,0 +1,41 @@ +import * as operations from 'shared/operations'; + +describe('operations', () => { + describe('#valueOf', () => { + it('returns an Operation', () => { + let op: operations.Operation = operations.valueOf({ + type: operations.SCROLL_VERTICALLY, + count: 10, + }); + expect(op.type).to.equal(operations.SCROLL_VERTICALLY); + expect(op.count).to.equal(10); + }); + + it('throws an Error on missing required parameter', () => { + expect(() => operations.valueOf({ + type: operations.SCROLL_VERTICALLY, + })).to.throw(TypeError); + }); + + it('fills default valus of optional parameter', () => { + let op: operations.Operation = operations.valueOf({ + type: operations.COMMAND_SHOW_OPEN, + }); + + expect(op.type).to.equal(operations.COMMAND_SHOW_OPEN) + expect(op.alter).to.be.false; + }); + + it('throws an Error on mismatch of parameter', () => { + expect(() => operations.valueOf({ + type: operations.SCROLL_VERTICALLY, + count: '10', + })).to.throw(TypeError); + + expect(() => valueOf({ + type: operations.COMMAND_SHOW_OPEN, + alter: 'true', + })).to.throw(TypeError); + }); + }); +}) diff --git a/tsconfig.json b/tsconfig.json index 575601b..b61ee23 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,11 +2,11 @@ "compilerOptions": { "target": "es2017", "module": "commonjs", + "lib": ["es6", "dom", "es2017"], "allowJs": true, "checkJs": true, + "noEmit": true, "jsx": "react", - "declaration": true, - "declarationMap": true, "sourceMap": true, "outDir": "./build", "removeComments": true, @@ -29,5 +29,8 @@ "esModuleInterop": true, "typeRoots": ["node_modules/@types", "node_modules/web-ext-types"] - } + }, + "include": [ + "src" + ] } -- cgit v1.2.3