diff options
-rw-r--r-- | .eslintrc | 18 | ||||
-rw-r--r-- | karma.conf.js | 12 | ||||
-rw-r--r-- | package-lock.json | 373 | ||||
-rw-r--r-- | package.json | 16 | ||||
-rw-r--r-- | src/background/controllers/AddonEnabledController.ts (renamed from src/background/controllers/AddonEnabledController.js) | 4 | ||||
-rw-r--r-- | src/background/controllers/CommandController.ts (renamed from src/background/controllers/CommandController.js) | 14 | ||||
-rw-r--r-- | src/background/controllers/FindController.ts (renamed from src/background/controllers/FindController.js) | 6 | ||||
-rw-r--r-- | src/background/controllers/LinkController.js | 15 | ||||
-rw-r--r-- | src/background/controllers/LinkController.ts | 19 | ||||
-rw-r--r-- | src/background/controllers/MarkController.js | 15 | ||||
-rw-r--r-- | src/background/controllers/MarkController.ts | 17 | ||||
-rw-r--r-- | src/background/controllers/OperationController.ts (renamed from src/background/controllers/OperationController.js) | 15 | ||||
-rw-r--r-- | src/background/controllers/SettingController.ts (renamed from src/background/controllers/SettingController.js) | 9 | ||||
-rw-r--r-- | src/background/controllers/VersionController.ts (renamed from src/background/controllers/VersionController.js) | 6 | ||||
-rw-r--r-- | src/background/controllers/version.js | 13 | ||||
-rw-r--r-- | src/background/domains/CommandDocs.ts (renamed from src/background/domains/CommandDocs.js) | 3 | ||||
-rw-r--r-- | src/background/domains/CompletionGroup.js | 14 | ||||
-rw-r--r-- | src/background/domains/CompletionGroup.ts | 7 | ||||
-rw-r--r-- | src/background/domains/CompletionItem.js | 24 | ||||
-rw-r--r-- | src/background/domains/CompletionItem.ts | 7 | ||||
-rw-r--r-- | src/background/domains/Completions.js | 27 | ||||
-rw-r--r-- | src/background/domains/GlobalMark.js | 24 | ||||
-rw-r--r-- | src/background/domains/GlobalMark.ts | 7 | ||||
-rw-r--r-- | src/background/domains/Setting.js | 51 | ||||
-rw-r--r-- | src/background/index.ts (renamed from src/background/index.js) | 0 | ||||
-rw-r--r-- | src/background/infrastructures/ConsoleClient.ts (renamed from src/background/infrastructures/ConsoleClient.js) | 12 | ||||
-rw-r--r-- | src/background/infrastructures/ContentMessageClient.ts (renamed from src/background/infrastructures/ContentMessageClient.js) | 14 | ||||
-rw-r--r-- | src/background/infrastructures/ContentMessageListener.ts (renamed from src/background/infrastructures/ContentMessageListener.js) | 82 | ||||
-rw-r--r-- | src/background/infrastructures/MemoryStorage.ts (renamed from src/background/infrastructures/MemoryStorage.js) | 6 | ||||
-rw-r--r-- | src/background/presenters/IndicatorPresenter.ts (renamed from src/background/presenters/IndicatorPresenter.js) | 4 | ||||
-rw-r--r-- | src/background/presenters/NotifyPresenter.ts (renamed from src/background/presenters/NotifyPresenter.js) | 10 | ||||
-rw-r--r-- | src/background/presenters/TabPresenter.ts (renamed from src/background/presenters/TabPresenter.js) | 44 | ||||
-rw-r--r-- | src/background/presenters/WindowPresenter.ts (renamed from src/background/presenters/WindowPresenter.js) | 2 | ||||
-rw-r--r-- | src/background/repositories/BookmarkRepository.ts (renamed from src/background/repositories/BookmarkRepository.js) | 4 | ||||
-rw-r--r-- | src/background/repositories/BrowserSettingRepository.js | 8 | ||||
-rw-r--r-- | src/background/repositories/BrowserSettingRepository.ts | 24 | ||||
-rw-r--r-- | src/background/repositories/CompletionsRepository.ts (renamed from src/background/repositories/CompletionsRepository.js) | 14 | ||||
-rw-r--r-- | src/background/repositories/FindRepository.ts (renamed from src/background/repositories/FindRepository.js) | 6 | ||||
-rw-r--r-- | src/background/repositories/MarkRepository.ts (renamed from src/background/repositories/MarkRepository.js) | 8 | ||||
-rw-r--r-- | src/background/repositories/PersistentSettingRepository.ts (renamed from src/background/repositories/PersistentSettingRepository.js) | 6 | ||||
-rw-r--r-- | src/background/repositories/SettingRepository.js | 23 | ||||
-rw-r--r-- | src/background/repositories/SettingRepository.ts | 51 | ||||
-rw-r--r-- | src/background/repositories/VersionRepository.js | 10 | ||||
-rw-r--r-- | src/background/usecases/AddonEnabledUseCase.ts (renamed from src/background/usecases/AddonEnabledUseCase.js) | 18 | ||||
-rw-r--r-- | src/background/usecases/CommandUseCase.ts (renamed from src/background/usecases/CommandUseCase.js) | 57 | ||||
-rw-r--r-- | src/background/usecases/CompletionsUseCase.ts (renamed from src/background/usecases/CompletionsUseCase.js) | 135 | ||||
-rw-r--r-- | src/background/usecases/ConsoleUseCase.js | 61 | ||||
-rw-r--r-- | src/background/usecases/ConsoleUseCase.ts | 65 | ||||
-rw-r--r-- | src/background/usecases/FindUseCase.ts (renamed from src/background/usecases/FindUseCase.js) | 14 | ||||
-rw-r--r-- | src/background/usecases/LinkUseCase.ts (renamed from src/background/usecases/LinkUseCase.js) | 8 | ||||
-rw-r--r-- | src/background/usecases/MarkUseCase.ts (renamed from src/background/usecases/MarkUseCase.js) | 33 | ||||
-rw-r--r-- | src/background/usecases/SettingUseCase.ts (renamed from src/background/usecases/SettingUseCase.js) | 21 | ||||
-rw-r--r-- | src/background/usecases/TabSelectUseCase.ts (renamed from src/background/usecases/TabSelectUseCase.js) | 24 | ||||
-rw-r--r-- | src/background/usecases/TabUseCase.ts (renamed from src/background/usecases/TabUseCase.js) | 36 | ||||
-rw-r--r-- | src/background/usecases/VersionUseCase.ts (renamed from src/background/usecases/VersionUseCase.js) | 12 | ||||
-rw-r--r-- | src/background/usecases/ZoomUseCase.js | 35 | ||||
-rw-r--r-- | src/background/usecases/ZoomUseCase.ts | 39 | ||||
-rw-r--r-- | src/background/usecases/filters.js | 72 | ||||
-rw-r--r-- | src/background/usecases/filters.ts | 76 | ||||
-rw-r--r-- | src/background/usecases/parsers.js | 31 | ||||
-rw-r--r-- | src/background/usecases/parsers.ts | 36 | ||||
-rw-r--r-- | src/console/actions/console.ts (renamed from src/console/actions/console.js) | 34 | ||||
-rw-r--r-- | src/console/actions/index.js | 13 | ||||
-rw-r--r-- | src/console/actions/index.ts | 63 | ||||
-rw-r--r-- | src/console/components/Console.tsx (renamed from src/console/components/Console.jsx) | 72 | ||||
-rw-r--r-- | src/console/components/console/Completion.tsx (renamed from src/console/components/console/Completion.jsx) | 45 | ||||
-rw-r--r-- | src/console/components/console/CompletionItem.tsx (renamed from src/console/components/console/CompletionItem.jsx) | 9 | ||||
-rw-r--r-- | src/console/components/console/CompletionTitle.tsx (renamed from src/console/components/console/CompletionTitle.jsx) | 11 | ||||
-rw-r--r-- | src/console/components/console/Input.tsx (renamed from src/console/components/console/Input.jsx) | 33 | ||||
-rw-r--r-- | src/console/components/console/Message.tsx (renamed from src/console/components/console/Message.jsx) | 13 | ||||
-rw-r--r-- | src/console/index.tsx (renamed from src/console/index.jsx) | 19 | ||||
-rw-r--r-- | src/console/reducers/index.ts (renamed from src/console/reducers/index.js) | 23 | ||||
-rw-r--r-- | src/content/Mark.ts | 6 | ||||
-rw-r--r-- | src/content/MessageListener.ts | 32 | ||||
-rw-r--r-- | src/content/actions/addon.js | 19 | ||||
-rw-r--r-- | src/content/actions/addon.ts | 19 | ||||
-rw-r--r-- | src/content/actions/find.js | 68 | ||||
-rw-r--r-- | src/content/actions/find.ts | 100 | ||||
-rw-r--r-- | src/content/actions/follow-controller.ts (renamed from src/content/actions/follow-controller.js) | 12 | ||||
-rw-r--r-- | src/content/actions/index.js | 31 | ||||
-rw-r--r-- | src/content/actions/index.ts | 122 | ||||
-rw-r--r-- | src/content/actions/input.js | 16 | ||||
-rw-r--r-- | src/content/actions/input.ts | 17 | ||||
-rw-r--r-- | src/content/actions/mark.js | 46 | ||||
-rw-r--r-- | src/content/actions/mark.ts | 46 | ||||
-rw-r--r-- | src/content/actions/operation.ts (renamed from src/content/actions/operation.js) | 27 | ||||
-rw-r--r-- | src/content/actions/setting.js | 37 | ||||
-rw-r--r-- | src/content/actions/setting.ts | 28 | ||||
-rw-r--r-- | src/content/components/common/follow.ts (renamed from src/content/components/common/follow.js) | 79 | ||||
-rw-r--r-- | src/content/components/common/hint.ts (renamed from src/content/components/common/hint.js) | 33 | ||||
-rw-r--r-- | src/content/components/common/index.js | 55 | ||||
-rw-r--r-- | src/content/components/common/index.ts | 61 | ||||
-rw-r--r-- | src/content/components/common/input.ts (renamed from src/content/components/common/input.js) | 47 | ||||
-rw-r--r-- | src/content/components/common/keymapper.ts (renamed from src/content/components/common/keymapper.js) | 36 | ||||
-rw-r--r-- | src/content/components/common/mark.js | 74 | ||||
-rw-r--r-- | src/content/components/common/mark.ts | 79 | ||||
-rw-r--r-- | src/content/components/frame-content.ts (renamed from src/content/components/frame-content.js) | 0 | ||||
-rw-r--r-- | src/content/components/top-content/find.js | 41 | ||||
-rw-r--r-- | src/content/components/top-content/find.ts | 46 | ||||
-rw-r--r-- | src/content/components/top-content/follow-controller.ts (renamed from src/content/components/top-content/follow-controller.js) | 67 | ||||
-rw-r--r-- | src/content/components/top-content/index.ts (renamed from src/content/components/top-content/index.js) | 28 | ||||
-rw-r--r-- | src/content/console-frames.ts (renamed from src/content/console-frames.js) | 18 | ||||
-rw-r--r-- | src/content/focuses.ts (renamed from src/content/focuses.js) | 8 | ||||
-rw-r--r-- | src/content/hint-key-producer.ts (renamed from src/content/hint-key-producer.js) | 10 | ||||
-rw-r--r-- | src/content/index.ts (renamed from src/content/index.js) | 11 | ||||
-rw-r--r-- | src/content/navigates.ts (renamed from src/content/navigates.js) | 45 | ||||
-rw-r--r-- | src/content/reducers/addon.js | 15 | ||||
-rw-r--r-- | src/content/reducers/addon.ts | 22 | ||||
-rw-r--r-- | src/content/reducers/find.js | 17 | ||||
-rw-r--r-- | src/content/reducers/find.ts | 25 | ||||
-rw-r--r-- | src/content/reducers/follow-controller.ts (renamed from src/content/reducers/follow-controller.js) | 16 | ||||
-rw-r--r-- | src/content/reducers/index.js | 11 | ||||
-rw-r--r-- | src/content/reducers/index.ts | 21 | ||||
-rw-r--r-- | src/content/reducers/input.js | 18 | ||||
-rw-r--r-- | src/content/reducers/input.ts | 26 | ||||
-rw-r--r-- | src/content/reducers/mark.ts (renamed from src/content/reducers/mark.js) | 16 | ||||
-rw-r--r-- | src/content/reducers/setting.js | 16 | ||||
-rw-r--r-- | src/content/reducers/setting.ts | 40 | ||||
-rw-r--r-- | src/content/scrolls.ts (renamed from src/content/scrolls.js) | 48 | ||||
-rw-r--r-- | src/content/site-style.ts (renamed from src/content/site-style.js) | 2 | ||||
-rw-r--r-- | src/content/store/index.ts | 8 | ||||
-rw-r--r-- | src/content/urls.ts (renamed from src/content/urls.js) | 11 | ||||
-rw-r--r-- | src/settings/actions/index.js | 7 | ||||
-rw-r--r-- | src/settings/actions/index.ts | 36 | ||||
-rw-r--r-- | src/settings/actions/setting.js | 63 | ||||
-rw-r--r-- | src/settings/actions/setting.ts | 73 | ||||
-rw-r--r-- | src/settings/components/form/BlacklistForm.tsx (renamed from src/settings/components/form/BlacklistForm.jsx) | 28 | ||||
-rw-r--r-- | src/settings/components/form/KeymapsForm.tsx (renamed from src/settings/components/form/KeymapsForm.jsx) | 41 | ||||
-rw-r--r-- | src/settings/components/form/PropertiesForm.tsx (renamed from src/settings/components/form/PropertiesForm.jsx) | 32 | ||||
-rw-r--r-- | src/settings/components/form/SearchForm.tsx (renamed from src/settings/components/form/SearchForm.jsx) | 50 | ||||
-rw-r--r-- | src/settings/components/index.jsx | 153 | ||||
-rw-r--r-- | src/settings/components/index.tsx | 199 | ||||
-rw-r--r-- | src/settings/components/ui/AddButton.tsx (renamed from src/settings/components/ui/AddButton.jsx) | 5 | ||||
-rw-r--r-- | src/settings/components/ui/DeleteButton.tsx (renamed from src/settings/components/ui/DeleteButton.jsx) | 5 | ||||
-rw-r--r-- | src/settings/components/ui/Input.jsx | 60 | ||||
-rw-r--r-- | src/settings/components/ui/Input.tsx | 82 | ||||
-rw-r--r-- | src/settings/index.tsx (renamed from src/settings/index.jsx) | 0 | ||||
-rw-r--r-- | src/settings/keymaps.ts (renamed from src/settings/keymaps.js) | 3 | ||||
-rw-r--r-- | src/settings/reducers/setting.ts (renamed from src/settings/reducers/setting.js) | 28 | ||||
-rw-r--r-- | src/settings/storage.ts | 15 | ||||
-rw-r--r-- | src/shared/SettingData.ts | 414 | ||||
-rw-r--r-- | src/shared/Settings.ts | 200 | ||||
-rw-r--r-- | src/shared/blacklists.ts (renamed from src/shared/blacklists.js) | 4 | ||||
-rw-r--r-- | src/shared/messages.js | 71 | ||||
-rw-r--r-- | src/shared/messages.ts | 276 | ||||
-rw-r--r-- | src/shared/operations.js | 78 | ||||
-rw-r--r-- | src/shared/operations.ts | 447 | ||||
-rw-r--r-- | src/shared/properties.ts | 50 | ||||
-rw-r--r-- | src/shared/property-defs.ts | 50 | ||||
-rw-r--r-- | src/shared/settings/default.js | 85 | ||||
-rw-r--r-- | src/shared/settings/properties.js | 24 | ||||
-rw-r--r-- | src/shared/settings/storage.js | 32 | ||||
-rw-r--r-- | src/shared/settings/validator.js | 76 | ||||
-rw-r--r-- | src/shared/settings/values.js | 108 | ||||
-rw-r--r-- | src/shared/urls.ts (renamed from src/shared/urls.js) | 6 | ||||
-rw-r--r-- | src/shared/utils/dom.ts (renamed from src/shared/utils/dom.js) | 41 | ||||
-rw-r--r-- | src/shared/utils/keys.ts (renamed from src/shared/utils/keys.js) | 22 | ||||
-rw-r--r-- | src/shared/utils/re.ts (renamed from src/shared/utils/re.js) | 2 | ||||
-rw-r--r-- | test/background/domains/GlobalMark.test.js | 11 | ||||
-rw-r--r-- | test/background/infrastructures/MemoryStorage.test.ts (renamed from test/background/infrastructures/MemoryStorage.test.js) | 0 | ||||
-rw-r--r-- | test/background/repositories/Mark.test.ts (renamed from test/background/repositories/Mark.test.js) | 3 | ||||
-rw-r--r-- | test/background/repositories/Version.js | 34 | ||||
-rw-r--r-- | test/background/usecases/filters.test.ts (renamed from test/background/usecases/filters.test.js) | 0 | ||||
-rw-r--r-- | test/background/usecases/parsers.test.js | 47 | ||||
-rw-r--r-- | test/background/usecases/parsers.test.ts | 34 | ||||
-rw-r--r-- | test/console/actions/console.test.ts (renamed from test/console/actions/console.test.js) | 2 | ||||
-rw-r--r-- | test/console/components/console/Completion.test.tsx (renamed from test/console/components/console/Completion.test.jsx) | 0 | ||||
-rw-r--r-- | test/console/reducers/console.test.ts (renamed from test/console/reducers/console.test.js) | 2 | ||||
-rw-r--r-- | test/content/actions/follow-controller.test.ts (renamed from test/content/actions/follow-controller.test.js) | 2 | ||||
-rw-r--r-- | test/content/actions/input.test.ts (renamed from test/content/actions/input.test.js) | 2 | ||||
-rw-r--r-- | test/content/actions/mark.test.ts (renamed from test/content/actions/mark.test.js) | 2 | ||||
-rw-r--r-- | test/content/actions/setting.test.js | 35 | ||||
-rw-r--r-- | test/content/actions/setting.test.ts | 43 | ||||
-rw-r--r-- | test/content/components/common/follow.test.ts (renamed from test/content/components/common/follow.test.js) | 0 | ||||
-rw-r--r-- | test/content/components/common/hint.test.ts (renamed from test/content/components/common/hint.test.js) | 0 | ||||
-rw-r--r-- | test/content/components/common/input.test.ts (renamed from test/content/components/common/input.test.js) | 14 | ||||
-rw-r--r-- | test/content/hint-key-producer.test.ts (renamed from test/content/hint-key-producer.test.js) | 0 | ||||
-rw-r--r-- | test/content/navigates.test.ts (renamed from test/content/navigates.test.js) | 0 | ||||
-rw-r--r-- | test/content/reducers/addon.test.ts (renamed from test/content/reducers/addon.test.js) | 2 | ||||
-rw-r--r-- | test/content/reducers/find.test.ts (renamed from test/content/reducers/find.test.js) | 2 | ||||
-rw-r--r-- | test/content/reducers/follow-controller.test.ts (renamed from test/content/reducers/follow-controller.test.js) | 2 | ||||
-rw-r--r-- | test/content/reducers/input.test.ts (renamed from test/content/reducers/input.test.js) | 2 | ||||
-rw-r--r-- | test/content/reducers/mark.test.ts (renamed from test/content/reducers/mark.test.js) | 2 | ||||
-rw-r--r-- | test/content/reducers/setting.test.js | 17 | ||||
-rw-r--r-- | test/content/reducers/setting.test.ts | 31 | ||||
-rw-r--r-- | test/main.ts (renamed from test/main.js) | 0 | ||||
-rw-r--r-- | test/settings/components/form/BlacklistForm.test.tsx (renamed from test/settings/components/form/BlacklistForm.test.jsx) | 0 | ||||
-rw-r--r-- | test/settings/components/form/KeymapsForm.test.tsx (renamed from test/settings/components/form/KeymapsForm.test.jsx) | 14 | ||||
-rw-r--r-- | test/settings/components/form/PropertiesForm.test.tsx (renamed from test/settings/components/form/PropertiesForm.test.jsx) | 0 | ||||
-rw-r--r-- | test/settings/components/form/SearchEngineForm.test.tsx (renamed from test/settings/components/form/SearchEngineForm.test.jsx) | 60 | ||||
-rw-r--r-- | test/settings/components/ui/input.test.tsx (renamed from test/settings/components/ui/input.test.jsx) | 0 | ||||
-rw-r--r-- | test/settings/reducers/setting.test.ts (renamed from test/settings/reducers/setting.test.js) | 5 | ||||
-rw-r--r-- | test/shared/SettingData.test.ts | 293 | ||||
-rw-r--r-- | test/shared/Settings.test.ts | 190 | ||||
-rw-r--r-- | test/shared/blacklists.test.ts (renamed from test/shared/blacklists.test.js) | 0 | ||||
-rw-r--r-- | test/shared/operations.test.ts | 41 | ||||
-rw-r--r-- | test/shared/properties.test.js | 18 | ||||
-rw-r--r-- | test/shared/property-defs.test.js | 18 | ||||
-rw-r--r-- | test/shared/settings/validator.test.js | 81 | ||||
-rw-r--r-- | test/shared/settings/values.test.js | 138 | ||||
-rw-r--r-- | test/shared/urls.test.ts (renamed from test/shared/urls.test.js) | 0 | ||||
-rw-r--r-- | test/shared/utils/keys.test.ts (renamed from test/shared/utils/keys.test.js) | 0 | ||||
-rw-r--r-- | test/shared/utils/re.test.ts (renamed from test/shared/utils/re.test.js) | 0 | ||||
-rw-r--r-- | tsconfig.json | 36 | ||||
-rw-r--r-- | webpack.config.js | 14 |
205 files changed, 5117 insertions, 2774 deletions
@@ -5,12 +5,17 @@ "browser" : true, "webextensions": true }, - "plugins": ["react"], - "parser": "babel-eslint", + "plugins": [ + "react", + "@typescript-eslint" + ], + "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaFeatures": { "jsx": true - } + }, + "sourceType": "module", + "project": "./tsconfig.json" }, "extends": [ "eslint:all", "plugin:react/recommended" ], "rules": { @@ -30,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", @@ -42,13 +48,14 @@ "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", "no-ternary": "off", "no-undefined": "off", "no-undef-init": "off", - "no-unused-vars": ["error", { "varsIgnorePattern": "h" }], + "no-unused-vars": "off", "no-use-before-define": "off", "no-warning-comments": "off", "object-curly-newline": ["error", { "consistent": true }], @@ -71,6 +78,7 @@ "react/jsx-indent": ["error", 2], "react/prop-types": "off", - "react/react-in-jsx-scope": "off" + "react/react-in-jsx-scope": "off", + "@typescript-eslint/no-unused-vars": "error" } } diff --git a/karma.conf.js b/karma.conf.js index 32da469..b422605 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -6,16 +6,16 @@ module.exports = function (config) { basePath: '', frameworks: ['mocha', 'sinon'], files: [ - 'test/main.js', - 'test/**/*.test.js', - 'test/**/*.test.jsx', + 'test/main.ts', + 'test/**/*.test.ts', + 'test/**/*.test.tsx', 'test/**/*.html' ], preprocessors: { - 'test/main.js': [ 'webpack', 'sourcemap' ], - 'test/**/*.test.js': [ 'webpack', 'sourcemap' ], - 'test/**/*.test.jsx': [ 'webpack', 'sourcemap' ], + 'test/main.ts': [ 'webpack', 'sourcemap' ], + 'test/**/*.test.ts': [ 'webpack', 'sourcemap' ], + 'test/**/*.test.tsx': [ 'webpack', 'sourcemap' ], 'test/**/*.html': ['html2js'] }, diff --git a/package-lock.json b/package-lock.json index cbc8dae..372fac9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -196,6 +196,42 @@ "esutils": "^2.0.0" } }, + "@babel/helper-create-class-features-plugin": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.4.4.tgz", + "integrity": "sha512-UbBHIa2qeAGgyiNR9RszVF7bUHEdgS4JAUNT8SiqrAN6YJVxlOxeLr5pBzb5kan302dejJ9nla4RyKcR1XT6XA==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-member-expression-to-functions": "^7.0.0", + "@babel/helper-optimise-call-expression": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.4.4", + "@babel/helper-split-export-declaration": "^7.4.4" + }, + "dependencies": { + "@babel/helper-split-export-declaration": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", + "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", + "dev": true, + "requires": { + "@babel/types": "^7.4.4" + } + }, + "@babel/types": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.4.tgz", + "integrity": "sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.11", + "to-fast-properties": "^2.0.0" + } + } + } + }, "@babel/helper-function-name": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", @@ -216,12 +252,115 @@ "@babel/types": "^7.0.0" } }, + "@babel/helper-member-expression-to-functions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.0.0.tgz", + "integrity": "sha512-avo+lm/QmZlv27Zsi0xEor2fKcqWG56D5ae9dzklpIaY7cQMK5N8VSpaNVPPagiqmy7LrEjK1IWdGMOqPu5csg==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz", + "integrity": "sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, "@babel/helper-plugin-utils": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==", "dev": true }, + "@babel/helper-replace-supers": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.4.4.tgz", + "integrity": "sha512-04xGEnd+s01nY1l15EuMS1rfKktNF+1CkKmHoErDppjAAZL+IUBZpzT748x262HF7fibaQPhbvWUl5HeSt1EXg==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.0.0", + "@babel/helper-optimise-call-expression": "^7.0.0", + "@babel/traverse": "^7.4.4", + "@babel/types": "^7.4.4" + }, + "dependencies": { + "@babel/generator": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.4.4.tgz", + "integrity": "sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ==", + "dev": true, + "requires": { + "@babel/types": "^7.4.4", + "jsesc": "^2.5.1", + "lodash": "^4.17.11", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", + "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", + "dev": true, + "requires": { + "@babel/types": "^7.4.4" + } + }, + "@babel/parser": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.4.tgz", + "integrity": "sha512-5pCS4mOsL+ANsFZGdvNLybx4wtqAZJ0MJjMHxvzI3bvIsz6sQvzW8XX92EYIkiPtIvcfG3Aj+Ir5VNyjnZhP7w==", + "dev": true + }, + "@babel/traverse": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.4.tgz", + "integrity": "sha512-Gw6qqkw/e6AGzlyj9KnkabJX7VcubqPtkUQVAwkc0wUMldr3A/hezNB3Rc5eIvId95iSGkGIOe5hh1kMKf951A==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/generator": "^7.4.4", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.4.4", + "@babel/parser": "^7.4.4", + "@babel/types": "^7.4.4", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.11" + } + }, + "@babel/types": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.4.tgz", + "integrity": "sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.11", + "to-fast-properties": "^2.0.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, "@babel/helper-split-export-declaration": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz", @@ -386,6 +525,16 @@ "integrity": "sha512-ATz6yX/L8LEnC3dtLQnIx4ydcPxhLcoy9Vl6re00zb2w5lG6itY6Vhnr1KFRPq/FHNsgl/gh2mjNN20f9iJTTA==", "dev": true }, + "@babel/plugin-proposal-class-properties": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.4.4.tgz", + "integrity": "sha512-WjKTI8g8d5w1Bc9zgwSz2nfrsNQsXcCf9J9cdCvrJV6RF56yztwm4TmJC0MgJ9tvwO9gUA/mcYe89bLdGfiXFg==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.4.4", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, "@babel/plugin-syntax-jsx": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.2.0.tgz", @@ -395,6 +544,15 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, + "@babel/plugin-syntax-typescript": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.3.3.tgz", + "integrity": "sha512-dGwbSMA1YhVS8+31CnPR7LB4pcbrzcV99wQzby4uAfrkZPYZlQ7ImwdpzLqi6Z6IL02b8IAL379CaMwo0x5Lag==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, "@babel/plugin-transform-react-display-name": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.2.0.tgz", @@ -435,6 +593,16 @@ "@babel/plugin-syntax-jsx": "^7.2.0" } }, + "@babel/plugin-transform-typescript": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.4.4.tgz", + "integrity": "sha512-rwDvjaMTx09WC0rXGBRlYSSkEHOKRrecY6hEr3SVIPKII8DVWXtapNAfAyMC0dovuO+zYArcAuKeu3q9DNRfzA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-typescript": "^7.2.0" + } + }, "@babel/preset-react": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.0.0.tgz", @@ -448,6 +616,16 @@ "@babel/plugin-transform-react-jsx-source": "^7.0.0" } }, + "@babel/preset-typescript": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.3.3.tgz", + "integrity": "sha512-mzMVuIP4lqtn4du2ynEfdO0+RYcslwrZiJHXu4MGaC1ctJiW2fyaeDrtjJGs7R/KebZ1sgowcIoWf4uRpEfKEg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-transform-typescript": "^7.3.2" + } + }, "@babel/runtime": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.4.tgz", @@ -579,6 +757,142 @@ "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, + "@types/chai": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", + "integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==", + "dev": true + }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "dev": true, + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, + "@types/mocha": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.6.tgz", + "integrity": "sha512-1axi39YdtBI7z957vdqXI4Ac25e7YihYQtJa+Clnxg1zTJEaIRbndt71O3sP4GAMgiAm0pY26/b9BrY4MR/PMw==", + "dev": true + }, + "@types/prop-types": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.1.tgz", + "integrity": "sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg==", + "dev": true + }, + "@types/react": { + "version": "16.8.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.8.15.tgz", + "integrity": "sha512-dMhzw1rWK+wwJWvPp5Pk12ksSrm/z/C/+lOQbMZ7YfDQYnJ02bc0wtg4EJD9qrFhuxFrf/ywNgwTboucobJqQg==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "csstype": "^2.2.0" + } + }, + "@types/react-dom": { + "version": "16.8.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.8.4.tgz", + "integrity": "sha512-eIRpEW73DCzPIMaNBDP5pPIpK1KXyZwNgfxiVagb5iGiz6da+9A5hslSX6GAQKdO7SayVCS/Fr2kjqprgAvkfA==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/react-redux": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.0.8.tgz", + "integrity": "sha512-vIBC15E84ehN6RzdGwRVa41whp9e4CkfPm+WfD0r6y6vqBf4tQPKZeKEBfLLM8k79uSwQC7rh3rH/MFaN1IESQ==", + "dev": true, + "requires": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "redux": "^4.0.0" + } + }, + "@types/redux-promise": { + "version": "0.5.28", + "resolved": "https://registry.npmjs.org/@types/redux-promise/-/redux-promise-0.5.28.tgz", + "integrity": "sha512-HNWAIjTeMcdAgl4wI2XQdlrJeJMS/TyohD8Yzf3Ebp0fPR4M9wg4/+EBrbbLAsBrGxmSkXdvy1H2ty21dhlS7A==", + "dev": true, + "requires": { + "redux": "^3.6.0" + }, + "dependencies": { + "redux": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", + "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==", + "dev": true, + "requires": { + "lodash": "^4.2.1", + "lodash-es": "^4.2.1", + "loose-envify": "^1.1.0", + "symbol-observable": "^1.0.3" + } + } + } + }, + "@typescript-eslint/eslint-plugin": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.7.0.tgz", + "integrity": "sha512-NUSz1aTlIzzTjFFVFyzrbo8oFjHg3K/M9MzYByqbMCxeFdErhLAcGITVfXzSz+Yvp5OOpMu3HkIttB0NyKl54Q==", + "dev": true, + "requires": { + "@typescript-eslint/parser": "1.7.0", + "@typescript-eslint/typescript-estree": "1.7.0", + "eslint-utils": "^1.3.1", + "regexpp": "^2.0.1", + "requireindex": "^1.2.0", + "tsutils": "^3.7.0" + } + }, + "@typescript-eslint/parser": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.7.0.tgz", + "integrity": "sha512-1QFKxs2V940372srm12ovSE683afqc1jB6zF/f8iKhgLz1yoSjYeGHipasao33VXKI+0a/ob9okeogGdKGvvlg==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "1.7.0", + "eslint-scope": "^4.0.0", + "eslint-visitor-keys": "^1.0.0" + }, + "dependencies": { + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + } + } + }, + "@typescript-eslint/typescript-estree": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.7.0.tgz", + "integrity": "sha512-K5uedUxVmlYrVkFbyV3htDipvLqTE3QMOUQEHYJaKtgzxj6r7c5Ca/DG1tGgFxX+fsbi9nDIrf4arq7Ib7H/Yw==", + "dev": true, + "requires": { + "lodash.unescape": "4.0.1", + "semver": "5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + } + } + }, "@webassemblyjs/ast": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", @@ -2255,6 +2569,12 @@ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true }, + "csstype": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.4.tgz", + "integrity": "sha512-lAJUJP3M6HxFXbqtGRc0iZrdyeN+WzOWeY0q/VnFzI+kqVrYIzC7bWlKqCW7oCIdzoPkvfp82EVvrTlQ8zsWQg==", + "dev": true + }, "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", @@ -3948,14 +4268,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3975,8 +4293,7 @@ "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", @@ -4124,7 +4441,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6077,6 +6393,12 @@ } } }, + "karma-babel-preprocessor": { + "version": "8.0.0-beta.0", + "resolved": "https://registry.npmjs.org/karma-babel-preprocessor/-/karma-babel-preprocessor-8.0.0-beta.0.tgz", + "integrity": "sha512-nv3GbDAKdonWuTJc+Kg4jEdRXzoP7uKKQ6HfTJb5PNTY+OJYKzrtUBUSez/wrutUFtztVT+MQxJHamd7MNCmBQ==", + "dev": true + }, "karma-firefox-launcher": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-1.1.0.tgz", @@ -6408,12 +6730,24 @@ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", "dev": true }, + "lodash-es": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.11.tgz", + "integrity": "sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q==", + "dev": true + }, "lodash.tail": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz", "integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=", "dev": true }, + "lodash.unescape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", + "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=", + "dev": true + }, "log-symbols": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", @@ -8901,6 +9235,12 @@ "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", "dev": true }, + "requireindex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", + "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", + "dev": true + }, "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -10326,6 +10666,15 @@ "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", "dev": true }, + "tsutils": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.10.0.tgz", + "integrity": "sha512-q20XSMq7jutbGB8luhKKsQldRKWvyBO2BGqni3p4yq8Ys9bEP/xQw3KepKmMRt9gJ4lvQSScrihJrcKdKoSU7Q==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "tty-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", @@ -10378,6 +10727,12 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "typescript": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz", + "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==", + "dev": true + }, "uglify-js": { "version": "3.3.25", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.3.25.tgz", @@ -11100,6 +11455,12 @@ } } }, + "web-ext-types": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/web-ext-types/-/web-ext-types-3.1.0.tgz", + "integrity": "sha512-HKVibk040vuhpbOljcIYYYC8GP9w9REbHpquI3im/aoZqoDIRq9DnsHl4Zsg+4Fg3SBnWsnvlIr1rnspV4TdXQ==", + "dev": true + }, "webextensions-api-fake": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/webextensions-api-fake/-/webextensions-api-fake-0.7.4.tgz", diff --git a/package.json b/package.json index 98cdc60..a799554 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "start": "webpack --mode development -w --debug --devtool inline-source-map", "build": "NODE_ENV=production webpack --mode production --progress --display-error-details", "package": "npm run build && script/package", - "lint": "eslint --ext .jsx,.js src", + "lint": "eslint --ext .js,.jsx,.ts,.tsx src", + "type-checks": "tsc", "test": "karma start", "test:e2e": "mocha --timeout 8000 e2e" }, @@ -22,7 +23,17 @@ "devDependencies": { "@babel/cli": "^7.4.4", "@babel/core": "^7.4.4", + "@babel/plugin-proposal-class-properties": "^7.4.4", "@babel/preset-react": "^7.0.0", + "@babel/preset-typescript": "^7.3.3", + "@types/chai": "^4.1.7", + "@types/mocha": "^5.2.6", + "@types/prop-types": "^15.7.1", + "@types/react": "^16.8.15", + "@types/react-dom": "^16.8.4", + "@types/react-redux": "^7.0.8", + "@types/redux-promise": "^0.5.28", + "@typescript-eslint/eslint-plugin": "^1.7.0", "babel-eslint": "^10.0.1", "babel-loader": "^8.0.5", "chai": "^4.2.0", @@ -32,6 +43,7 @@ "html-webpack-plugin": "^3.2.0", "jszip": "^3.2.1", "karma": "^4.1.0", + "karma-babel-preprocessor": "^8.0.0-beta.0", "karma-firefox-launcher": "^1.1.0", "karma-html2js-preprocessor": "^1.1.0", "karma-mocha": "^1.3.0", @@ -51,6 +63,8 @@ "sass-loader": "^7.1.0", "sinon-chrome": "^3.0.1", "style-loader": "^0.23.1", + "typescript": "^3.4.5", + "web-ext-types": "^3.1.0", "webextensions-api-fake": "^0.7.4", "webpack": "^4.30.0", "webpack-cli": "^3.3.1" diff --git a/src/background/controllers/AddonEnabledController.js b/src/background/controllers/AddonEnabledController.ts index 9a3a521..251af25 100644 --- a/src/background/controllers/AddonEnabledController.js +++ b/src/background/controllers/AddonEnabledController.ts @@ -1,11 +1,13 @@ import AddonEnabledUseCase from '../usecases/AddonEnabledUseCase'; export default class AddonEnabledController { + private addonEnabledUseCase: AddonEnabledUseCase; + constructor() { this.addonEnabledUseCase = new AddonEnabledUseCase(); } - indicate(enabled) { + indicate(enabled: boolean): Promise<any> { return this.addonEnabledUseCase.indicate(enabled); } } diff --git a/src/background/controllers/CommandController.js b/src/background/controllers/CommandController.ts index b113709..f3a6b7f 100644 --- a/src/background/controllers/CommandController.js +++ b/src/background/controllers/CommandController.ts @@ -1,19 +1,23 @@ import CompletionsUseCase from '../usecases/CompletionsUseCase'; import CommandUseCase from '../usecases/CommandUseCase'; -import Completions from '../domains/Completions'; +import CompletionGroup from '../domains/CompletionGroup'; -const trimStart = (str) => { +const trimStart = (str: string): string => { // NOTE String.trimStart is available on Firefox 61 return str.replace(/^\s+/, ''); }; export default class CommandController { + private completionsUseCase: CompletionsUseCase; + + private commandIndicator: CommandUseCase; + constructor() { this.completionsUseCase = new CompletionsUseCase(); this.commandIndicator = new CommandUseCase(); } - getCompletions(line) { + getCompletions(line: string): Promise<CompletionGroup[]> { let trimmed = trimStart(line); let words = trimmed.split(/ +/); let name = words[0]; @@ -45,11 +49,11 @@ export default class CommandController { case 'set': return this.completionsUseCase.querySet(name, keywords); } - return Promise.resolve(Completions.empty()); + return Promise.resolve([]); } // eslint-disable-next-line complexity - exec(line) { + exec(line: string): Promise<any> { let trimmed = trimStart(line); let words = trimmed.split(/ +/); let name = words[0]; diff --git a/src/background/controllers/FindController.js b/src/background/controllers/FindController.ts index caeff98..28959e2 100644 --- a/src/background/controllers/FindController.js +++ b/src/background/controllers/FindController.ts @@ -1,15 +1,17 @@ import FindUseCase from '../usecases/FindUseCase'; export default class FindController { + private findUseCase: FindUseCase; + constructor() { this.findUseCase = new FindUseCase(); } - getKeyword() { + getKeyword(): Promise<string> { return this.findUseCase.getKeyword(); } - setKeyword(keyword) { + setKeyword(keyword: string): Promise<any> { return this.findUseCase.setKeyword(keyword); } } diff --git a/src/background/controllers/LinkController.js b/src/background/controllers/LinkController.js deleted file mode 100644 index 7e395b1..0000000 --- a/src/background/controllers/LinkController.js +++ /dev/null @@ -1,15 +0,0 @@ -import LinkUseCase from '../usecases/LinkUseCase'; - -export default class LinkController { - constructor() { - this.linkUseCase = new LinkUseCase(); - } - - openToTab(url, tabId) { - this.linkUseCase.openToTab(url, tabId); - } - - openNewTab(url, openerId, background) { - this.linkUseCase.openNewTab(url, openerId, background); - } -} diff --git a/src/background/controllers/LinkController.ts b/src/background/controllers/LinkController.ts new file mode 100644 index 0000000..707b28a --- /dev/null +++ b/src/background/controllers/LinkController.ts @@ -0,0 +1,19 @@ +import LinkUseCase from '../usecases/LinkUseCase'; + +export default class LinkController { + private linkUseCase: LinkUseCase; + + constructor() { + this.linkUseCase = new LinkUseCase(); + } + + openToTab(url: string, tabId: number): Promise<void> { + return this.linkUseCase.openToTab(url, tabId); + } + + openNewTab( + url: string, openerId: number, background: boolean, + ): Promise<void> { + return this.linkUseCase.openNewTab(url, openerId, background); + } +} diff --git a/src/background/controllers/MarkController.js b/src/background/controllers/MarkController.js deleted file mode 100644 index 0478369..0000000 --- a/src/background/controllers/MarkController.js +++ /dev/null @@ -1,15 +0,0 @@ -import MarkUseCase from '../usecases/MarkUseCase'; - -export default class MarkController { - constructor() { - this.markUseCase = new MarkUseCase(); - } - - setGlobal(key, x, y) { - this.markUseCase.setGlobal(key, x, y); - } - - jumpGlobal(key) { - this.markUseCase.jumpGlobal(key); - } -} diff --git a/src/background/controllers/MarkController.ts b/src/background/controllers/MarkController.ts new file mode 100644 index 0000000..419a08b --- /dev/null +++ b/src/background/controllers/MarkController.ts @@ -0,0 +1,17 @@ +import MarkUseCase from '../usecases/MarkUseCase'; + +export default class MarkController { + private markUseCase: MarkUseCase; + + constructor() { + this.markUseCase = new MarkUseCase(); + } + + setGlobal(key: string, x: number, y: number): Promise<any> { + return this.markUseCase.setGlobal(key, x, y); + } + + jumpGlobal(key: string): Promise<any> { + return this.markUseCase.jumpGlobal(key); + } +} diff --git a/src/background/controllers/OperationController.js b/src/background/controllers/OperationController.ts index 416aa9c..fa09512 100644 --- a/src/background/controllers/OperationController.js +++ 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'; @@ -6,6 +6,16 @@ import TabSelectUseCase from '../usecases/TabSelectUseCase'; import ZoomUseCase from '../usecases/ZoomUseCase'; export default class OperationController { + private findUseCase: FindUseCase; + + private consoleUseCase: ConsoleUseCase; + + private tabUseCase: TabUseCase; + + private tabSelectUseCase: TabSelectUseCase; + + private zoomUseCase: ZoomUseCase; + constructor() { this.findUseCase = new FindUseCase(); this.consoleUseCase = new ConsoleUseCase(); @@ -15,7 +25,7 @@ export default class OperationController { } // eslint-disable-next-line complexity, max-lines-per-function - exec(operation) { + exec(operation: operations.Operation): Promise<any> { switch (operation.type) { case operations.TAB_CLOSE: return this.tabUseCase.close(false); @@ -72,6 +82,7 @@ export default class OperationController { case operations.CANCEL: return this.consoleUseCase.hideConsole(); } + throw new Error('unknown operation: ' + operation.type); } } diff --git a/src/background/controllers/SettingController.js b/src/background/controllers/SettingController.ts index e895d72..dfd2817 100644 --- a/src/background/controllers/SettingController.js +++ b/src/background/controllers/SettingController.ts @@ -1,17 +1,22 @@ import SettingUseCase from '../usecases/SettingUseCase'; import ContentMessageClient from '../infrastructures/ContentMessageClient'; +import Settings from '../../shared/Settings'; export default class SettingController { + private settingUseCase: SettingUseCase; + + private contentMessageClient: ContentMessageClient; + constructor() { this.settingUseCase = new SettingUseCase(); this.contentMessageClient = new ContentMessageClient(); } - getSetting() { + getSetting(): Promise<Settings> { return this.settingUseCase.get(); } - async reload() { + async reload(): Promise<any> { await this.settingUseCase.reload(); this.contentMessageClient.broadcastSettingsChanged(); } diff --git a/src/background/controllers/VersionController.js b/src/background/controllers/VersionController.ts index c596f9b..2e2a197 100644 --- a/src/background/controllers/VersionController.js +++ b/src/background/controllers/VersionController.ts @@ -1,11 +1,13 @@ import VersionUseCase from '../usecases/VersionUseCase'; export default class VersionController { + private versionUseCase: VersionUseCase; + constructor() { this.versionUseCase = new VersionUseCase(); } - notify() { - this.versionUseCase.notify(); + notify(): Promise<void> { + return this.versionUseCase.notify(); } } diff --git a/src/background/controllers/version.js b/src/background/controllers/version.js deleted file mode 100644 index ec0f634..0000000 --- a/src/background/controllers/version.js +++ /dev/null @@ -1,13 +0,0 @@ -import VersionInteractor from '../usecases/version'; - -export default class VersionController { - constructor() { - this.versionInteractor = new VersionInteractor(); - } - - notifyIfUpdated() { - browser.runtime.onInstalled.addListener(() => { - return this.versionInteractor.notify(); - }); - } -} diff --git a/src/background/domains/CommandDocs.js b/src/background/domains/CommandDocs.ts index 734c68e..25ea62a 100644 --- a/src/background/domains/CommandDocs.js +++ b/src/background/domains/CommandDocs.ts @@ -8,5 +8,4 @@ export default { bdeletes: 'Close all tabs matched by keywords', quit: 'Close the current tab', quitall: 'Close all tabs', -}; - +} as {[key: string]: string}; diff --git a/src/background/domains/CompletionGroup.js b/src/background/domains/CompletionGroup.js deleted file mode 100644 index 1749d72..0000000 --- a/src/background/domains/CompletionGroup.js +++ /dev/null @@ -1,14 +0,0 @@ -export default class CompletionGroup { - constructor(name, items) { - this.name0 = name; - this.items0 = items; - } - - get name() { - return this.name0; - } - - get items() { - return this.items0; - } -} diff --git a/src/background/domains/CompletionGroup.ts b/src/background/domains/CompletionGroup.ts new file mode 100644 index 0000000..1eea7d8 --- /dev/null +++ b/src/background/domains/CompletionGroup.ts @@ -0,0 +1,7 @@ +import CompletionItem from './CompletionItem'; + +export default interface CompletionGroup { + name: string; + items: CompletionItem[]; + // eslint-disable-next-line semi +} diff --git a/src/background/domains/CompletionItem.js b/src/background/domains/CompletionItem.js deleted file mode 100644 index c7ad8a1..0000000 --- a/src/background/domains/CompletionItem.js +++ /dev/null @@ -1,24 +0,0 @@ -export default class CompletionItem { - constructor({ caption, content, url, icon }) { - this.caption0 = caption; - this.content0 = content; - this.url0 = url; - this.icon0 = icon; - } - - get caption() { - return this.caption0; - } - - get content() { - return this.content0; - } - - get url() { - return this.url0; - } - - get icon() { - return this.icon0; - } -} diff --git a/src/background/domains/CompletionItem.ts b/src/background/domains/CompletionItem.ts new file mode 100644 index 0000000..657efaa --- /dev/null +++ b/src/background/domains/CompletionItem.ts @@ -0,0 +1,7 @@ +export default interface CompletionItem { + readonly caption?: string; + readonly content?: string; + readonly url?: string; + readonly icon?: string; + // eslint-disable-next-line semi +} diff --git a/src/background/domains/Completions.js b/src/background/domains/Completions.js deleted file mode 100644 index f399743..0000000 --- a/src/background/domains/Completions.js +++ /dev/null @@ -1,27 +0,0 @@ -export default class Completions { - constructor(groups) { - this.g = groups; - } - - get groups() { - return this.g; - } - - serialize() { - return this.groups.map(group => ({ - name: group.name, - items: group.items.map(item => ({ - caption: item.caption, - content: item.content, - url: item.url, - icon: item.icon, - })), - })); - } - - static empty() { - return EMPTY_COMPLETIONS; - } -} - -let EMPTY_COMPLETIONS = new Completions([]); diff --git a/src/background/domains/GlobalMark.js b/src/background/domains/GlobalMark.js deleted file mode 100644 index f0586f1..0000000 --- a/src/background/domains/GlobalMark.js +++ /dev/null @@ -1,24 +0,0 @@ -export default class GlobalMark { - constructor(tabId, url, x, y) { - this.tabId0 = tabId; - this.url0 = url; - this.x0 = x; - this.y0 = y; - } - - get tabId() { - return this.tabId0; - } - - get url() { - return this.url0; - } - - get x() { - return this.x0; - } - - get y() { - return this.y0; - } -} diff --git a/src/background/domains/GlobalMark.ts b/src/background/domains/GlobalMark.ts new file mode 100644 index 0000000..1ae912e --- /dev/null +++ b/src/background/domains/GlobalMark.ts @@ -0,0 +1,7 @@ +export default interface GlobalMark { + readonly tabId: number; + readonly url: string; + readonly x: number; + readonly y: number; + // eslint-disable-next-line semi +} diff --git a/src/background/domains/Setting.js b/src/background/domains/Setting.js deleted file mode 100644 index 106ec0f..0000000 --- a/src/background/domains/Setting.js +++ /dev/null @@ -1,51 +0,0 @@ -import DefaultSettings from '../../shared/settings/default'; -import * as settingsValues from '../../shared/settings/values'; - -export default class Setting { - constructor({ source, json, form }) { - this.obj = { - source, json, form - }; - } - - get source() { - return this.obj.source; - } - - get json() { - return this.obj.json; - } - - get form() { - return this.obj.form; - } - - value() { - let value = JSON.parse(DefaultSettings.json); - if (this.obj.source === 'json') { - value = settingsValues.valueFromJson(this.obj.json); - } else if (this.obj.source === 'form') { - value = settingsValues.valueFromForm(this.obj.form); - } - if (!value.properties) { - value.properties = {}; - } - return { ...settingsValues.valueFromJson(DefaultSettings.json), ...value }; - } - - serialize() { - return this.obj; - } - - static deserialize(obj) { - return new Setting({ source: obj.source, json: obj.json, form: obj.form }); - } - - static defaultSettings() { - return new Setting({ - source: DefaultSettings.source, - json: DefaultSettings.json, - form: {}, - }); - } -} diff --git a/src/background/index.js b/src/background/index.ts index f9efd4d..f9efd4d 100644 --- a/src/background/index.js +++ b/src/background/index.ts diff --git a/src/background/infrastructures/ConsoleClient.js b/src/background/infrastructures/ConsoleClient.ts index f691515..c162634 100644 --- a/src/background/infrastructures/ConsoleClient.js +++ b/src/background/infrastructures/ConsoleClient.ts @@ -1,34 +1,34 @@ -import messages from '../../shared/messages'; +import * as messages from '../../shared/messages'; export default class ConsoleClient { - showCommand(tabId, command) { + showCommand(tabId: number, command: string): Promise<any> { return browser.tabs.sendMessage(tabId, { type: messages.CONSOLE_SHOW_COMMAND, command, }); } - showFind(tabId) { + showFind(tabId: number): Promise<any> { return browser.tabs.sendMessage(tabId, { type: messages.CONSOLE_SHOW_FIND }); } - showInfo(tabId, message) { + showInfo(tabId: number, message: string): Promise<any> { return browser.tabs.sendMessage(tabId, { type: messages.CONSOLE_SHOW_INFO, text: message, }); } - showError(tabId, message) { + showError(tabId: number, message: string): Promise<any> { return browser.tabs.sendMessage(tabId, { type: messages.CONSOLE_SHOW_ERROR, text: message, }); } - hide(tabId) { + hide(tabId: number): Promise<any> { return browser.tabs.sendMessage(tabId, { type: messages.CONSOLE_HIDE, }); diff --git a/src/background/infrastructures/ContentMessageClient.js b/src/background/infrastructures/ContentMessageClient.ts index 0fab5a3..d4bc476 100644 --- a/src/background/infrastructures/ContentMessageClient.js +++ b/src/background/infrastructures/ContentMessageClient.ts @@ -1,10 +1,10 @@ -import messages from '../../shared/messages'; +import * as messages from '../../shared/messages'; export default class ContentMessageClient { - async broadcastSettingsChanged() { + async broadcastSettingsChanged(): Promise<void> { let tabs = await browser.tabs.query({}); for (let tab of tabs) { - if (tab.url.startsWith('about:')) { + if (!tab.id || tab.url && tab.url.startsWith('about:')) { continue; } browser.tabs.sendMessage(tab.id, { @@ -13,20 +13,20 @@ export default class ContentMessageClient { } } - async getAddonEnabled(tabId) { + async getAddonEnabled(tabId: number): Promise<boolean> { let { enabled } = await browser.tabs.sendMessage(tabId, { type: messages.ADDON_ENABLED_QUERY, - }); + }) as { enabled: boolean }; return enabled; } - toggleAddonEnabled(tabId) { + toggleAddonEnabled(tabId: number): Promise<void> { return browser.tabs.sendMessage(tabId, { type: messages.ADDON_TOGGLE_ENABLED, }); } - scrollTo(tabId, x, y) { + scrollTo(tabId: number, x: number, y: number): Promise<void> { return browser.tabs.sendMessage(tabId, { type: messages.TAB_SCROLL_TO, x, diff --git a/src/background/infrastructures/ContentMessageListener.js b/src/background/infrastructures/ContentMessageListener.ts index 5b0f62e..1cc2696 100644 --- a/src/background/infrastructures/ContentMessageListener.js +++ b/src/background/infrastructures/ContentMessageListener.ts @@ -1,4 +1,5 @@ -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'; import FindController from '../controllers/FindController'; @@ -8,6 +9,22 @@ import OperationController from '../controllers/OperationController'; import MarkController from '../controllers/MarkController'; export default class ContentMessageListener { + private settingController: SettingController; + + private commandController: CommandController; + + private findController: FindController; + + private addonEnabledController: AddonEnabledController; + + private linkController: LinkController; + + private backgroundOperationController: OperationController; + + private markController: MarkController; + + private consolePorts: {[tabId: number]: browser.runtime.Port}; + constructor() { this.settingController = new SettingController(); this.commandController = new CommandController(); @@ -20,20 +37,28 @@ export default class ContentMessageListener { this.consolePorts = {}; } - run() { - browser.runtime.onMessage.addListener((message, sender) => { + run(): void { + browser.runtime.onMessage.addListener(( + message: any, sender: browser.runtime.MessageSender, + ) => { try { - let ret = this.onMessage(message, sender); + let ret = this.onMessage(message, sender.tab as browser.tabs.Tab); if (!(ret instanceof Promise)) { return {}; } return ret.catch((e) => { + if (!sender.tab || !sender.tab.id) { + return; + } return browser.tabs.sendMessage(sender.tab.id, { type: messages.CONSOLE_SHOW_ERROR, text: e.message, }); }); } catch (e) { + if (!sender.tab || !sender.tab.id) { + return; + } return browser.tabs.sendMessage(sender.tab.id, { type: messages.CONSOLE_SHOW_ERROR, text: e.message, @@ -43,7 +68,9 @@ export default class ContentMessageListener { browser.runtime.onConnect.addListener(this.onConnected.bind(this)); } - onMessage(message, sender) { + onMessage( + message: messages.Message, senderTab: browser.tabs.Tab, + ): Promise<any> | any { switch (message.type) { case messages.CONSOLE_QUERY_COMPLETIONS: return this.onConsoleQueryCompletions(message.text); @@ -59,7 +86,10 @@ export default class ContentMessageListener { return this.onAddonEnabledResponse(message.enabled); case messages.OPEN_URL: return this.onOpenUrl( - message.newTab, message.url, sender.tab.id, message.background); + message.newTab, + message.url, + senderTab.id as number, + message.background); case messages.BACKGROUND_OPERATION: return this.onBackgroundOperation(message.operation); case messages.MARK_SET_GLOBAL: @@ -67,56 +97,60 @@ export default class ContentMessageListener { case messages.MARK_JUMP_GLOBAL: return this.onMarkJumpGlobal(message.key); case messages.CONSOLE_FRAME_MESSAGE: - return this.onConsoleFrameMessage(sender.tab.id, message.message); + return this.onConsoleFrameMessage( + senderTab.id as number, message.message, + ); } + throw new Error('unsupported message: ' + message.type); } - async onConsoleQueryCompletions(line) { + async onConsoleQueryCompletions(line: string): Promise<CompletionGroup[]> { let completions = await this.commandController.getCompletions(line); - return Promise.resolve(completions.serialize()); + return Promise.resolve(completions); } - onConsoleEnterCommand(text) { + onConsoleEnterCommand(text: string): Promise<any> { return this.commandController.exec(text); } - - onSettingsQuery() { + onSettingsQuery(): Promise<any> { return this.settingController.getSetting(); } - onFindGetKeyword() { + onFindGetKeyword(): Promise<string> { return this.findController.getKeyword(); } - onFindSetKeyword(keyword) { + onFindSetKeyword(keyword: string): Promise<any> { return this.findController.setKeyword(keyword); } - onAddonEnabledResponse(enabled) { + onAddonEnabledResponse(enabled: boolean): Promise<any> { return this.addonEnabledController.indicate(enabled); } - onOpenUrl(newTab, url, openerId, background) { + onOpenUrl( + newTab: boolean, url: string, openerId: number, background: boolean, + ): Promise<any> { if (newTab) { return this.linkController.openNewTab(url, openerId, background); } return this.linkController.openToTab(url, openerId); } - onBackgroundOperation(operation) { + onBackgroundOperation(operation: any): Promise<any> { return this.backgroundOperationController.exec(operation); } - onMarkSetGlobal(key, x, y) { + onMarkSetGlobal(key: string, x: number, y: number): Promise<any> { return this.markController.setGlobal(key, x, y); } - onMarkJumpGlobal(key) { + onMarkJumpGlobal(key: string): Promise<any> { return this.markController.jumpGlobal(key); } - onConsoleFrameMessage(tabId, message) { + onConsoleFrameMessage(tabId: number, message: any): void { let port = this.consolePorts[tabId]; if (!port) { return; @@ -124,12 +158,14 @@ export default class ContentMessageListener { port.postMessage(message); } - onConnected(port) { + onConnected(port: browser.runtime.Port): void { if (port.name !== 'vimvixen-console') { return; } - let id = port.sender.tab.id; - this.consolePorts[id] = port; + if (port.sender && port.sender.tab && port.sender.tab.id) { + let id = port.sender.tab.id; + this.consolePorts[id] = port; + } } } diff --git a/src/background/infrastructures/MemoryStorage.js b/src/background/infrastructures/MemoryStorage.ts index 3a7e4f2..baf9ffa 100644 --- a/src/background/infrastructures/MemoryStorage.js +++ b/src/background/infrastructures/MemoryStorage.ts @@ -1,7 +1,7 @@ -const db = {}; +const db: {[key: string]: any} = {}; export default class MemoryStorage { - set(name, value) { + set(name: string, value: any): void { let data = JSON.stringify(value); if (typeof data === 'undefined') { throw new Error('value is not serializable'); @@ -9,7 +9,7 @@ export default class MemoryStorage { db[name] = data; } - get(name) { + get(name: string): any { let data = db[name]; if (!data) { return undefined; diff --git a/src/background/presenters/IndicatorPresenter.js b/src/background/presenters/IndicatorPresenter.ts index 5737519..d9a615a 100644 --- a/src/background/presenters/IndicatorPresenter.js +++ b/src/background/presenters/IndicatorPresenter.ts @@ -1,12 +1,12 @@ export default class IndicatorPresenter { - indicate(enabled) { + indicate(enabled: boolean): Promise<void> { let path = enabled ? 'resources/enabled_32x32.png' : 'resources/disabled_32x32.png'; return browser.browserAction.setIcon({ path }); } - onClick(listener) { + onClick(listener: (arg: browser.tabs.Tab) => void): void { browser.browserAction.onClicked.addListener(listener); } } diff --git a/src/background/presenters/NotifyPresenter.js b/src/background/presenters/NotifyPresenter.ts index a81f227..23932f7 100644 --- a/src/background/presenters/NotifyPresenter.js +++ b/src/background/presenters/NotifyPresenter.ts @@ -1,8 +1,12 @@ const NOTIFICATION_ID = 'vimvixen-update'; export default class NotifyPresenter { - notify(title, message, onclick) { - const listener = (id) => { + async notify( + title: string, + message: string, + onclick: () => void, + ): Promise<void> { + const listener = (id: string) => { if (id !== NOTIFICATION_ID) { return; } @@ -13,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/presenters/TabPresenter.js b/src/background/presenters/TabPresenter.ts index 744be39..33c6513 100644 --- a/src/background/presenters/TabPresenter.js +++ b/src/background/presenters/TabPresenter.ts @@ -3,27 +3,29 @@ import MemoryStorage from '../infrastructures/MemoryStorage'; const CURRENT_SELECTED_KEY = 'tabs.current.selected'; const LAST_SELECTED_KEY = 'tabs.last.selected'; +type Tab = browser.tabs.Tab; + export default class TabPresenter { - open(url, tabId) { + open(url: string, tabId?: number): Promise<Tab> { return browser.tabs.update(tabId, { url }); } - create(url, opts) { + create(url: string, opts?: object): Promise<Tab> { return browser.tabs.create({ url, ...opts }); } - async getCurrent() { + async getCurrent(): Promise<Tab> { let tabs = await browser.tabs.query({ active: true, currentWindow: true }); return tabs[0]; } - getAll() { + getAll(): Promise<Tab[]> { return browser.tabs.query({ currentWindow: true }); } - async getLastSelectedId() { + async getLastSelectedId(): Promise<number | undefined> { let cache = new MemoryStorage(); let tabId = await cache.get(LAST_SELECTED_KEY); if (tabId === null || typeof tabId === 'undefined') { @@ -32,25 +34,25 @@ export default class TabPresenter { return tabId; } - async getByKeyword(keyword, excludePinned = false) { + async getByKeyword(keyword: string, excludePinned = false): Promise<Tab[]> { let tabs = await browser.tabs.query({ currentWindow: true }); return tabs.filter((t) => { - return t.url.toLowerCase().includes(keyword.toLowerCase()) || + return t.url && t.url.toLowerCase().includes(keyword.toLowerCase()) || t.title && t.title.toLowerCase().includes(keyword.toLowerCase()); }).filter((t) => { return !(excludePinned && t.pinned); }); } - select(tabId) { + select(tabId: number): Promise<Tab> { return browser.tabs.update(tabId, { active: true }); } - remove(ids) { + remove(ids: number[]): Promise<void> { return browser.tabs.remove(ids); } - async reopen() { + async reopen(): Promise<any> { let window = await browser.windows.getCurrent(); let sessions = await browser.sessions.getRecentlyClosed(); let session = sessions.find((s) => { @@ -59,39 +61,43 @@ export default class TabPresenter { if (!session) { return; } - if (session.tab) { + if (session.tab && session.tab.sessionId) { return browser.sessions.restore(session.tab.sessionId); } - return browser.sessions.restore(session.window.sessionId); + if (session.window && session.window.sessionId) { + return browser.sessions.restore(session.window.sessionId); + } } - reload(tabId, cache) { + reload(tabId: number, cache: boolean): Promise<void> { return browser.tabs.reload(tabId, { bypassCache: cache }); } - setPinned(tabId, pinned) { + setPinned(tabId: number, pinned: boolean): Promise<Tab> { return browser.tabs.update(tabId, { pinned }); } - duplicate(id) { + duplicate(id: number): Promise<Tab> { return browser.tabs.duplicate(id); } - getZoom(tabId) { + getZoom(tabId: number): Promise<number> { return browser.tabs.getZoom(tabId); } - setZoom(tabId, factor) { + setZoom(tabId: number, factor: number): Promise<void> { return browser.tabs.setZoom(tabId, factor); } - onSelected(listener) { + onSelected( + listener: (arg: { tabId: number, windowId: number}) => void, + ): void { browser.tabs.onActivated.addListener(listener); } } let tabPresenter = new TabPresenter(); -tabPresenter.onSelected((tab) => { +tabPresenter.onSelected((tab: any) => { let cache = new MemoryStorage(); let lastId = cache.get(CURRENT_SELECTED_KEY); diff --git a/src/background/presenters/WindowPresenter.js b/src/background/presenters/WindowPresenter.ts index a82c4a2..e04f258 100644 --- a/src/background/presenters/WindowPresenter.js +++ b/src/background/presenters/WindowPresenter.ts @@ -1,5 +1,5 @@ export default class WindowPresenter { - create(url) { + create(url: string): Promise<browser.windows.Window> { return browser.windows.create({ url }); } } diff --git a/src/background/repositories/BookmarkRepository.js b/src/background/repositories/BookmarkRepository.ts index 99f7ec4..b4da509 100644 --- a/src/background/repositories/BookmarkRepository.js +++ b/src/background/repositories/BookmarkRepository.ts @@ -1,5 +1,7 @@ export default class BookmarkRepository { - async create(title, url) { + async create( + title: string, url: string + ): Promise<browser.bookmarks.BookmarkTreeNode> { let item = await browser.bookmarks.create({ type: 'bookmark', title, diff --git a/src/background/repositories/BrowserSettingRepository.js b/src/background/repositories/BrowserSettingRepository.js deleted file mode 100644 index a9d2c06..0000000 --- a/src/background/repositories/BrowserSettingRepository.js +++ /dev/null @@ -1,8 +0,0 @@ -import * as urls from '../../shared/urls'; - -export default class BrowserSettingRepository { - async getHomepageUrls() { - let { value } = await browser.browserSettings.homepageOverride.get({}); - return value.split('|').map(urls.normalizeUrl); - } -} diff --git a/src/background/repositories/BrowserSettingRepository.ts b/src/background/repositories/BrowserSettingRepository.ts new file mode 100644 index 0000000..33b35dd --- /dev/null +++ b/src/background/repositories/BrowserSettingRepository.ts @@ -0,0 +1,24 @@ +import * as urls from '../../shared/urls'; + +declare namespace browser.browserSettings.homepageOverride { + + type BrowserSettings = { + value: string; + levelOfControl: LevelOfControlType; + }; + + type LevelOfControlType = + 'not_controllable' | + 'controlled_by_other_extensions' | + 'controllable_by_this_extension' | + 'controlled_by_this_extension'; + + function get(param: object): Promise<BrowserSettings>; +} + +export default class BrowserSettingRepository { + async getHomepageUrls(): Promise<string[]> { + let { value } = await browser.browserSettings.homepageOverride.get({}); + return value.split('|').map(urls.normalizeUrl); + } +} diff --git a/src/background/repositories/CompletionsRepository.js b/src/background/repositories/CompletionsRepository.ts index 1318d36..18af587 100644 --- a/src/background/repositories/CompletionsRepository.js +++ b/src/background/repositories/CompletionsRepository.ts @@ -1,7 +1,13 @@ +type Tab = browser.tabs.Tab; +type BookmarkTreeNode = browser.bookmarks.BookmarkTreeNode; + export default class CompletionsRepository { - async queryBookmarks(keywords) { + async queryBookmarks(keywords: string): Promise<BookmarkTreeNode[]> { let items = await browser.bookmarks.search({ query: keywords }); return items.filter((item) => { + if (!item.url) { + return false; + } let url = undefined; try { url = new URL(item.url); @@ -12,17 +18,17 @@ export default class CompletionsRepository { }); } - queryHistories(keywords) { + queryHistories(keywords: string): Promise<browser.history.HistoryItem[]> { return browser.history.search({ text: keywords, startTime: 0, }); } - async queryTabs(keywords, excludePinned) { + async queryTabs(keywords: string, excludePinned: boolean): Promise<Tab[]> { let tabs = await browser.tabs.query({ currentWindow: true }); return tabs.filter((t) => { - return t.url.toLowerCase().includes(keywords.toLowerCase()) || + return t.url && t.url.toLowerCase().includes(keywords.toLowerCase()) || t.title && t.title.toLowerCase().includes(keywords.toLowerCase()); }).filter((t) => { return !(excludePinned && t.pinned); diff --git a/src/background/repositories/FindRepository.js b/src/background/repositories/FindRepository.ts index 74ec914..bf286e6 100644 --- a/src/background/repositories/FindRepository.js +++ b/src/background/repositories/FindRepository.ts @@ -3,15 +3,17 @@ import MemoryStorage from '../infrastructures/MemoryStorage'; const FIND_KEYWORD_KEY = 'find-keyword'; export default class FindRepository { + private cache: MemoryStorage; + constructor() { this.cache = new MemoryStorage(); } - getKeyword() { + getKeyword(): Promise<string> { return Promise.resolve(this.cache.get(FIND_KEYWORD_KEY)); } - setKeyword(keyword) { + setKeyword(keyword: string): Promise<any> { this.cache.set(FIND_KEYWORD_KEY, keyword); return Promise.resolve(); } diff --git a/src/background/repositories/MarkRepository.js b/src/background/repositories/MarkRepository.ts index 282c712..69c85f6 100644 --- a/src/background/repositories/MarkRepository.js +++ b/src/background/repositories/MarkRepository.ts @@ -4,21 +4,23 @@ import GlobalMark from '../domains/GlobalMark'; const MARK_KEY = 'mark'; export default class MarkRepository { + private cache: MemoryStorage; + constructor() { this.cache = new MemoryStorage(); } - getMark(key) { + getMark(key: string): Promise<GlobalMark | undefined> { let marks = this.getOrEmptyMarks(); let data = marks[key]; if (!data) { return Promise.resolve(undefined); } - let mark = new GlobalMark(data.tabId, data.url, data.x, data.y); + let mark = { tabId: data.tabId, url: data.url, x: data.x, y: data.y }; return Promise.resolve(mark); } - setMark(key, mark) { + setMark(key: string, mark: GlobalMark): Promise<any> { let marks = this.getOrEmptyMarks(); marks[key] = { tabId: mark.tabId, url: mark.url, x: mark.x, y: mark.y }; this.cache.set(MARK_KEY, marks); diff --git a/src/background/repositories/PersistentSettingRepository.js b/src/background/repositories/PersistentSettingRepository.ts index 4cab107..ff882a5 100644 --- a/src/background/repositories/PersistentSettingRepository.js +++ b/src/background/repositories/PersistentSettingRepository.ts @@ -1,12 +1,12 @@ -import Setting from '../domains/Setting'; +import SettingData from '../../shared/SettingData'; export default class SettingRepository { - async load() { + async load(): Promise<SettingData | null> { let { settings } = await browser.storage.local.get('settings'); if (!settings) { return null; } - return Setting.deserialize(settings); + return SettingData.valueOf(settings as any); } } diff --git a/src/background/repositories/SettingRepository.js b/src/background/repositories/SettingRepository.js deleted file mode 100644 index c4667a9..0000000 --- a/src/background/repositories/SettingRepository.js +++ /dev/null @@ -1,23 +0,0 @@ -import MemoryStorage from '../infrastructures/MemoryStorage'; - -const CACHED_SETTING_KEY = 'setting'; - -export default class SettingRepository { - constructor() { - this.cache = new MemoryStorage(); - } - - get() { - return Promise.resolve(this.cache.get(CACHED_SETTING_KEY)); - } - - update(value) { - return this.cache.set(CACHED_SETTING_KEY, value); - } - - async setProperty(name, value) { - let current = await this.get(); - current.properties[name] = value; - return this.update(current); - } -} diff --git a/src/background/repositories/SettingRepository.ts b/src/background/repositories/SettingRepository.ts new file mode 100644 index 0000000..eb83a2c --- /dev/null +++ b/src/background/repositories/SettingRepository.ts @@ -0,0 +1,51 @@ +import MemoryStorage from '../infrastructures/MemoryStorage'; +import Settings from '../../shared/Settings'; +import * as PropertyDefs from '../../shared/property-defs'; + +const CACHED_SETTING_KEY = 'setting'; + +export default class SettingRepository { + private cache: MemoryStorage; + + constructor() { + this.cache = new MemoryStorage(); + } + + get(): Promise<Settings> { + return Promise.resolve(this.cache.get(CACHED_SETTING_KEY)); + } + + update(value: Settings): void { + return this.cache.set(CACHED_SETTING_KEY, value); + } + + async setProperty( + name: string, value: string | number | boolean, + ): Promise<void> { + let def = PropertyDefs.defs.find(d => name === d.name); + if (!def) { + throw new Error('unknown property: ' + name); + } + if (typeof value !== def.type) { + throw new TypeError(`property type of ${name} mismatch: ${typeof value}`); + } + let newValue = value; + if (typeof value === 'string' && value === '') { + newValue = def.defaultValue; + } + + let current = await this.get(); + switch (name) { + case 'hintchars': + current.properties.hintchars = newValue as string; + break; + case 'smoothscroll': + current.properties.smoothscroll = newValue as boolean; + break; + case 'complete': + current.properties.complete = newValue as string; + break; + } + return this.update(current); + } +} diff --git a/src/background/repositories/VersionRepository.js b/src/background/repositories/VersionRepository.js deleted file mode 100644 index 4c71d05..0000000 --- a/src/background/repositories/VersionRepository.js +++ /dev/null @@ -1,10 +0,0 @@ -export default class VersionRepository { - async get() { - let { version } = await browser.storage.local.get('version'); - return version; - } - - update(version) { - return browser.storage.local.set({ version }); - } -} diff --git a/src/background/usecases/AddonEnabledUseCase.js b/src/background/usecases/AddonEnabledUseCase.ts index bb2c347..0a6fb03 100644 --- a/src/background/usecases/AddonEnabledUseCase.js +++ b/src/background/usecases/AddonEnabledUseCase.ts @@ -3,10 +3,20 @@ import TabPresenter from '../presenters/TabPresenter'; import ContentMessageClient from '../infrastructures/ContentMessageClient'; export default class AddonEnabledUseCase { + private indicatorPresentor: IndicatorPresenter; + + private tabPresenter: TabPresenter; + + private contentMessageClient: ContentMessageClient; + constructor() { this.indicatorPresentor = new IndicatorPresenter(); - this.indicatorPresentor.onClick(tab => this.onIndicatorClick(tab.id)); + this.indicatorPresentor.onClick((tab) => { + if (tab.id) { + this.onIndicatorClick(tab.id); + } + }); this.tabPresenter = new TabPresenter(); this.tabPresenter.onSelected(info => this.onTabSelected(info.tabId)); @@ -14,15 +24,15 @@ export default class AddonEnabledUseCase { this.contentMessageClient = new ContentMessageClient(); } - indicate(enabled) { + indicate(enabled: boolean): Promise<void> { return this.indicatorPresentor.indicate(enabled); } - onIndicatorClick(tabId) { + onIndicatorClick(tabId: number): Promise<void> { return this.contentMessageClient.toggleAddonEnabled(tabId); } - async onTabSelected(tabId) { + async onTabSelected(tabId: number): Promise<void> { let enabled = await this.contentMessageClient.getAddonEnabled(tabId); return this.indicatorPresentor.indicate(enabled); } diff --git a/src/background/usecases/CommandUseCase.js b/src/background/usecases/CommandUseCase.ts index 9ec46fe..2247d7b 100644 --- a/src/background/usecases/CommandUseCase.js +++ b/src/background/usecases/CommandUseCase.ts @@ -6,9 +6,20 @@ import SettingRepository from '../repositories/SettingRepository'; import BookmarkRepository from '../repositories/BookmarkRepository'; import ConsoleClient from '../infrastructures/ConsoleClient'; import ContentMessageClient from '../infrastructures/ContentMessageClient'; -import * as properties from 'shared/settings/properties'; export default class CommandIndicator { + private tabPresenter: TabPresenter; + + private windowPresenter: WindowPresenter; + + private settingRepository: SettingRepository; + + private bookmarkRepository: BookmarkRepository; + + private consoleClient: ConsoleClient; + + private contentMessageClient: ContentMessageClient; + constructor() { this.tabPresenter = new TabPresenter(); this.windowPresenter = new WindowPresenter(); @@ -19,34 +30,34 @@ export default class CommandIndicator { this.contentMessageClient = new ContentMessageClient(); } - async open(keywords) { + async open(keywords: string): Promise<browser.tabs.Tab> { let url = await this.urlOrSearch(keywords); return this.tabPresenter.open(url); } - async tabopen(keywords) { + async tabopen(keywords: string): Promise<browser.tabs.Tab> { let url = await this.urlOrSearch(keywords); return this.tabPresenter.create(url); } - async winopen(keywords) { + async winopen(keywords: string): Promise<browser.windows.Window> { let url = await this.urlOrSearch(keywords); return this.windowPresenter.create(url); } // eslint-disable-next-line max-statements - async buffer(keywords) { + async buffer(keywords: string): Promise<any> { if (keywords.length === 0) { return; } - if (!isNaN(keywords)) { + if (!isNaN(Number(keywords))) { let tabs = await this.tabPresenter.getAll(); let index = parseInt(keywords, 10) - 1; if (index < 0 || tabs.length <= index) { throw new RangeError(`tab ${index + 1} does not exist`); } - return this.tabPresenter.select(tabs[index].id); + return this.tabPresenter.select(tabs[index].id as number); } else if (keywords.trim() === '%') { // Select current window return; @@ -66,13 +77,13 @@ export default class CommandIndicator { } for (let tab of tabs) { if (tab.index > current.index) { - return this.tabPresenter.select(tab.id); + return this.tabPresenter.select(tab.id as number); } } - return this.tabPresenter.select(tabs[0].id); + return this.tabPresenter.select(tabs[0].id as number); } - async bdelete(force, keywords) { + async bdelete(force: boolean, keywords: string): Promise<any> { let excludePinned = !force; let tabs = await this.tabPresenter.getByKeyword(keywords, excludePinned); if (tabs.length === 0) { @@ -80,45 +91,45 @@ export default class CommandIndicator { } else if (tabs.length > 1) { throw new Error('More than one match for ' + keywords); } - return this.tabPresenter.remove([tabs[0].id]); + return this.tabPresenter.remove([tabs[0].id as number]); } - async bdeletes(force, keywords) { + async bdeletes(force: boolean, keywords: string): Promise<any> { let excludePinned = !force; let tabs = await this.tabPresenter.getByKeyword(keywords, excludePinned); - let ids = tabs.map(tab => tab.id); + let ids = tabs.map(tab => tab.id as number); return this.tabPresenter.remove(ids); } - async quit() { + async quit(): Promise<any> { let tab = await this.tabPresenter.getCurrent(); - return this.tabPresenter.remove([tab.id]); + return this.tabPresenter.remove([tab.id as number]); } - async quitAll() { + async quitAll(): Promise<any> { let tabs = await this.tabPresenter.getAll(); - let ids = tabs.map(tab => tab.id); + let ids = tabs.map(tab => tab.id as number); this.tabPresenter.remove(ids); } - async addbookmark(title) { + async addbookmark(title: string): Promise<any> { let tab = await this.tabPresenter.getCurrent(); - let item = await this.bookmarkRepository.create(title, tab.url); + let item = await this.bookmarkRepository.create(title, tab.url as string); let message = 'Saved current page: ' + item.url; - return this.consoleClient.showInfo(tab.id, message); + return this.consoleClient.showInfo(tab.id as number, message); } - async set(keywords) { + async set(keywords: string): Promise<any> { if (keywords.length === 0) { return; } - let [name, value] = parsers.parseSetOption(keywords, properties.types); + let [name, value] = parsers.parseSetOption(keywords); await this.settingRepository.setProperty(name, value); return this.contentMessageClient.broadcastSettingsChanged(); } - async urlOrSearch(keywords) { + async urlOrSearch(keywords: string): Promise<any> { let settings = await this.settingRepository.get(); return urls.searchUrl(keywords, settings.search); } diff --git a/src/background/usecases/CompletionsUseCase.js b/src/background/usecases/CompletionsUseCase.ts index 7dc30ac..ae1ceed 100644 --- a/src/background/usecases/CompletionsUseCase.js +++ b/src/background/usecases/CompletionsUseCase.ts @@ -1,23 +1,30 @@ -import CompletionItem from '../domains/CompletionItem'; import CompletionGroup from '../domains/CompletionGroup'; -import Completions from '../domains/Completions'; import CommandDocs from '../domains/CommandDocs'; import CompletionsRepository from '../repositories/CompletionsRepository'; import * as filters from './filters'; import SettingRepository from '../repositories/SettingRepository'; import TabPresenter from '../presenters/TabPresenter'; -import * as properties from '../../shared/settings/properties'; +import * as PropertyDefs from '../../shared/property-defs'; const COMPLETION_ITEM_LIMIT = 10; +type Tab = browser.tabs.Tab; +type HistoryItem = browser.history.HistoryItem; + export default class CompletionsUseCase { + private tabPresenter: TabPresenter; + + private completionsRepository: CompletionsRepository; + + private settingRepository: SettingRepository; + constructor() { this.tabPresenter = new TabPresenter(); this.completionsRepository = new CompletionsRepository(); this.settingRepository = new SettingRepository(); } - queryConsoleCommand(prefix) { + queryConsoleCommand(prefix: string): Promise<CompletionGroup[]> { let keys = Object.keys(CommandDocs); let items = keys .filter(name => name.startsWith(prefix)) @@ -28,48 +35,49 @@ export default class CompletionsUseCase { })); if (items.length === 0) { - return Promise.resolve(Completions.empty()); + return Promise.resolve([]); } - return Promise.resolve( - new Completions([new CompletionGroup('Console Command', items)]) - ); + return Promise.resolve([{ name: 'Console Command', items }]); } - async queryOpen(name, keywords) { + async queryOpen(name: string, keywords: string): Promise<CompletionGroup[]> { let settings = await this.settingRepository.get(); - let groups = []; + let groups: CompletionGroup[] = []; - let complete = settings.properties.complete || properties.defaults.complete; + let complete = settings.properties.complete; for (let c of complete) { if (c === 's') { // eslint-disable-next-line no-await-in-loop let engines = await this.querySearchEngineItems(name, keywords); if (engines.length > 0) { - groups.push(new CompletionGroup('Search Engines', engines)); + groups.push({ name: 'Search Engines', items: engines }); } } else if (c === 'h') { // eslint-disable-next-line no-await-in-loop let histories = await this.queryHistoryItems(name, keywords); if (histories.length > 0) { - groups.push(new CompletionGroup('History', histories)); + groups.push({ name: 'History', items: histories }); } } else if (c === 'b') { // eslint-disable-next-line no-await-in-loop let bookmarks = await this.queryBookmarkItems(name, keywords); if (bookmarks.length > 0) { - groups.push(new CompletionGroup('Bookmarks', bookmarks)); + groups.push({ name: 'Bookmarks', items: bookmarks }); } } } - return new Completions(groups); + return groups; } // eslint-disable-next-line max-statements - async queryBuffer(name, keywords) { + async queryBuffer( + name: string, + keywords: string, + ): Promise<CompletionGroup[]> { let lastId = await this.tabPresenter.getLastSelectedId(); let trimmed = keywords.trim(); - let tabs = []; - if (trimmed.length > 0 && !isNaN(trimmed)) { + let tabs: Tab[] = []; + if (trimmed.length > 0 && !isNaN(Number(trimmed))) { let all = await this.tabPresenter.getAll(); let index = parseInt(trimmed, 10) - 1; if (index >= 0 && index < all.length) { @@ -77,18 +85,18 @@ export default class CompletionsUseCase { } } else if (trimmed === '%') { let all = await this.tabPresenter.getAll(); - let tab = all.find(t => t.active); + let tab = all.find(t => t.active) as Tab; tabs = [tab]; } else if (trimmed === '#') { if (typeof lastId !== 'undefined' && lastId !== null) { let all = await this.tabPresenter.getAll(); - let tab = all.find(t => t.id === lastId); + let tab = all.find(t => t.id === lastId) as Tab; tabs = [tab]; } } else { tabs = await this.completionsRepository.queryTabs(keywords, false); } - const flag = (tab) => { + const flag = (tab: Tab) => { if (tab.active) { return '%'; } else if (tab.id === lastId) { @@ -96,87 +104,90 @@ export default class CompletionsUseCase { } return ' '; }; - let items = tabs.map(tab => new CompletionItem({ + let items = tabs.map(tab => ({ caption: tab.index + 1 + ': ' + flag(tab) + ' ' + tab.title, content: name + ' ' + tab.title, url: tab.url, - icon: tab.favIconUrl + icon: tab.favIconUrl, })); if (items.length === 0) { - return Promise.resolve(Completions.empty()); + return Promise.resolve([]); } - return new Completions([new CompletionGroup('Buffers', items)]); + return [{ name: 'Buffers', items }]; } - queryBdelete(name, keywords) { + queryBdelete(name: string, keywords: string): Promise<CompletionGroup[]> { return this.queryTabs(name, true, keywords); } - queryBdeleteForce(name, keywords) { + queryBdeleteForce( + name: string, keywords: string, + ): Promise<CompletionGroup[]> { return this.queryTabs(name, false, keywords); } - querySet(name, keywords) { - let items = Object.keys(properties.docs).map((key) => { - if (properties.types[key] === 'boolean') { + querySet(name: string, keywords: string): Promise<CompletionGroup[]> { + let items = PropertyDefs.defs.map((def) => { + if (def.type === 'boolean') { return [ - new CompletionItem({ - caption: key, - content: name + ' ' + key, - url: 'Enable ' + properties.docs[key], - }), - new CompletionItem({ - caption: 'no' + key, - content: name + ' no' + key, - url: 'Disable ' + properties.docs[key], - }), + { + caption: def.name, + content: name + ' ' + def.name, + url: 'Enable ' + def.description, + }, { + caption: 'no' + def.name, + content: name + ' no' + def.name, + url: 'Disable ' + def.description + } ]; } return [ - new CompletionItem({ - caption: key, - content: name + ' ' + key, - url: 'Set ' + properties.docs[key], - }) + { + caption: def.name, + content: name + ' ' + def.name, + url: 'Set ' + def.description, + } ]; }); - items = items.reduce((acc, val) => acc.concat(val), []); - items = items.filter((item) => { + let flatten = items.reduce((acc, val) => acc.concat(val), []); + flatten = flatten.filter((item) => { return item.caption.startsWith(keywords); }); - if (items.length === 0) { - return Promise.resolve(Completions.empty()); + if (flatten.length === 0) { + return Promise.resolve([]); } return Promise.resolve( - new Completions([new CompletionGroup('Properties', items)]) + [{ name: 'Properties', items: flatten }], ); } - async queryTabs(name, excludePinned, args) { + async queryTabs( + name: string, excludePinned: boolean, args: string, + ): Promise<CompletionGroup[]> { let tabs = await this.completionsRepository.queryTabs(args, excludePinned); - let items = tabs.map(tab => new CompletionItem({ + let items = tabs.map(tab => ({ caption: tab.title, content: name + ' ' + tab.title, url: tab.url, icon: tab.favIconUrl })); if (items.length === 0) { - return Promise.resolve(Completions.empty()); + return Promise.resolve([]); } - return new Completions([new CompletionGroup('Buffers', items)]); + return [{ name: 'Buffers', items }]; } - async querySearchEngineItems(name, keywords) { + async querySearchEngineItems(name: string, keywords: string) { let settings = await this.settingRepository.get(); let engines = Object.keys(settings.search.engines) .filter(key => key.startsWith(keywords)); - return engines.map(key => new CompletionItem({ + return engines.map(key => ({ caption: key, content: name + ' ' + key, })); } - async queryHistoryItems(name, keywords) { + async queryHistoryItems(name: string, keywords: string) { let histories = await this.completionsRepository.queryHistories(keywords); histories = [histories] .map(filters.filterBlankTitle) @@ -184,19 +195,21 @@ export default class CompletionsUseCase { .map(filters.filterByTailingSlash) .map(pages => filters.filterByPathname(pages, COMPLETION_ITEM_LIMIT)) .map(pages => filters.filterByOrigin(pages, COMPLETION_ITEM_LIMIT))[0] - .sort((x, y) => x.visitCount < y.visitCount) + .sort((x: HistoryItem, y: HistoryItem): number => { + return Number(x.visitCount) - Number(y.visitCount); + }) .slice(0, COMPLETION_ITEM_LIMIT); - return histories.map(page => new CompletionItem({ + return histories.map(page => ({ caption: page.title, content: name + ' ' + page.url, url: page.url })); } - async queryBookmarkItems(name, keywords) { + async queryBookmarkItems(name: string, keywords: string) { let bookmarks = await this.completionsRepository.queryBookmarks(keywords); return bookmarks.slice(0, COMPLETION_ITEM_LIMIT) - .map(page => new CompletionItem({ + .map(page => ({ caption: page.title, content: name + ' ' + page.url, url: page.url diff --git a/src/background/usecases/ConsoleUseCase.js b/src/background/usecases/ConsoleUseCase.js deleted file mode 100644 index e8e5d4a..0000000 --- a/src/background/usecases/ConsoleUseCase.js +++ /dev/null @@ -1,61 +0,0 @@ -import TabPresenter from '../presenters/TabPresenter'; -import ConsoleClient from '../infrastructures/ConsoleClient'; - -export default class ConsoleUseCase { - constructor() { - this.tabPresenter = new TabPresenter(); - this.consoleClient = new ConsoleClient(); - } - - async showCommand() { - let tab = await this.tabPresenter.getCurrent(); - return this.consoleClient.showCommand(tab.id, ''); - } - - async showOpenCommand(alter) { - let tab = await this.tabPresenter.getCurrent(); - let command = 'open '; - if (alter) { - command += tab.url; - } - return this.consoleClient.showCommand(tab.id, command); - } - - async showTabopenCommand(alter) { - let tab = await this.tabPresenter.getCurrent(); - let command = 'tabopen '; - if (alter) { - command += tab.url; - } - return this.consoleClient.showCommand(tab.id, command); - } - - async showWinopenCommand(alter) { - let tab = await this.tabPresenter.getCurrent(); - let command = 'winopen '; - if (alter) { - command += tab.url; - } - return this.consoleClient.showCommand(tab.id, command); - } - - async showBufferCommand() { - let tab = await this.tabPresenter.getCurrent(); - let command = 'buffer '; - return this.consoleClient.showCommand(tab.id, command); - } - - async showAddbookmarkCommand(alter) { - let tab = await this.tabPresenter.getCurrent(); - let command = 'addbookmark '; - if (alter) { - command += tab.title; - } - return this.consoleClient.showCommand(tab.id, command); - } - - async hideConsole() { - let tab = await this.tabPresenter.getCurrent(); - return this.consoleClient.hide(tab.id); - } -} diff --git a/src/background/usecases/ConsoleUseCase.ts b/src/background/usecases/ConsoleUseCase.ts new file mode 100644 index 0000000..60c0439 --- /dev/null +++ b/src/background/usecases/ConsoleUseCase.ts @@ -0,0 +1,65 @@ +import TabPresenter from '../presenters/TabPresenter'; +import ConsoleClient from '../infrastructures/ConsoleClient'; + +export default class ConsoleUseCase { + private tabPresenter: TabPresenter; + + private consoleClient: ConsoleClient; + + constructor() { + this.tabPresenter = new TabPresenter(); + this.consoleClient = new ConsoleClient(); + } + + async showCommand(): Promise<any> { + let tab = await this.tabPresenter.getCurrent(); + return this.consoleClient.showCommand(tab.id as number, ''); + } + + async showOpenCommand(alter: boolean): Promise<any> { + let tab = await this.tabPresenter.getCurrent(); + let command = 'open '; + if (alter) { + command += tab.url || ''; + } + return this.consoleClient.showCommand(tab.id as number, command); + } + + async showTabopenCommand(alter: boolean): Promise<any> { + let tab = await this.tabPresenter.getCurrent(); + let command = 'tabopen '; + if (alter) { + command += tab.url || ''; + } + return this.consoleClient.showCommand(tab.id as number, command); + } + + async showWinopenCommand(alter: boolean): Promise<any> { + let tab = await this.tabPresenter.getCurrent(); + let command = 'winopen '; + if (alter) { + command += tab.url || ''; + } + return this.consoleClient.showCommand(tab.id as number, command); + } + + async showBufferCommand(): Promise<any> { + let tab = await this.tabPresenter.getCurrent(); + let command = 'buffer '; + return this.consoleClient.showCommand(tab.id as number, command); + } + + async showAddbookmarkCommand(alter: boolean): Promise<any> { + let tab = await this.tabPresenter.getCurrent(); + let command = 'addbookmark '; + if (alter) { + command += tab.title || ''; + } + return this.consoleClient.showCommand(tab.id as number, command); + } + + async hideConsole(): Promise<any> { + let tab = await this.tabPresenter.getCurrent(); + return this.consoleClient.hide(tab.id as number); + } +} diff --git a/src/background/usecases/FindUseCase.js b/src/background/usecases/FindUseCase.ts index 224e4a9..d567800 100644 --- a/src/background/usecases/FindUseCase.js +++ b/src/background/usecases/FindUseCase.ts @@ -3,22 +3,28 @@ import TabPresenter from '../presenters/TabPresenter'; import ConsoleClient from '../infrastructures/ConsoleClient'; export default class FindUseCase { + private tabPresenter: TabPresenter; + + private findRepository: FindRepository; + + private consoleClient: ConsoleClient; + constructor() { this.tabPresenter = new TabPresenter(); this.findRepository = new FindRepository(); this.consoleClient = new ConsoleClient(); } - getKeyword() { + getKeyword(): Promise<string> { return this.findRepository.getKeyword(); } - setKeyword(keyword) { + setKeyword(keyword: string): Promise<any> { return this.findRepository.setKeyword(keyword); } - async findStart() { + async findStart(): Promise<any> { let tab = await this.tabPresenter.getCurrent(); - return this.consoleClient.showFind(tab.id); + return this.consoleClient.showFind(tab.id as number); } } diff --git a/src/background/usecases/LinkUseCase.js b/src/background/usecases/LinkUseCase.ts index 89412c5..2f4df7b 100644 --- a/src/background/usecases/LinkUseCase.js +++ b/src/background/usecases/LinkUseCase.ts @@ -1,17 +1,17 @@ -import SettingRepository from '../repositories/SettingRepository'; import TabPresenter from '../presenters/TabPresenter'; export default class LinkUseCase { + private tabPresenter: TabPresenter; + constructor() { - this.settingRepository = new SettingRepository(); this.tabPresenter = new TabPresenter(); } - openToTab(url, tabId) { + openToTab(url: string, tabId: number): Promise<any> { return this.tabPresenter.open(url, tabId); } - openNewTab(url, openerId, background) { + openNewTab(url: string, openerId: number, background: boolean): Promise<any> { return this.tabPresenter.create(url, { openerTabId: openerId, active: !background }); diff --git a/src/background/usecases/MarkUseCase.js b/src/background/usecases/MarkUseCase.ts index 39c796b..e376c55 100644 --- a/src/background/usecases/MarkUseCase.js +++ b/src/background/usecases/MarkUseCase.ts @@ -1,10 +1,17 @@ -import GlobalMark from '../domains/GlobalMark'; import TabPresenter from '../presenters/TabPresenter'; import MarkRepository from '../repositories/MarkRepository'; import ConsoleClient from '../infrastructures/ConsoleClient'; import ContentMessageClient from '../infrastructures/ContentMessageClient'; export default class MarkUseCase { + private tabPresenter: TabPresenter; + + private markRepository: MarkRepository; + + private consoleClient: ConsoleClient; + + private contentMessageClient: ContentMessageClient; + constructor() { this.tabPresenter = new TabPresenter(); this.markRepository = new MarkRepository(); @@ -12,28 +19,28 @@ export default class MarkUseCase { this.contentMessageClient = new ContentMessageClient(); } - async setGlobal(key, x, y) { + async setGlobal(key: string, x: number, y: number): Promise<any> { let tab = await this.tabPresenter.getCurrent(); - let mark = new GlobalMark(tab.id, tab.url, x, y); + let mark = { tabId: tab.id as number, url: tab.url as string, x, y }; return this.markRepository.setMark(key, mark); } - async jumpGlobal(key) { + async jumpGlobal(key: string): Promise<any> { let current = await this.tabPresenter.getCurrent(); let mark = await this.markRepository.getMark(key); if (!mark) { - return this.consoleClient.showError(current.id, 'Mark is not set'); + return this.consoleClient.showError( + current.id as number, 'Mark is not set'); } - - return this.contentMessageClient.scrollTo( - mark.tabId, mark.x, mark.y - ).then(() => { + try { + await this.contentMessageClient.scrollTo(mark.tabId, mark.x, mark.y); return this.tabPresenter.select(mark.tabId); - }).catch(async() => { + } catch (e) { let tab = await this.tabPresenter.create(mark.url); - let mark2 = new GlobalMark(tab.id, mark.url, mark.x, mark.y); - return this.markRepository.setMark(key, mark2); - }); + return this.markRepository.setMark(key, { + tabId: tab.id as number, url: mark.url, x: mark.x, y: mark.y, + }); + } } } diff --git a/src/background/usecases/SettingUseCase.js b/src/background/usecases/SettingUseCase.ts index 9e17408..aa3b534 100644 --- a/src/background/usecases/SettingUseCase.js +++ b/src/background/usecases/SettingUseCase.ts @@ -1,28 +1,31 @@ -import Setting from '../domains/Setting'; // eslint-disable-next-line max-len import PersistentSettingRepository from '../repositories/PersistentSettingRepository'; import SettingRepository from '../repositories/SettingRepository'; +import { DefaultSettingData } from '../../shared/SettingData'; +import Settings from '../../shared/Settings'; export default class SettingUseCase { + private persistentSettingRepository: PersistentSettingRepository; + + private settingRepository: SettingRepository; + constructor() { this.persistentSettingRepository = new PersistentSettingRepository(); this.settingRepository = new SettingRepository(); } - get() { + get(): Promise<Settings> { return this.settingRepository.get(); } - async reload() { - let settings = await this.persistentSettingRepository.load(); - if (!settings) { - settings = Setting.defaultSettings(); + async reload(): Promise<Settings> { + let data = await this.persistentSettingRepository.load(); + if (!data) { + data = DefaultSettingData; } - let value = settings.value(); - + let value = data.toSettings(); this.settingRepository.update(value); - return value; } } diff --git a/src/background/usecases/TabSelectUseCase.js b/src/background/usecases/TabSelectUseCase.ts index 16b3e14..a0b52f0 100644 --- a/src/background/usecases/TabSelectUseCase.js +++ b/src/background/usecases/TabSelectUseCase.ts @@ -1,11 +1,13 @@ import TabPresenter from '../presenters/TabPresenter'; export default class TabSelectUseCase { + private tabPresenter: TabPresenter; + constructor() { this.tabPresenter = new TabPresenter(); } - async selectPrev(count) { + async selectPrev(count: number): Promise<any> { let tabs = await this.tabPresenter.getAll(); if (tabs.length < 2) { return; @@ -15,10 +17,10 @@ export default class TabSelectUseCase { return; } let select = (tab.index - count + tabs.length) % tabs.length; - return this.tabPresenter.select(tabs[select].id); + return this.tabPresenter.select(tabs[select].id as number); } - async selectNext(count) { + async selectNext(count: number): Promise<any> { let tabs = await this.tabPresenter.getAll(); if (tabs.length < 2) { return; @@ -28,24 +30,24 @@ export default class TabSelectUseCase { return; } let select = (tab.index + count) % tabs.length; - return this.tabPresenter.select(tabs[select].id); + return this.tabPresenter.select(tabs[select].id as number); } - async selectFirst() { + async selectFirst(): Promise<any> { let tabs = await this.tabPresenter.getAll(); - return this.tabPresenter.select(tabs[0].id); + return this.tabPresenter.select(tabs[0].id as number); } - async selectLast() { + async selectLast(): Promise<any> { let tabs = await this.tabPresenter.getAll(); - return this.tabPresenter.select(tabs[tabs.length - 1].id); + return this.tabPresenter.select(tabs[tabs.length - 1].id as number); } - async selectPrevSelected() { + async selectPrevSelected(): Promise<any> { let tabId = await this.tabPresenter.getLastSelectedId(); if (tabId === null || typeof tabId === 'undefined') { - return; + return Promise.resolve(); } - this.tabPresenter.select(tabId); + return this.tabPresenter.select(tabId); } } diff --git a/src/background/usecases/TabUseCase.js b/src/background/usecases/TabUseCase.ts index d930842..1615333 100644 --- a/src/background/usecases/TabUseCase.js +++ b/src/background/usecases/TabUseCase.ts @@ -2,20 +2,24 @@ import TabPresenter from '../presenters/TabPresenter'; import BrowserSettingRepository from '../repositories/BrowserSettingRepository'; export default class TabUseCase { + private tabPresenter: TabPresenter; + + private browserSettingRepository: BrowserSettingRepository; + constructor() { this.tabPresenter = new TabPresenter(); this.browserSettingRepository = new BrowserSettingRepository(); } - async close(force) { + async close(force: boolean): Promise<any> { let tab = await this.tabPresenter.getCurrent(); if (!force && tab.pinned) { - return; + return Promise.resolve(); } - return this.tabPresenter.remove([tab.id]); + return this.tabPresenter.remove([tab.id as number]); } - async closeRight() { + async closeRight(): Promise<any> { let tabs = await this.tabPresenter.getAll(); tabs.sort((t1, t2) => t1.index - t2.index); let index = tabs.findIndex(t => t.active); @@ -25,42 +29,42 @@ export default class TabUseCase { for (let i = index + 1; i < tabs.length; ++i) { let tab = tabs[i]; if (!tab.pinned) { - this.tabPresenter.remove(tab.id); + this.tabPresenter.remove([tab.id as number]); } } } - reopen() { + reopen(): Promise<any> { return this.tabPresenter.reopen(); } - async reload(cache) { + async reload(cache: boolean): Promise<any> { let tab = await this.tabPresenter.getCurrent(); - return this.tabPresenter.reload(tab.id, cache); + return this.tabPresenter.reload(tab.id as number, cache); } - async setPinned(pinned) { + async setPinned(pinned: boolean): Promise<any> { let tab = await this.tabPresenter.getCurrent(); - return this.tabPresenter.setPinned(tab.id, pinned); + return this.tabPresenter.setPinned(tab.id as number, pinned); } - async togglePinned() { + async togglePinned(): Promise<any> { let tab = await this.tabPresenter.getCurrent(); - return this.tabPresenter.setPinned(tab.id, !tab.pinned); + return this.tabPresenter.setPinned(tab.id as number, !tab.pinned); } - async duplicate() { + async duplicate(): Promise<any> { let tab = await this.tabPresenter.getCurrent(); - return this.tabPresenter.duplicate(tab.id); + return this.tabPresenter.duplicate(tab.id as number); } - async openPageSource() { + async openPageSource(): Promise<any> { let tab = await this.tabPresenter.getCurrent(); let url = 'view-source:' + tab.url; return this.tabPresenter.create(url); } - async openHome(newTab) { + async openHome(newTab: boolean): Promise<any> { let tab = await this.tabPresenter.getCurrent(); let urls = await this.browserSettingRepository.getHomepageUrls(); if (urls.length === 1 && urls[0] === 'about:home') { diff --git a/src/background/usecases/VersionUseCase.js b/src/background/usecases/VersionUseCase.ts index ed5112b..8154eba 100644 --- a/src/background/usecases/VersionUseCase.js +++ b/src/background/usecases/VersionUseCase.ts @@ -1,23 +1,27 @@ -import manifest from '../../../manifest.json'; import TabPresenter from '../presenters/TabPresenter'; import NotifyPresenter from '../presenters/NotifyPresenter'; export default class VersionUseCase { + private tabPresenter: TabPresenter; + + private notifyPresenter: NotifyPresenter; + constructor() { this.tabPresenter = new TabPresenter(); this.notifyPresenter = new NotifyPresenter(); } - notify() { + notify(): Promise<void> { + let manifest = browser.runtime.getManifest(); let title = `Vim Vixen ${manifest.version} has been installed`; let message = 'Click here to see release notes'; let url = this.releaseNoteUrl(manifest.version); - this.notifyPresenter.notify(title, message, () => { + return this.notifyPresenter.notify(title, message, () => { this.tabPresenter.create(url); }); } - releaseNoteUrl(version) { + releaseNoteUrl(version?: string): string { if (version) { return `https://github.com/ueokande/vim-vixen/releases/tag/${version}`; } diff --git a/src/background/usecases/ZoomUseCase.js b/src/background/usecases/ZoomUseCase.js deleted file mode 100644 index 692d6d9..0000000 --- a/src/background/usecases/ZoomUseCase.js +++ /dev/null @@ -1,35 +0,0 @@ -import TabPresenter from '../presenters/TabPresenter'; - -const ZOOM_SETTINGS = [ - 0.33, 0.50, 0.66, 0.75, 0.80, 0.90, 1.00, - 1.10, 1.25, 1.50, 1.75, 2.00, 2.50, 3.00 -]; - -export default class ZoomUseCase { - constructor() { - this.tabPresenter = new TabPresenter(); - } - - async zoomIn(tabId) { - let tab = await this.tabPresenter.getCurrent(); - let current = await this.tabPresenter.getZoom(tab.id); - let factor = ZOOM_SETTINGS.find(f => f > current); - if (factor) { - return this.tabPresenter.setZoom(tabId, factor); - } - } - - async zoomOut(tabId) { - let tab = await this.tabPresenter.getCurrent(); - let current = await this.tabPresenter.getZoom(tab.id); - let factor = [].concat(ZOOM_SETTINGS).reverse().find(f => f < current); - if (factor) { - return this.tabPresenter.setZoom(tabId, factor); - } - } - - zoomNutoral(tabId) { - return this.tabPresenter.setZoom(tabId, 1); - } - -} diff --git a/src/background/usecases/ZoomUseCase.ts b/src/background/usecases/ZoomUseCase.ts new file mode 100644 index 0000000..661c3cd --- /dev/null +++ b/src/background/usecases/ZoomUseCase.ts @@ -0,0 +1,39 @@ +import TabPresenter from '../presenters/TabPresenter'; + +const ZOOM_SETTINGS: number[] = [ + 0.33, 0.50, 0.66, 0.75, 0.80, 0.90, 1.00, + 1.10, 1.25, 1.50, 1.75, 2.00, 2.50, 3.00 +]; + +export default class ZoomUseCase { + private tabPresenter: TabPresenter; + + constructor() { + this.tabPresenter = new TabPresenter(); + } + + async zoomIn(): Promise<any> { + let tab = await this.tabPresenter.getCurrent(); + let tabId = tab.id as number; + let current = await this.tabPresenter.getZoom(tabId); + let factor = ZOOM_SETTINGS.find(f => f > current); + if (factor) { + return this.tabPresenter.setZoom(tabId as number, factor); + } + } + + async zoomOut(): Promise<any> { + let tab = await this.tabPresenter.getCurrent(); + let tabId = tab.id as number; + let current = await this.tabPresenter.getZoom(tabId); + let factor = ZOOM_SETTINGS.slice(0).reverse().find(f => f < current); + if (factor) { + return this.tabPresenter.setZoom(tabId as number, factor); + } + } + + async zoomNutoral(): Promise<any> { + let tab = await this.tabPresenter.getCurrent(); + return this.tabPresenter.setZoom(tab.id as number, 1); + } +} diff --git a/src/background/usecases/filters.js b/src/background/usecases/filters.js deleted file mode 100644 index d057dca..0000000 --- a/src/background/usecases/filters.js +++ /dev/null @@ -1,72 +0,0 @@ -const filterHttp = (items) => { - let httpsHosts = items.map(x => new URL(x.url)) - .filter(x => x.protocol === 'https:') - .map(x => x.host); - httpsHosts = new Set(httpsHosts); - - return items.filter((item) => { - let url = new URL(item.url); - return url.protocol === 'https:' || !httpsHosts.has(url.host); - }); -}; - -const filterBlankTitle = (items) => { - return items.filter(item => item.title && item.title !== ''); -}; - -const filterByTailingSlash = (items) => { - let urls = items.map(item => new URL(item.url)); - let simplePaths = urls - .filter(url => url.hash === '' && url.search === '') - .map(url => url.origin + url.pathname); - simplePaths = new Set(simplePaths); - - return items.filter((item) => { - let url = new URL(item.url); - if (url.hash !== '' || url.search !== '' || - url.pathname.slice(-1) !== '/') { - return true; - } - return !simplePaths.has(url.origin + url.pathname.slice(0, -1)); - }); -}; - -const filterByPathname = (items, min) => { - let hash = {}; - for (let item of items) { - let url = new URL(item.url); - let pathname = url.origin + url.pathname; - if (!hash[pathname]) { - hash[pathname] = item; - } else if (hash[pathname].url.length > item.url.length) { - hash[pathname] = item; - } - } - let filtered = Object.values(hash); - if (filtered.length < min) { - return items; - } - return filtered; -}; - -const filterByOrigin = (items, min) => { - let hash = {}; - for (let item of items) { - let origin = new URL(item.url).origin; - if (!hash[origin]) { - hash[origin] = item; - } else if (hash[origin].url.length > item.url.length) { - hash[origin] = item; - } - } - let filtered = Object.values(hash); - if (filtered.length < min) { - return items; - } - return filtered; -}; - -export { - filterHttp, filterBlankTitle, filterByTailingSlash, - filterByPathname, filterByOrigin -}; diff --git a/src/background/usecases/filters.ts b/src/background/usecases/filters.ts new file mode 100644 index 0000000..84a42fb --- /dev/null +++ b/src/background/usecases/filters.ts @@ -0,0 +1,76 @@ +type Item = browser.history.HistoryItem; + +const filterHttp = (items: Item[]): Item[] => { + let httpsHosts = items.map(x => new URL(x.url as string)) + .filter(x => x.protocol === 'https:') + .map(x => x.host); + let hostsSet = new Set(httpsHosts); + + return items.filter((item: Item) => { + let url = new URL(item.url as string); + return url.protocol === 'https:' || !hostsSet.has(url.host); + }); +}; + +const filterBlankTitle = (items: Item[]): Item[] => { + return items.filter(item => item.title && item.title !== ''); +}; + +const filterByTailingSlash = (items: Item[]): Item[] => { + let urls = items.map(item => new URL(item.url as string)); + let simplePaths = urls + .filter(url => url.hash === '' && url.search === '') + .map(url => url.origin + url.pathname); + let pathsSet = new Set(simplePaths); + + return items.filter((item) => { + let url = new URL(item.url as string); + if (url.hash !== '' || url.search !== '' || + url.pathname.slice(-1) !== '/') { + return true; + } + return !pathsSet.has(url.origin + url.pathname.slice(0, -1)); + }); +}; + +const filterByPathname = (items: Item[], min: number): Item[] => { + let hash: {[key: string]: Item} = {}; + for (let item of items) { + let url = new URL(item.url as string); + let pathname = url.origin + url.pathname; + if (!hash[pathname]) { + hash[pathname] = item; + } else if ((hash[pathname].url as string).length > + (item.url as string).length) { + hash[pathname] = item; + } + } + let filtered = Object.values(hash); + if (filtered.length < min) { + return items; + } + return filtered; +}; + +const filterByOrigin = (items: Item[], min: number): Item[] => { + let hash: {[key: string]: Item} = {}; + for (let item of items) { + let origin = new URL(item.url as string).origin; + if (!hash[origin]) { + hash[origin] = item; + } else if ((hash[origin].url as string).length > + (item.url as string).length) { + hash[origin] = item; + } + } + let filtered = Object.values(hash); + if (filtered.length < min) { + return items; + } + return filtered; +}; + +export { + filterHttp, filterBlankTitle, filterByTailingSlash, + filterByPathname, filterByOrigin +}; diff --git a/src/background/usecases/parsers.js b/src/background/usecases/parsers.js deleted file mode 100644 index 43c8177..0000000 --- a/src/background/usecases/parsers.js +++ /dev/null @@ -1,31 +0,0 @@ -const mustNumber = (v) => { - let num = Number(v); - if (isNaN(num)) { - throw new Error('Not number: ' + v); - } - return num; -}; - -const parseSetOption = (word, types) => { - let [key, value] = word.split('='); - if (value === undefined) { - value = !key.startsWith('no'); - key = value ? key : key.slice(2); - } - let type = types[key]; - if (!type) { - throw new Error('Unknown property: ' + key); - } - if (type === 'boolean' && typeof value !== 'boolean' || - type !== 'boolean' && typeof value === 'boolean') { - throw new Error('Invalid argument: ' + word); - } - - switch (type) { - case 'string': return [key, value]; - case 'number': return [key, mustNumber(value)]; - case 'boolean': return [key, value]; - } -}; - -export { parseSetOption }; diff --git a/src/background/usecases/parsers.ts b/src/background/usecases/parsers.ts new file mode 100644 index 0000000..6135fd8 --- /dev/null +++ b/src/background/usecases/parsers.ts @@ -0,0 +1,36 @@ +import * as PropertyDefs from '../../shared//property-defs'; + +const mustNumber = (v: any): number => { + let num = Number(v); + if (isNaN(num)) { + throw new Error('Not number: ' + v); + } + return num; +}; + +const parseSetOption = ( + args: string, +): any[] => { + let [key, value]: any[] = args.split('='); + if (value === undefined) { + value = !key.startsWith('no'); + key = value ? key : key.slice(2); + } + let def = PropertyDefs.defs.find(d => d.name === key); + if (!def) { + throw new Error('Unknown property: ' + key); + } + if (def.type === 'boolean' && typeof value !== 'boolean' || + def.type !== 'boolean' && typeof value === 'boolean') { + throw new Error('Invalid argument: ' + args); + } + + switch (def.type) { + case 'string': return [key, value]; + case 'number': return [key, mustNumber(value)]; + case 'boolean': return [key, value]; + } + throw new Error('Unknown property type: ' + def.type); +}; + +export { parseSetOption }; diff --git a/src/console/actions/console.js b/src/console/actions/console.ts index 3713a76..b1494b0 100644 --- a/src/console/actions/console.js +++ b/src/console/actions/console.ts @@ -1,40 +1,40 @@ -import messages from 'shared/messages'; -import actions from 'console/actions'; +import * as 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<actions.ConsoleAction> => { 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<actions.ConsoleAction> => { 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.js b/src/console/actions/index.js deleted file mode 100644 index b394179..0000000 --- a/src/console/actions/index.js +++ /dev/null @@ -1,13 +0,0 @@ -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', -}; diff --git a/src/console/actions/index.ts b/src/console/actions/index.ts new file mode 100644 index 0000000..3770496 --- /dev/null +++ b/src/console/actions/index.ts @@ -0,0 +1,63 @@ +// 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.jsx b/src/console/components/Console.tsx index 5427e43..3274047 100644 --- a/src/console/components/Console.jsx +++ b/src/console/components/Console.tsx @@ -1,26 +1,40 @@ 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'; import * as consoleActions from '../../console/actions/console'; +import { State as AppState } from '../reducers'; const COMPLETION_MAX_ITEMS = 33; -class Console extends React.Component { +type StateProps = ReturnType<typeof mapStateToProps>; +interface DispatchProps { + dispatch: (action: any) => void, +} +type Props = StateProps & DispatchProps + +class Console extends React.Component<Props> { + private input: React.RefObject<Input>; + + constructor(props: Props) { + super(props); + + this.input = React.createRef(); + } + onBlur() { if (this.props.mode === 'command' || this.props.mode === 'find') { return this.props.dispatch(consoleActions.hideCommand()); } } - doEnter(e) { + doEnter(e: React.KeyboardEvent<HTMLInputElement>) { e.stopPropagation(); e.preventDefault(); - let value = e.target.value; + let value = (e.target as HTMLInputElement).value; if (this.props.mode === 'command') { return this.props.dispatch(consoleActions.enterCommand(value)); } else if (this.props.mode === 'find') { @@ -28,28 +42,25 @@ class Console extends React.Component { } } - selectNext(e) { + selectNext(e: React.KeyboardEvent<HTMLInputElement>) { this.props.dispatch(consoleActions.completionNext()); e.stopPropagation(); e.preventDefault(); } - selectPrev(e) { + selectPrev(e: React.KeyboardEvent<HTMLInputElement>) { this.props.dispatch(consoleActions.completionPrev()); e.stopPropagation(); e.preventDefault(); } - onKeyDown(e) { - if (e.keyCode === KeyboardEvent.DOM_VK_ESCAPE && e.ctrlKey) { - this.props.dispatch(consoleActions.hideCommand()); - } - switch (e.keyCode) { - case KeyboardEvent.DOM_VK_ESCAPE: + onKeyDown(e: React.KeyboardEvent<HTMLInputElement>) { + switch (e.key) { + case 'Escape': return this.props.dispatch(consoleActions.hideCommand()); - case KeyboardEvent.DOM_VK_RETURN: + case 'Enter': return this.doEnter(e); - case KeyboardEvent.DOM_VK_TAB: + case 'Tab': if (e.shiftKey) { this.props.dispatch(consoleActions.completionPrev()); } else { @@ -58,22 +69,22 @@ class Console extends React.Component { e.stopPropagation(); e.preventDefault(); break; - case KeyboardEvent.DOM_VK_OPEN_BRACKET: + case '[': if (e.ctrlKey) { return this.props.dispatch(consoleActions.hideCommand()); } break; - case KeyboardEvent.DOM_VK_M: + case 'm': if (e.ctrlKey) { return this.doEnter(e); } break; - case KeyboardEvent.DOM_VK_N: + case 'n': if (e.ctrlKey) { this.selectNext(e); } break; - case KeyboardEvent.DOM_VK_P: + case 'p': if (e.ctrlKey) { this.selectPrev(e); } @@ -81,7 +92,7 @@ class Console extends React.Component { } } - onChange(e) { + onChange(e: React.ChangeEvent<HTMLInputElement>) { let text = e.target.value; this.props.dispatch(consoleActions.setConsoleText(text)); if (this.props.mode === 'command') { @@ -90,10 +101,7 @@ class Console extends React.Component { } - componentDidUpdate(prevProps) { - if (!this.input) { - return; - } + componentDidUpdate(prevProps: Props) { if (prevProps.mode !== 'command' && this.props.mode === 'command') { this.props.dispatch( consoleActions.getCompletions(this.props.consoleText)); @@ -114,7 +122,7 @@ class Console extends React.Component { select={this.props.select} /> <Input - ref={(c) => { this.input = c; }} + ref={this.input} mode={this.props.mode} onBlur={this.onBlur.bind(this)} onKeyDown={this.onKeyDown.bind(this)} @@ -134,16 +142,14 @@ class Console extends React.Component { focus() { window.focus(); - this.input.focus(); + if (this.input.current) { + this.input.current.focus(); + } } } -Console.propTypes = { - mode: PropTypes.string, - consoleText: PropTypes.string, - messageText: PropTypes.string, - children: PropTypes.string, -}; +const mapStateToProps = (state: AppState) => ({ ...state }); -const mapStateToProps = state => state; -export default connect(mapStateToProps)(Console); +export default connect( + mapStateToProps, +)(Console); diff --git a/src/console/components/console/Completion.jsx b/src/console/components/console/Completion.tsx index 5477cb6..169a39c 100644 --- a/src/console/components/console/Completion.jsx +++ 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<Props, State> { + 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.jsx b/src/console/components/console/CompletionItem.tsx index 3dc552b..1cbf3de 100644 --- a/src/console/components/console/CompletionItem.jsx +++ 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.jsx b/src/console/components/console/CompletionTitle.tsx index 4fcba3f..2543619 100644 --- a/src/console/components/console/CompletionTitle.jsx +++ 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 <li className='vimvixen-console-completion-title'> {props.title} </li>; }; -CompletionTitle.propTypes = { - title: PropTypes.string, -}; - export default CompletionTitle; diff --git a/src/console/components/console/Input.jsx b/src/console/components/console/Input.tsx index cbd3348..54ea251 100644 --- a/src/console/components/console/Input.jsx +++ 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<HTMLInputElement>) => void; + onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void; + onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; +} + +class Input extends React.Component<Props> { + private input: React.RefObject<HTMLInputElement>; + + constructor(props: Props) { + super(props); + + this.input = React.createRef(); + } + focus() { - this.input.focus(); + if (this.input.current) { + this.input.current.focus(); + } } render() { @@ -21,7 +38,7 @@ class Input extends React.Component { </i> <input className='vimvixen-console-command-input' - ref={(c) => { this.input = c; }} + ref={this.input} onBlur={this.props.onBlur} onKeyDown={this.props.onKeyDown} onChange={this.props.onChange} @@ -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.jsx b/src/console/components/console/Message.tsx index dd96248..9fa2788 100644 --- a/src/console/components/console/Message.jsx +++ 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) => { </p> ); } -}; - -Message.propTypes = { - children: PropTypes.string, + return null; }; export default Message; diff --git a/src/console/index.jsx b/src/console/index.tsx index 3190a9a..b655154 100644 --- a/src/console/index.jsx +++ b/src/console/index.tsx @@ -1,8 +1,8 @@ -import messages from 'shared/messages'; -import reducers from 'console/reducers'; +import * as 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,21 +22,22 @@ window.addEventListener('load', () => { wrapper); }); -const onMessage = (message) => { - switch (message.type) { +const onMessage = (message: any): any => { + 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()); } }; 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.js b/src/console/reducers/index.ts index 614a72f..b6be483 100644 --- a/src/console/reducers/index.js +++ b/src/console/reducers/index.ts @@ -1,4 +1,14 @@ -import actions from 'console/actions'; +import * as actions from '../actions'; + +export 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/src/content/Mark.ts b/src/content/Mark.ts new file mode 100644 index 0000000..f1282fc --- /dev/null +++ b/src/content/Mark.ts @@ -0,0 +1,6 @@ +export default interface Mark { + x: number; + y: number; + // eslint-disable-next-line semi +} + 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.js b/src/content/actions/addon.js deleted file mode 100644 index b30cf16..0000000 --- a/src/content/actions/addon.js +++ /dev/null @@ -1,19 +0,0 @@ -import messages from 'shared/messages'; -import actions from 'content/actions'; - -const enable = () => setEnabled(true); - -const disable = () => setEnabled(false); - -const setEnabled = async(enabled) => { - await browser.runtime.sendMessage({ - type: messages.ADDON_ENABLED_RESPONSE, - enabled, - }); - return { - type: actions.ADDON_SET_ENABLED, - enabled, - }; -}; - -export { enable, disable, setEnabled }; diff --git a/src/content/actions/addon.ts b/src/content/actions/addon.ts new file mode 100644 index 0000000..8dedae0 --- /dev/null +++ b/src/content/actions/addon.ts @@ -0,0 +1,19 @@ +import * as messages from '../../shared/messages'; +import * as actions from './index'; + +const enable = (): Promise<actions.AddonAction> => setEnabled(true); + +const disable = (): Promise<actions.AddonAction> => setEnabled(false); + +const setEnabled = async(enabled: boolean): Promise<actions.AddonAction> => { + await browser.runtime.sendMessage({ + type: messages.ADDON_ENABLED_RESPONSE, + enabled, + }); + return { + type: actions.ADDON_SET_ENABLED, + enabled, + }; +}; + +export { enable, disable, setEnabled }; diff --git a/src/content/actions/find.js b/src/content/actions/find.js deleted file mode 100644 index e08d7e5..0000000 --- a/src/content/actions/find.js +++ /dev/null @@ -1,68 +0,0 @@ -// -// window.find(aString, aCaseSensitive, aBackwards, aWrapAround, -// aWholeWord, aSearchInFrames); -// -// NOTE: window.find is not standard API -// https://developer.mozilla.org/en-US/docs/Web/API/Window/find - -import messages from 'shared/messages'; -import actions from 'content/actions'; -import * as consoleFrames from '../console-frames'; - -const find = (string, backwards) => { - 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); - if (found) { - return found; - } - window.getSelection().removeAllRanges(); - return window.find(string, caseSensitive, backwards, wrapScan); -}; - -const findNext = async(currentKeyword, reset, backwards) => { - if (reset) { - window.getSelection().removeAllRanges(); - } - - let keyword = currentKeyword; - if (currentKeyword) { - browser.runtime.sendMessage({ - type: messages.FIND_SET_KEYWORD, - keyword: currentKeyword, - }); - } else { - keyword = await browser.runtime.sendMessage({ - type: messages.FIND_GET_KEYWORD, - }); - } - if (!keyword) { - return consoleFrames.postError('No previous search keywords'); - } - let found = find(keyword, backwards); - if (found) { - consoleFrames.postInfo('Pattern found: ' + keyword); - } else { - consoleFrames.postError('Pattern not found: ' + keyword); - } - - return { - type: actions.FIND_SET_KEYWORD, - keyword, - found, - }; -}; - -const next = (currentKeyword, reset) => { - return findNext(currentKeyword, reset, false); -}; - -const prev = (currentKeyword, reset) => { - return findNext(currentKeyword, reset, true); -}; - -export { next, prev }; diff --git a/src/content/actions/find.ts b/src/content/actions/find.ts new file mode 100644 index 0000000..53e03ae --- /dev/null +++ b/src/content/actions/find.ts @@ -0,0 +1,100 @@ +// +// window.find(aString, aCaseSensitive, aBackwards, aWrapAround, +// aWholeWord, aSearchInFrames); +// +// NOTE: window.find is not standard API +// https://developer.mozilla.org/en-US/docs/Web/API/Window/find + +import * as messages from '../../shared/messages'; +import * as actions from './index'; +import * as consoleFrames from '../console-frames'; + +interface MyWindow extends Window { + find( + aString: string, + aCaseSensitive?: boolean, + aBackwards?: boolean, + aWrapAround?: boolean, + aWholeWord?: boolean, + aSearchInFrames?: boolean, + aShowDialog?: boolean): boolean; +} + +// eslint-disable-next-line no-var, vars-on-top, init-declarations +declare var window: MyWindow; + +const find = (str: string, backwards: boolean): boolean => { + let caseSensitive = false; + let wrapScan = true; + + + // NOTE: aWholeWord dows not implemented, and aSearchInFrames does not work + // because of same origin policy + + // eslint-disable-next-line no-extra-parens + let found = window.find(str, caseSensitive, backwards, wrapScan); + if (found) { + return found; + } + let sel = window.getSelection(); + if (sel) { + sel.removeAllRanges(); + } + + // eslint-disable-next-line no-extra-parens + return window.find(str, caseSensitive, backwards, wrapScan); +}; + +// eslint-disable-next-line max-statements +const findNext = async( + currentKeyword: string, reset: boolean, backwards: boolean, +): Promise<actions.FindAction> => { + if (reset) { + let sel = window.getSelection(); + if (sel) { + sel.removeAllRanges(); + } + } + + let keyword = currentKeyword; + if (currentKeyword) { + browser.runtime.sendMessage({ + type: messages.FIND_SET_KEYWORD, + keyword: currentKeyword, + }); + } else { + keyword = await browser.runtime.sendMessage({ + type: messages.FIND_GET_KEYWORD, + }); + } + if (!keyword) { + await consoleFrames.postError('No previous search keywords'); + return { type: actions.NOOP }; + } + let found = find(keyword, backwards); + if (found) { + consoleFrames.postInfo('Pattern found: ' + keyword); + } else { + consoleFrames.postError('Pattern not found: ' + keyword); + } + + return { + type: actions.FIND_SET_KEYWORD, + keyword, + found, + }; +}; + +const next = ( + currentKeyword: string, reset: boolean, +): Promise<actions.FindAction> => { + return findNext(currentKeyword, reset, false); +}; + +const prev = ( + currentKeyword: string, reset: boolean, +): Promise<actions.FindAction> => { + return findNext(currentKeyword, reset, true); +}; + +export { next, prev }; diff --git a/src/content/actions/follow-controller.js b/src/content/actions/follow-controller.ts index 006b248..115b3b6 100644 --- a/src/content/actions/follow-controller.js +++ 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.js b/src/content/actions/index.js deleted file mode 100644 index 0a16fdf..0000000 --- a/src/content/actions/index.js +++ /dev/null @@ -1,31 +0,0 @@ -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', -}; diff --git a/src/content/actions/index.ts b/src/content/actions/index.ts new file mode 100644 index 0000000..8aa9c23 --- /dev/null +++ b/src/content/actions/index.ts @@ -0,0 +1,122 @@ +import Redux from 'redux'; +import Settings from '../../shared/Settings'; +import * as keyUtils from '../../shared/utils/keys'; + +// Enable/disable +export const ADDON_SET_ENABLED = 'addon.set.enabled'; + +// Find +export const FIND_SET_KEYWORD = 'find.set.keyword'; + +// Settings +export const SETTING_SET = 'setting.set'; + +// User input +export const INPUT_KEY_PRESS = 'input.key.press'; +export const INPUT_CLEAR_KEYS = 'input.clear.keys'; + +// Completion +export const COMPLETION_SET_ITEMS = 'completion.set.items'; +export const COMPLETION_SELECT_NEXT = 'completions.select.next'; +export const COMPLETION_SELECT_PREV = 'completions.select.prev'; + +// Follow +export const FOLLOW_CONTROLLER_ENABLE = 'follow.controller.enable'; +export const FOLLOW_CONTROLLER_DISABLE = 'follow.controller.disable'; +export const FOLLOW_CONTROLLER_KEY_PRESS = 'follow.controller.key.press'; +export const FOLLOW_CONTROLLER_BACKSPACE = 'follow.controller.backspace'; + +// Mark +export const MARK_START_SET = 'mark.start.set'; +export const MARK_START_JUMP = 'mark.start.jump'; +export const MARK_CANCEL = 'mark.cancel'; +export const MARK_SET_LOCAL = 'mark.set.local'; + +export const NOOP = 'noop'; + +export interface AddonSetEnabledAction extends Redux.Action { + type: typeof ADDON_SET_ENABLED; + enabled: boolean; +} + +export interface FindSetKeywordAction extends Redux.Action { + type: typeof FIND_SET_KEYWORD; + keyword: string; + found: boolean; +} + +export interface SettingSetAction extends Redux.Action { + type: typeof SETTING_SET; + settings: Settings, +} + +export interface InputKeyPressAction extends Redux.Action { + type: typeof INPUT_KEY_PRESS; + key: keyUtils.Key; +} + +export interface InputClearKeysAction extends Redux.Action { + type: typeof INPUT_CLEAR_KEYS; +} + +export interface FollowControllerEnableAction extends Redux.Action { + type: typeof FOLLOW_CONTROLLER_ENABLE; + newTab: boolean; + background: boolean; +} + +export interface FollowControllerDisableAction extends Redux.Action { + type: typeof FOLLOW_CONTROLLER_DISABLE; +} + +export interface FollowControllerKeyPressAction extends Redux.Action { + type: typeof FOLLOW_CONTROLLER_KEY_PRESS; + key: string; +} + +export interface FollowControllerBackspaceAction extends Redux.Action { + type: typeof FOLLOW_CONTROLLER_BACKSPACE; +} + +export interface MarkStartSetAction extends Redux.Action { + type: typeof MARK_START_SET; +} + +export interface MarkStartJumpAction extends Redux.Action { + type: typeof MARK_START_JUMP; +} + +export interface MarkCancelAction extends Redux.Action { + type: typeof MARK_CANCEL; +} + +export interface MarkSetLocalAction extends Redux.Action { + type: typeof MARK_SET_LOCAL; + key: string; + x: number; + y: number; +} + +export interface NoopAction extends Redux.Action { + type: typeof NOOP; +} + +export type AddonAction = AddonSetEnabledAction; +export type FindAction = FindSetKeywordAction | NoopAction; +export type SettingAction = SettingSetAction; +export type InputAction = InputKeyPressAction | InputClearKeysAction; +export type FollowAction = + FollowControllerEnableAction | FollowControllerDisableAction | + FollowControllerKeyPressAction | FollowControllerBackspaceAction; +export type MarkAction = + MarkStartSetAction | MarkStartJumpAction | + MarkCancelAction | MarkSetLocalAction | NoopAction; + +export type Action = + AddonAction | + FindAction | + SettingAction | + InputAction | + FollowAction | + MarkAction | + NoopAction; diff --git a/src/content/actions/input.js b/src/content/actions/input.js deleted file mode 100644 index 465a486..0000000 --- a/src/content/actions/input.js +++ /dev/null @@ -1,16 +0,0 @@ -import actions from 'content/actions'; - -const keyPress = (key) => { - return { - type: actions.INPUT_KEY_PRESS, - key, - }; -}; - -const clearKeys = () => { - return { - type: actions.INPUT_CLEAR_KEYS - }; -}; - -export { keyPress, clearKeys }; diff --git a/src/content/actions/input.ts b/src/content/actions/input.ts new file mode 100644 index 0000000..1df6452 --- /dev/null +++ b/src/content/actions/input.ts @@ -0,0 +1,17 @@ +import * as actions from './index'; +import * as keyUtils from '../../shared/utils/keys'; + +const keyPress = (key: keyUtils.Key): actions.InputAction => { + return { + type: actions.INPUT_KEY_PRESS, + key, + }; +}; + +const clearKeys = (): actions.InputAction => { + return { + type: actions.INPUT_CLEAR_KEYS + }; +}; + +export { keyPress, clearKeys }; diff --git a/src/content/actions/mark.js b/src/content/actions/mark.js deleted file mode 100644 index 712a811..0000000 --- a/src/content/actions/mark.js +++ /dev/null @@ -1,46 +0,0 @@ -import actions from 'content/actions'; -import messages from 'shared/messages'; - -const startSet = () => { - return { type: actions.MARK_START_SET }; -}; - -const startJump = () => { - return { type: actions.MARK_START_JUMP }; -}; - -const cancel = () => { - return { type: actions.MARK_CANCEL }; -}; - -const setLocal = (key, x, y) => { - return { - type: actions.MARK_SET_LOCAL, - key, - x, - y, - }; -}; - -const setGlobal = (key, x, y) => { - browser.runtime.sendMessage({ - type: messages.MARK_SET_GLOBAL, - key, - x, - y, - }); - return { type: '' }; -}; - -const jumpGlobal = (key) => { - browser.runtime.sendMessage({ - type: messages.MARK_JUMP_GLOBAL, - key, - }); - return { type: '' }; -}; - -export { - startSet, startJump, cancel, setLocal, - setGlobal, jumpGlobal, -}; diff --git a/src/content/actions/mark.ts b/src/content/actions/mark.ts new file mode 100644 index 0000000..5eb9554 --- /dev/null +++ b/src/content/actions/mark.ts @@ -0,0 +1,46 @@ +import * as actions from './index'; +import * as messages from '../../shared/messages'; + +const startSet = (): actions.MarkAction => { + return { type: actions.MARK_START_SET }; +}; + +const startJump = (): actions.MarkAction => { + return { type: actions.MARK_START_JUMP }; +}; + +const cancel = (): actions.MarkAction => { + return { type: actions.MARK_CANCEL }; +}; + +const setLocal = (key: string, x: number, y: number): actions.MarkAction => { + return { + type: actions.MARK_SET_LOCAL, + key, + x, + y, + }; +}; + +const setGlobal = (key: string, x: number, y: number): actions.MarkAction => { + browser.runtime.sendMessage({ + type: messages.MARK_SET_GLOBAL, + key, + x, + y, + }); + return { type: actions.NOOP }; +}; + +const jumpGlobal = (key: string): actions.MarkAction => { + browser.runtime.sendMessage({ + type: messages.MARK_JUMP_GLOBAL, + key, + }); + return { type: actions.NOOP }; +}; + +export { + startSet, startJump, cancel, setLocal, + setGlobal, jumpGlobal, +}; diff --git a/src/content/actions/operation.js b/src/content/actions/operation.ts index ed9b2cf..41e080b 100644 --- a/src/content/actions/operation.js +++ b/src/content/actions/operation.ts @@ -1,18 +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'; // eslint-disable-next-line complexity, max-lines-per-function -const exec = (operation, settings, addonEnabled) => { - let smoothscroll = settings.properties.smoothscroll || - properties.defaults.smoothscroll; +const exec = ( + operation: operations.Operation, + settings: any, + addonEnabled: boolean, +): Promise<actions.Action> | actions.Action => { + let smoothscroll = settings.properties.smoothscroll; switch (operation.type) { case operations.ADDON_ENABLE: return addonActions.enable(); @@ -98,7 +101,7 @@ const exec = (operation, settings, addonEnabled) => { operation, }); } - return { type: '' }; + return { type: actions.NOOP }; }; export { exec }; diff --git a/src/content/actions/setting.js b/src/content/actions/setting.js deleted file mode 100644 index 1c15dd7..0000000 --- a/src/content/actions/setting.js +++ /dev/null @@ -1,37 +0,0 @@ -import actions from 'content/actions'; -import * as keyUtils from 'shared/utils/keys'; -import operations from 'shared/operations'; -import messages from 'shared/messages'; - -const reservedKeymaps = { - '<Esc>': { type: operations.CANCEL }, - '<C-[>': { type: operations.CANCEL }, -}; - -const set = (value) => { - let entries = []; - if (value.keymaps) { - let keymaps = { ...value.keymaps, ...reservedKeymaps }; - entries = Object.entries(keymaps).map((entry) => { - return [ - keyUtils.fromMapKeys(entry[0]), - entry[1], - ]; - }); - } - - return { - type: actions.SETTING_SET, - value: { ...value, - keymaps: entries, } - }; -}; - -const load = async() => { - let settings = await browser.runtime.sendMessage({ - type: messages.SETTINGS_QUERY, - }); - return set(settings); -}; - -export { set, load }; diff --git a/src/content/actions/setting.ts b/src/content/actions/setting.ts new file mode 100644 index 0000000..92f8559 --- /dev/null +++ b/src/content/actions/setting.ts @@ -0,0 +1,28 @@ +import * as actions from './index'; +import * as operations from '../../shared/operations'; +import * as messages from '../../shared/messages'; +import Settings, { Keymaps } from '../../shared/Settings'; + +const reservedKeymaps: Keymaps = { + '<Esc>': { type: operations.CANCEL }, + '<C-[>': { type: operations.CANCEL }, +}; + +const set = (settings: Settings): actions.SettingAction => { + return { + type: actions.SETTING_SET, + settings: { + ...settings, + keymaps: { ...settings.keymaps, ...reservedKeymaps }, + } + }; +}; + +const load = async(): Promise<actions.SettingAction> => { + let settings = await browser.runtime.sendMessage({ + type: messages.SETTINGS_QUERY, + }); + return set(settings); +}; + +export { set, load }; diff --git a/src/content/components/common/follow.js b/src/content/components/common/follow.ts index 63ce603..67f2dd9 100644 --- a/src/content/components/common/follow.js +++ 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.js b/src/content/components/common/hint.ts index 1472587..2fcbb0f 100644 --- a/src/content/components/common/hint.js +++ 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.js b/src/content/components/common/index.js deleted file mode 100644 index bcab4fa..0000000 --- a/src/content/components/common/index.js +++ /dev/null @@ -1,55 +0,0 @@ -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 addonActions from '../../actions/addon'; -import * as blacklists from 'shared/blacklists'; - -export default class Common { - constructor(win, store) { - const input = new InputComponent(win.document.body, store); - const follow = new FollowComponent(win, store); - 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)); - - this.win = win; - this.store = store; - this.prevEnabled = undefined; - this.prevBlacklist = undefined; - - this.reloadSettings(); - - messages.onMessage(this.onMessage.bind(this)); - } - - onMessage(message) { - let { enabled } = this.store.getState().addon; - switch (message.type) { - case messages.SETTINGS_CHANGED: - return this.reloadSettings(); - case messages.ADDON_TOGGLE_ENABLED: - this.store.dispatch(addonActions.setEnabled(!enabled)); - } - } - - reloadSettings() { - try { - this.store.dispatch(settingActions.load()).then(({ value: settings }) => { - 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); - setTimeout(() => this.reloadSettings(), 500); - } - } -} diff --git a/src/content/components/common/index.ts b/src/content/components/common/index.ts new file mode 100644 index 0000000..5b097b6 --- /dev/null +++ b/src/content/components/common/index.ts @@ -0,0 +1,61 @@ +import InputComponent from './input'; +import FollowComponent from './follow'; +import MarkComponent from './mark'; +import KeymapperComponent from './keymapper'; +import * as settingActions from '../../actions/setting'; +import * as messages from '../../../shared/messages'; +import MessageListener from '../../MessageListener'; +import * as addonActions from '../../actions/addon'; +import * as blacklists from '../../../shared/blacklists'; +import * as keys from '../../../shared/utils/keys'; +import * as actions from '../../actions'; + +export default class Common { + private win: Window; + + private store: any; + + constructor(win: Window, store: any) { + const input = new InputComponent(win.document.body); + const follow = new FollowComponent(win); + const mark = new MarkComponent(store); + const keymapper = new KeymapperComponent(store); + + input.onKey((key: keys.Key) => follow.key(key)); + input.onKey((key: keys.Key) => mark.key(key)); + input.onKey((key: keys.Key) => keymapper.key(key)); + + this.win = win; + this.store = store; + + this.reloadSettings(); + + new MessageListener().onBackgroundMessage(this.onMessage.bind(this)); + } + + onMessage(message: messages.Message) { + let { enabled } = this.store.getState().addon; + switch (message.type) { + case messages.SETTINGS_CHANGED: + return this.reloadSettings(); + case messages.ADDON_TOGGLE_ENABLED: + this.store.dispatch(addonActions.setEnabled(!enabled)); + } + } + + reloadSettings() { + try { + this.store.dispatch(settingActions.load()) + .then((action: actions.SettingAction) => { + let enabled = !blacklists.includes( + action.settings.blacklist, this.win.location.href + ); + this.store.dispatch(addonActions.setEnabled(enabled)); + }); + } catch (e) { + // Sometime sendMessage fails when background script is not ready. + console.warn(e); + setTimeout(() => this.reloadSettings(), 500); + } + } +} diff --git a/src/content/components/common/input.js b/src/content/components/common/input.ts index eefaf10..1fe34c9 100644 --- a/src/content/components/common/input.js +++ 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; } @@ -52,7 +61,6 @@ export default class InputComponent { } let key = keys.fromKeyboardEvent(e); - for (let listener of this.onKeyListeners) { let stop = listener(key); if (stop) { @@ -63,13 +71,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.js b/src/content/components/common/keymapper.ts index ec0d093..c94bae0 100644 --- a/src/content/components/common/keymapper.js +++ b/src/content/components/common/keymapper.ts @@ -1,9 +1,12 @@ -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) => { +const mapStartsWith = ( + mapping: keyUtils.Key[], + keys: keyUtils.Key[], +): boolean => { if (mapping.length < keys.length) { return false; } @@ -16,26 +19,33 @@ const mapStartsWith = (mapping, keys) => { }; export default class KeymapperComponent { - constructor(store) { + private store: any; + + constructor(store: any) { this.store = store; } // eslint-disable-next-line max-statements - key(key) { + key(key: keyUtils.Key): boolean { this.store.dispatch(inputActions.keyPress(key)); let state = this.store.getState(); let input = state.input; - let keymaps = new Map(state.setting.keymaps); + let keymaps = new Map<keyUtils.Key[], operations.Operation>( + state.setting.keymaps.map( + (e: {key: keyUtils.Key[], op: operations.Operation}) => [e.key, e.op], + ) + ); - let matched = Array.from(keymaps.keys()).filter((mapping) => { - return mapStartsWith(mapping, input.keys); - }); + let matched = Array.from(keymaps.keys()).filter( + (mapping: keyUtils.Key[]) => { + return mapStartsWith(mapping, input.keys); + }); if (!state.addon.enabled) { // available keymaps are only ADDON_ENABLE and ADDON_TOGGLE_ENABLED if // the addon disabled matched = matched.filter((keys) => { - let type = keymaps.get(keys).type; + let type = (keymaps.get(keys) as operations.Operation).type; return type === operations.ADDON_ENABLE || type === operations.ADDON_TOGGLE_ENABLED; }); @@ -47,7 +57,7 @@ export default class KeymapperComponent { matched.length === 1 && input.keys.length < matched[0].length) { return true; } - let operation = keymaps.get(matched[0]); + let operation = keymaps.get(matched[0]) as operations.Operation; let act = operationActions.exec( operation, state.setting, state.addon.enabled ); diff --git a/src/content/components/common/mark.js b/src/content/components/common/mark.js deleted file mode 100644 index 0f838a9..0000000 --- a/src/content/components/common/mark.js +++ /dev/null @@ -1,74 +0,0 @@ -import * as markActions from 'content/actions/mark'; -import * as scrolls from 'content/scrolls'; -import * as consoleFrames from 'content/console-frames'; -import * as properties from 'shared/settings/properties'; - -const cancelKey = (key) => { - return key.key === 'Esc' || key.key === '[' && key.ctrlKey; -}; - -const globalKey = (key) => { - return (/^[A-Z0-9]$/).test(key); -}; - -export default class MarkComponent { - constructor(body, store) { - this.body = body; - this.store = store; - } - - // eslint-disable-next-line max-statements - key(key) { - let { mark: markStage, setting } = this.store.getState(); - let smoothscroll = setting.properties.smoothscroll || - properties.defaults.smoothscroll; - - if (!markStage.setMode && !markStage.jumpMode) { - return false; - } - - if (cancelKey(key)) { - this.store.dispatch(markActions.cancel()); - return true; - } - - if (key.ctrlKey || key.metaKey || key.altKey) { - consoleFrames.postError('Unknown mark'); - } else if (globalKey(key.key) && markStage.setMode) { - this.doSetGlobal(key); - } else if (globalKey(key.key) && markStage.jumpMode) { - this.doJumpGlobal(key); - } else if (markStage.setMode) { - this.doSet(key); - } else if (markStage.jumpMode) { - this.doJump(markStage.marks, key, smoothscroll); - } - - this.store.dispatch(markActions.cancel()); - return true; - } - - doSet(key) { - let { x, y } = scrolls.getScroll(); - this.store.dispatch(markActions.setLocal(key.key, x, y)); - } - - doJump(marks, key, smoothscroll) { - if (!marks[key.key]) { - consoleFrames.postError('Mark is not set'); - return; - } - - let { x, y } = marks[key.key]; - scrolls.scrollTo(x, y, smoothscroll); - } - - doSetGlobal(key) { - let { x, y } = scrolls.getScroll(); - this.store.dispatch(markActions.setGlobal(key.key, x, y)); - } - - doJumpGlobal(key) { - this.store.dispatch(markActions.jumpGlobal(key.key)); - } -} diff --git a/src/content/components/common/mark.ts b/src/content/components/common/mark.ts new file mode 100644 index 0000000..1237385 --- /dev/null +++ b/src/content/components/common/mark.ts @@ -0,0 +1,79 @@ +import * as markActions from '../../actions/mark'; +import * as scrolls from '../..//scrolls'; +import * as consoleFrames from '../..//console-frames'; +import * as keyUtils from '../../../shared/utils/keys'; +import Mark from '../../Mark'; + +const cancelKey = (key: keyUtils.Key): boolean => { + return key.key === 'Esc' || key.key === '[' && Boolean(key.ctrlKey); +}; + +const globalKey = (key: string): boolean => { + return (/^[A-Z0-9]$/).test(key); +}; + +export default class MarkComponent { + private store: any; + + constructor(store: any) { + this.store = store; + } + + // eslint-disable-next-line max-statements + key(key: keyUtils.Key) { + let { mark: markState, setting } = this.store.getState(); + let smoothscroll = setting.properties.smoothscroll; + + if (!markState.setMode && !markState.jumpMode) { + return false; + } + + if (cancelKey(key)) { + this.store.dispatch(markActions.cancel()); + return true; + } + + if (key.ctrlKey || key.metaKey || key.altKey) { + consoleFrames.postError('Unknown mark'); + } else if (globalKey(key.key) && markState.setMode) { + this.doSetGlobal(key); + } else if (globalKey(key.key) && markState.jumpMode) { + this.doJumpGlobal(key); + } else if (markState.setMode) { + this.doSet(key); + } else if (markState.jumpMode) { + this.doJump(markState.marks, key, smoothscroll); + } + + this.store.dispatch(markActions.cancel()); + return true; + } + + doSet(key: keyUtils.Key) { + let { x, y } = scrolls.getScroll(); + this.store.dispatch(markActions.setLocal(key.key, x, y)); + } + + doJump( + marks: { [key: string]: Mark }, + key: keyUtils.Key, + smoothscroll: boolean, + ) { + if (!marks[key.key]) { + consoleFrames.postError('Mark is not set'); + return; + } + + let { x, y } = marks[key.key]; + scrolls.scrollTo(x, y, smoothscroll); + } + + doSetGlobal(key: keyUtils.Key) { + let { x, y } = scrolls.getScroll(); + this.store.dispatch(markActions.setGlobal(key.key, x, y)); + } + + doJumpGlobal(key: keyUtils.Key) { + this.store.dispatch(markActions.jumpGlobal(key.key)); + } +} diff --git a/src/content/components/frame-content.js b/src/content/components/frame-content.ts index ca999ba..ca999ba 100644 --- a/src/content/components/frame-content.js +++ b/src/content/components/frame-content.ts diff --git a/src/content/components/top-content/find.js b/src/content/components/top-content/find.js deleted file mode 100644 index 4d46d79..0000000 --- a/src/content/components/top-content/find.js +++ /dev/null @@ -1,41 +0,0 @@ -import * as findActions from 'content/actions/find'; -import messages from 'shared/messages'; - -export default class FindComponent { - constructor(win, store) { - this.win = win; - this.store = store; - - messages.onMessage(this.onMessage.bind(this)); - } - - onMessage(message) { - switch (message.type) { - case messages.CONSOLE_ENTER_FIND: - return this.start(message.text); - case messages.FIND_NEXT: - return this.next(); - case messages.FIND_PREV: - return this.prev(); - } - } - - start(text) { - 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(text, true)); - } - - next() { - let state = this.store.getState().find; - return this.store.dispatch(findActions.next(state.keyword, false)); - } - - prev() { - let state = this.store.getState().find; - return this.store.dispatch(findActions.prev(state.keyword, false)); - } -} diff --git a/src/content/components/top-content/find.ts b/src/content/components/top-content/find.ts new file mode 100644 index 0000000..74b95bc --- /dev/null +++ b/src/content/components/top-content/find.ts @@ -0,0 +1,46 @@ +import * as findActions from '../../actions/find'; +import * as messages from '../../../shared/messages'; +import MessageListener from '../../MessageListener'; + +export default class FindComponent { + private store: any; + + constructor(store: any) { + this.store = store; + + new MessageListener().onWebMessage(this.onMessage.bind(this)); + } + + onMessage(message: messages.Message) { + switch (message.type) { + case messages.CONSOLE_ENTER_FIND: + return this.start(message.text); + case messages.FIND_NEXT: + return this.next(); + case messages.FIND_PREV: + return this.prev(); + } + } + + start(text: string) { + let state = this.store.getState().find; + + if (text.length === 0) { + return this.store.dispatch( + findActions.next(state.keyword as string, true)); + } + return this.store.dispatch(findActions.next(text, true)); + } + + next() { + let state = this.store.getState().find; + return this.store.dispatch( + findActions.next(state.keyword as string, false)); + } + + prev() { + let state = this.store.getState().find; + return this.store.dispatch( + findActions.prev(state.keyword as string, false)); + } +} diff --git a/src/content/components/top-content/follow-controller.js b/src/content/components/top-content/follow-controller.ts index 7f36604..d49b22a 100644 --- a/src/content/components/top-content/follow-controller.js +++ b/src/content/components/top-content/follow-controller.ts @@ -1,30 +1,45 @@ -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'; -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 +51,7 @@ export default class FollowController { } } - update() { + update(): void { let prevState = this.state; this.state = this.store.getState().followController; @@ -49,8 +64,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 +75,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 +124,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, @@ -141,7 +161,6 @@ export default class FollowController { } hintchars() { - return this.store.getState().setting.properties.hintchars || - properties.defaults.hintchars; + return this.store.getState().setting.properties.hintchars; } } diff --git a/src/content/components/top-content/index.js b/src/content/components/top-content/index.ts index 1aaef1b..ac95ea9 100644 --- a/src/content/components/top-content/index.js +++ 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.js b/src/content/console-frames.ts index ecb5a87..bd6b835 100644 --- a/src/content/console-frames.js +++ 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<any> => { + return browser.runtime.sendMessage({ type: messages.CONSOLE_FRAME_MESSAGE, message: { type: messages.CONSOLE_SHOW_ERROR, @@ -25,8 +25,8 @@ const postError = (text) => { }); }; -const postInfo = (text) => { - browser.runtime.sendMessage({ +const postInfo = (text: string): Promise<any> => { + return browser.runtime.sendMessage({ type: messages.CONSOLE_FRAME_MESSAGE, message: { type: messages.CONSOLE_SHOW_INFO, diff --git a/src/content/focuses.js b/src/content/focuses.ts index a6f6cc8..8f53881 100644 --- a/src/content/focuses.js +++ 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.js b/src/content/hint-key-producer.ts index 14b23b6..935394e 100644 --- a/src/content/hint-key-producer.js +++ 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.js b/src/content/index.ts index 9edb712..9d791fc 100644 --- a/src/content/index.js +++ 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 @@ -17,5 +12,5 @@ if (window.self === window.top) { } let style = window.document.createElement('style'); -style.textContent = consoleFrameStyle.default; +style.textContent = consoleFrameStyle; window.document.head.appendChild(style); diff --git a/src/content/navigates.js b/src/content/navigates.ts index c9baa30..a2007a6 100644 --- a/src/content/navigates.js +++ 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, next: /^(?:next|newer)\b|\u203a|\u2192|\xbb|\u226b|>>/i, }; // Return the last element in the document matching the supplied selector // and the optional filter, or null if there are no matches. -const selectLast = (win, selector, filter) => { - let nodes = win.document.querySelectorAll(selector); +// eslint-disable-next-line func-style +function selectLast<E extends Element>( + win: Window, + selector: string, + filter?: (e: E) => boolean, +): E | null { + let nodes = Array.from( + win.document.querySelectorAll(selector) as NodeListOf<E> + ); if (filter) { - nodes = Array.from(nodes).filter(filter); + nodes = nodes.filter(filter); } - return nodes.length ? nodes[nodes.length - 1] : null; -}; +} -const historyPrev = (win) => { +const historyPrev = (win: Window): void => { win.history.back(); }; -const historyNext = (win) => { +const historyNext = (win: Window): void => { win.history.forward(); }; // Code common to linkPrev and linkNext which navigates to the specified page. -const linkRel = (win, rel) => { - let link = selectLast(win, `link[rel~=${rel}][href]`); - +const linkRel = (win: Window, rel: string): void => { + let link = selectLast<HTMLLinkElement>(win, `link[rel~=${rel}][href]`); if (link) { - win.location = link.href; + win.location.href = link.href; return; } const pattern = REL_PATTERN[rel]; - link = selectLast(win, `a[rel~=${rel}][href]`) || + let a = selectLast<HTMLAnchorElement>(win, `a[rel~=${rel}][href]`) || // `innerText` is much slower than `textContent`, but produces much better // (i.e. less unexpected) results selectLast(win, 'a[href]', lnk => pattern.test(lnk.innerText)); - if (link) { - link.click(); + if (a) { + a.click(); } }; -const linkPrev = (win) => { +const linkPrev = (win: Window): void => { linkRel(win, 'prev'); }; -const linkNext = (win) => { +const linkNext = (win: Window): void => { linkRel(win, 'next'); }; -const parent = (win) => { +const parent = (win: Window): void => { const loc = win.location; if (loc.hash !== '') { loc.hash = ''; @@ -71,8 +76,8 @@ const parent = (win) => { } }; -const root = (win) => { - win.location = win.location.origin; +const root = (win: Window): void => { + win.location.href = win.location.origin; }; export { historyPrev, historyNext, linkPrev, linkNext, parent, root }; diff --git a/src/content/reducers/addon.js b/src/content/reducers/addon.js deleted file mode 100644 index 0def55a..0000000 --- a/src/content/reducers/addon.js +++ /dev/null @@ -1,15 +0,0 @@ -import actions from 'content/actions'; - -const defaultState = { - enabled: true, -}; - -export default function reducer(state = defaultState, action = {}) { - switch (action.type) { - case actions.ADDON_SET_ENABLED: - return { ...state, - enabled: action.enabled, }; - default: - return state; - } -} diff --git a/src/content/reducers/addon.ts b/src/content/reducers/addon.ts new file mode 100644 index 0000000..2131228 --- /dev/null +++ b/src/content/reducers/addon.ts @@ -0,0 +1,22 @@ +import * as actions from '../actions'; + +export interface State { + enabled: boolean; +} + +const defaultState: State = { + enabled: true, +}; + +export default function reducer( + state: State = defaultState, + action: actions.AddonAction, +): State { + switch (action.type) { + case actions.ADDON_SET_ENABLED: + return { ...state, + enabled: action.enabled, }; + default: + return state; + } +} diff --git a/src/content/reducers/find.js b/src/content/reducers/find.js deleted file mode 100644 index 4560e2c..0000000 --- a/src/content/reducers/find.js +++ /dev/null @@ -1,17 +0,0 @@ -import actions from 'content/actions'; - -const defaultState = { - keyword: null, - found: false, -}; - -export default function reducer(state = defaultState, action = {}) { - switch (action.type) { - case actions.FIND_SET_KEYWORD: - return { ...state, - keyword: action.keyword, - found: action.found, }; - default: - return state; - } -} diff --git a/src/content/reducers/find.ts b/src/content/reducers/find.ts new file mode 100644 index 0000000..8c3e637 --- /dev/null +++ b/src/content/reducers/find.ts @@ -0,0 +1,25 @@ +import * as actions from '../actions'; + +export interface State { + keyword: string | null; + found: boolean; +} + +const defaultState: State = { + keyword: null, + found: false, +}; + +export default function reducer( + state: State = defaultState, + action: actions.FindAction, +): State { + switch (action.type) { + case actions.FIND_SET_KEYWORD: + return { ...state, + keyword: action.keyword, + found: action.found, }; + default: + return state; + } +} diff --git a/src/content/reducers/follow-controller.js b/src/content/reducers/follow-controller.ts index 5869c47..6965704 100644 --- a/src/content/reducers/follow-controller.js +++ 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.js b/src/content/reducers/index.js deleted file mode 100644 index bf612a3..0000000 --- a/src/content/reducers/index.js +++ /dev/null @@ -1,11 +0,0 @@ -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'; - -export default combineReducers({ - addon, find, setting, input, followController, mark, -}); diff --git a/src/content/reducers/index.ts b/src/content/reducers/index.ts new file mode 100644 index 0000000..fb5eb84 --- /dev/null +++ b/src/content/reducers/index.ts @@ -0,0 +1,21 @@ +import { combineReducers } from 'redux'; +import addon, { State as AddonState } from './addon'; +import find, { State as FindState } from './find'; +import setting, { State as SettingState } from './setting'; +import input, { State as InputState } from './input'; +import followController, { State as FollowControllerState } + from './follow-controller'; +import mark, { State as MarkState } from './mark'; + +export interface State { + addon: AddonState; + find: FindState; + setting: SettingState; + input: InputState; + followController: FollowControllerState; + mark: MarkState; +} + +export default combineReducers({ + addon, find, setting, input, followController, mark, +}); diff --git a/src/content/reducers/input.js b/src/content/reducers/input.js deleted file mode 100644 index 23e7dd2..0000000 --- a/src/content/reducers/input.js +++ /dev/null @@ -1,18 +0,0 @@ -import actions from 'content/actions'; - -const defaultState = { - keys: [] -}; - -export default function reducer(state = defaultState, action = {}) { - switch (action.type) { - case actions.INPUT_KEY_PRESS: - return { ...state, - keys: state.keys.concat([action.key]), }; - case actions.INPUT_CLEAR_KEYS: - return { ...state, - keys: [], }; - default: - return state; - } -} diff --git a/src/content/reducers/input.ts b/src/content/reducers/input.ts new file mode 100644 index 0000000..35b9075 --- /dev/null +++ b/src/content/reducers/input.ts @@ -0,0 +1,26 @@ +import * as actions from '../actions'; +import * as keyUtils from '../../shared/utils/keys'; + +export interface State { + keys: keyUtils.Key[], +} + +const defaultState: State = { + keys: [] +}; + +export default function reducer( + state: State = defaultState, + action: actions.InputAction, +): State { + switch (action.type) { + case actions.INPUT_KEY_PRESS: + return { ...state, + keys: state.keys.concat([action.key]), }; + case actions.INPUT_CLEAR_KEYS: + return { ...state, + keys: [], }; + default: + return state; + } +} diff --git a/src/content/reducers/mark.js b/src/content/reducers/mark.ts index 2c96cc5..7409938 100644 --- a/src/content/reducers/mark.js +++ b/src/content/reducers/mark.ts @@ -1,12 +1,22 @@ -import actions from 'content/actions'; +import Mark from '../Mark'; +import * as actions from '../actions'; -const defaultState = { +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.js b/src/content/reducers/setting.js deleted file mode 100644 index a49db6d..0000000 --- a/src/content/reducers/setting.js +++ /dev/null @@ -1,16 +0,0 @@ -import actions from 'content/actions'; - -const defaultState = { - // keymaps is and arrays of key-binding pairs, which is entries of Map - keymaps: [], -}; - -export default function reducer(state = defaultState, action = {}) { - switch (action.type) { - case actions.SETTING_SET: - return { ...action.value }; - default: - return state; - } -} - diff --git a/src/content/reducers/setting.ts b/src/content/reducers/setting.ts new file mode 100644 index 0000000..9ca1380 --- /dev/null +++ b/src/content/reducers/setting.ts @@ -0,0 +1,40 @@ +import * as actions from '../actions'; +import * as keyUtils from '../../shared/utils/keys'; +import * as operations from '../../shared/operations'; +import { Search, Properties, DefaultSetting } from '../../shared/Settings'; + +export interface State { + keymaps: { key: keyUtils.Key[], op: operations.Operation }[]; + search: Search; + properties: Properties; +} + +// defaultState does not refer due to the state is load from +// background on load. +const defaultState: State = { + keymaps: [], + search: DefaultSetting.search, + properties: DefaultSetting.properties, +}; + +export default function reducer( + state: State = defaultState, + action: actions.SettingAction, +): State { + switch (action.type) { + case actions.SETTING_SET: + return { + keymaps: Object.entries(action.settings.keymaps).map((entry) => { + return { + key: keyUtils.fromMapKeys(entry[0]), + op: entry[1], + }; + }), + properties: action.settings.properties, + search: action.settings.search, + }; + default: + return state; + } +} + diff --git a/src/content/scrolls.js b/src/content/scrolls.ts index f3124a1..6a35315 100644 --- a/src/content/scrolls.js +++ 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); @@ -90,22 +94,12 @@ class Scroller { } } -class RoughtScroller { - constructor(element) { - this.element = element; - } - - scroll(x, y) { - this.element.scrollTo(x, y); - } -} - const getScroll = () => { let target = scrollTarget(); 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) { @@ -114,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) { @@ -123,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; @@ -133,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/site-style.js b/src/content/site-style.ts index e7a82a5..0c335fc 100644 --- a/src/content/site-style.js +++ b/src/content/site-style.ts @@ -1,4 +1,4 @@ -exports.default = ` +export default ` .vimvixen-console-frame { margin: 0; padding: 0; 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.js b/src/content/urls.ts index 6e7ea31..035b9bb 100644 --- a/src/content/urls.js +++ b/src/content/urls.ts @@ -1,7 +1,8 @@ -import messages from 'shared/messages'; +import * as messages from '../shared/messages'; import * as urls from '../shared/urls'; +import { Search } from '../shared/Settings'; -const yank = (win) => { +const yank = (win: Window) => { let input = win.document.createElement('input'); win.document.body.append(input); @@ -15,7 +16,7 @@ const yank = (win) => { input.remove(); }; -const paste = (win, newTab, searchSettings) => { +const paste = (win: Window, newTab: boolean, search: Search) => { let textarea = win.document.createElement('textarea'); win.document.body.append(textarea); @@ -25,8 +26,8 @@ const paste = (win, newTab, searchSettings) => { textarea.focus(); if (win.document.execCommand('paste')) { - let value = textarea.textContent; - let url = urls.searchUrl(value, searchSettings); + let value = textarea.textContent as string; + let url = urls.searchUrl(value, search); browser.runtime.sendMessage({ type: messages.OPEN_URL, url, diff --git a/src/settings/actions/index.js b/src/settings/actions/index.js deleted file mode 100644 index 016f2a5..0000000 --- a/src/settings/actions/index.js +++ /dev/null @@ -1,7 +0,0 @@ -export default { - // Settings - SETTING_SET_SETTINGS: 'setting.set.settings', - SETTING_SHOW_ERROR: 'setting.show.error', - SETTING_SWITCH_TO_FORM: 'setting.switch.to.form', - SETTING_SWITCH_TO_JSON: 'setting.switch.to.json', -}; diff --git a/src/settings/actions/index.ts b/src/settings/actions/index.ts new file mode 100644 index 0000000..b1e996e --- /dev/null +++ b/src/settings/actions/index.ts @@ -0,0 +1,36 @@ +import { + JSONSettings, FormSettings, SettingSource, +} from '../../shared/SettingData'; + +// Settings +export const SETTING_SET_SETTINGS = 'setting.set.settings'; +export const SETTING_SHOW_ERROR = 'setting.show.error'; +export const SETTING_SWITCH_TO_FORM = 'setting.switch.to.form'; +export const SETTING_SWITCH_TO_JSON = 'setting.switch.to.json'; + +interface SettingSetSettingsAcion { + type: typeof SETTING_SET_SETTINGS; + source: SettingSource; + json?: JSONSettings; + form?: FormSettings; +} + +interface SettingShowErrorAction { + type: typeof SETTING_SHOW_ERROR; + error: string; + json: JSONSettings; +} + +interface SettingSwitchToFormAction { + type: typeof SETTING_SWITCH_TO_FORM; + form: FormSettings, +} + +interface SettingSwitchToJsonAction { + type: typeof SETTING_SWITCH_TO_JSON; + json: JSONSettings, +} + +export type SettingAction = + SettingSetSettingsAcion | SettingShowErrorAction | + SettingSwitchToFormAction | SettingSwitchToJsonAction; diff --git a/src/settings/actions/setting.js b/src/settings/actions/setting.js deleted file mode 100644 index db63a45..0000000 --- a/src/settings/actions/setting.js +++ /dev/null @@ -1,63 +0,0 @@ -import actions from 'settings/actions'; -import * as validator from 'shared/settings/validator'; -import * as settingsValues from 'shared/settings/values'; -import * as settingsStorage from 'shared/settings/storage'; -import keymaps from '../keymaps'; - -const load = async() => { - let settings = await settingsStorage.loadRaw(); - return set(settings); -}; - -const save = async(settings) => { - try { - if (settings.source === 'json') { - let value = JSON.parse(settings.json); - validator.validate(value); - } - } catch (e) { - return { - type: actions.SETTING_SHOW_ERROR, - error: e.toString(), - json: settings.json, - }; - } - await settingsStorage.save(settings); - return set(settings); -}; - -const switchToForm = (json) => { - try { - validator.validate(JSON.parse(json)); - let form = settingsValues.formFromJson(json, keymaps.allowedOps); - return { - type: actions.SETTING_SWITCH_TO_FORM, - form, - }; - } catch (e) { - return { - type: actions.SETTING_SHOW_ERROR, - error: e.toString(), - json, - }; - } -}; - -const switchToJson = (form) => { - let json = settingsValues.jsonFromForm(form); - return { - type: actions.SETTING_SWITCH_TO_JSON, - json, - }; -}; - -const set = (settings) => { - return { - type: actions.SETTING_SET_SETTINGS, - source: settings.source, - json: settings.json, - form: settings.form, - }; -}; - -export { load, save, set, switchToForm, switchToJson }; diff --git a/src/settings/actions/setting.ts b/src/settings/actions/setting.ts new file mode 100644 index 0000000..9eb416e --- /dev/null +++ b/src/settings/actions/setting.ts @@ -0,0 +1,73 @@ +import * as actions from './index'; +import * as storages from '../storage'; +import SettingData, { + JSONSettings, FormSettings, SettingSource, +} from '../../shared/SettingData'; + +const load = async(): Promise<actions.SettingAction> => { + let data = await storages.load(); + return set(data); +}; + +const save = async(data: SettingData): Promise<actions.SettingAction> => { + try { + if (data.getSource() === SettingSource.JSON) { + // toSettings exercise validation + data.toSettings(); + } + } catch (e) { + return { + type: actions.SETTING_SHOW_ERROR, + error: e.toString(), + json: data.getJSON(), + }; + } + await storages.save(data); + return set(data); +}; + +const switchToForm = (json: JSONSettings): actions.SettingAction => { + try { + // toSettings exercise validation + let form = FormSettings.fromSettings(json.toSettings()); + return { + type: actions.SETTING_SWITCH_TO_FORM, + form, + }; + } catch (e) { + return { + type: actions.SETTING_SHOW_ERROR, + error: e.toString(), + json, + }; + } +}; + +const switchToJson = (form: FormSettings): actions.SettingAction => { + let json = JSONSettings.fromSettings(form.toSettings()); + return { + type: actions.SETTING_SWITCH_TO_JSON, + json, + }; +}; + +const set = (data: SettingData): actions.SettingAction => { + let source = data.getSource(); + switch (source) { + case SettingSource.JSON: + return { + type: actions.SETTING_SET_SETTINGS, + source: source, + json: data.getJSON(), + }; + case SettingSource.Form: + return { + type: actions.SETTING_SET_SETTINGS, + source: source, + form: data.getForm(), + }; + } + throw new Error(`unknown source: ${source}`); +}; + +export { load, save, set, switchToForm, switchToJson }; diff --git a/src/settings/components/form/BlacklistForm.jsx b/src/settings/components/form/BlacklistForm.tsx index c470758..637bc1e 100644 --- a/src/settings/components/form/BlacklistForm.jsx +++ b/src/settings/components/form/BlacklistForm.tsx @@ -2,9 +2,19 @@ import './BlacklistForm.scss'; import AddButton from '../ui/AddButton'; import DeleteButton from '../ui/DeleteButton'; import React from 'react'; -import PropTypes from 'prop-types'; -class BlacklistForm extends React.Component { +interface Props { + value: string[]; + onChange: (value: string[]) => void; + onBlur: () => void; +} + +class BlacklistForm extends React.Component<Props> { + public static defaultProps: Props = { + value: [], + onChange: () => {}, + onBlur: () => {}, + }; render() { return <div className='form-blacklist-form'> @@ -28,7 +38,7 @@ class BlacklistForm extends React.Component { </div>; } - bindValue(e) { + bindValue(e: any) { let name = e.target.name; let index = e.target.getAttribute('data-index'); let next = this.props.value ? this.props.value.slice() : []; @@ -48,16 +58,4 @@ class BlacklistForm extends React.Component { } } -BlacklistForm.propTypes = { - value: PropTypes.arrayOf(PropTypes.string), - onChange: PropTypes.func, - onBlur: PropTypes.func, -}; - -BlacklistForm.defaultProps = { - value: [], - onChange: () => {}, - onBlur: () => {}, -}; - export default BlacklistForm; diff --git a/src/settings/components/form/KeymapsForm.jsx b/src/settings/components/form/KeymapsForm.tsx index 01acf61..ad4d0e7 100644 --- a/src/settings/components/form/KeymapsForm.jsx +++ b/src/settings/components/form/KeymapsForm.tsx @@ -1,25 +1,35 @@ import './KeymapsForm.scss'; import React from 'react'; -import PropTypes from 'prop-types'; import Input from '../ui/Input'; import keymaps from '../../keymaps'; +import { FormKeymaps } from '../../../shared/SettingData'; -class KeymapsForm extends React.Component { +interface Props { + value: FormKeymaps; + onChange: (e: FormKeymaps) => void; + onBlur: () => void; +} + +class KeymapsForm extends React.Component<Props> { + public static defaultProps: Props = { + value: FormKeymaps.valueOf({}), + onChange: () => {}, + onBlur: () => {}, + } render() { + let values = this.props.value.toJSON(); return <div className='form-keymaps-form'> { keymaps.fields.map((group, index) => { return <div key={index} className='form-keymaps-form-field-group'> { - group.map((field) => { - let name = field[0]; - let label = field[1]; - let value = this.props.value[name] || ''; + group.map(([name, label]) => { + let value = values[name] || ''; return <Input type='text' id={name} name={name} key={name} label={label} value={value} - onChange={this.bindValue.bind(this)} + onValueChange={this.bindValue.bind(this)} onBlur={this.props.onBlur} />; }) @@ -30,22 +40,9 @@ class KeymapsForm extends React.Component { </div>; } - bindValue(e) { - let next = { ...this.props.value }; - next[e.target.name] = e.target.value; - - this.props.onChange(next); + bindValue(name: string, value: string) { + this.props.onChange(this.props.value.buildWithOverride(name, value)); } } -KeymapsForm.propTypes = { - value: PropTypes.objectOf(PropTypes.string), - onChange: PropTypes.func, -}; - -KeymapsForm.defaultProps = { - value: {}, - onChange: () => {}, -}; - export default KeymapsForm; diff --git a/src/settings/components/form/PropertiesForm.jsx b/src/settings/components/form/PropertiesForm.tsx index 979fdd8..0be5f5c 100644 --- a/src/settings/components/form/PropertiesForm.jsx +++ b/src/settings/components/form/PropertiesForm.tsx @@ -1,8 +1,20 @@ import './PropertiesForm.scss'; import React from 'react'; -import PropTypes from 'prop-types'; -class PropertiesForm extends React.Component { +interface Props { + types: {[key: string]: string}; + value: {[key: string]: any}; + onChange: (value: any) => void; + onBlur: () => void; +} + +class PropertiesForm extends React.Component<Props> { + public static defaultProps: Props = { + types: {}, + value: {}, + onChange: () => {}, + onBlur: () => {}, + }; render() { let types = this.props.types; @@ -12,13 +24,15 @@ class PropertiesForm extends React.Component { { Object.keys(types).map((name) => { let type = types[name]; - let inputType = null; + let inputType = ''; if (type === 'string') { inputType = 'text'; } else if (type === 'number') { inputType = 'number'; } else if (type === 'boolean') { inputType = 'checkbox'; + } else { + return null; } return <div key={name} className='form-properties-form-row'> <label> @@ -37,7 +51,7 @@ class PropertiesForm extends React.Component { </div>; } - bindValue(e) { + bindValue(e: React.ChangeEvent<HTMLInputElement>) { let name = e.target.name; let next = { ...this.props.value }; if (e.target.type.toLowerCase() === 'checkbox') { @@ -52,14 +66,4 @@ class PropertiesForm extends React.Component { } } -PropertiesForm.propTypes = { - value: PropTypes.objectOf(PropTypes.any), - onChange: PropTypes.func, -}; - -PropertiesForm.defaultProps = { - value: {}, - onChange: () => {}, -}; - export default PropertiesForm; diff --git a/src/settings/components/form/SearchForm.jsx b/src/settings/components/form/SearchForm.tsx index 6b0bd01..67dbeba 100644 --- a/src/settings/components/form/SearchForm.jsx +++ b/src/settings/components/form/SearchForm.tsx @@ -1,17 +1,24 @@ import './SearchForm.scss'; import React from 'react'; -import PropTypes from 'prop-types'; import AddButton from '../ui/AddButton'; import DeleteButton from '../ui/DeleteButton'; +import { FormSearch } from '../../../shared/SettingData'; -class SearchForm extends React.Component { +interface Props { + value: FormSearch; + onChange: (value: FormSearch) => void; + onBlur: () => void; +} - render() { - let value = this.props.value; - if (!value.engines) { - value.engines = []; - } +class SearchForm extends React.Component<Props> { + public static defaultProps: Props = { + value: FormSearch.valueOf({ default: '', engines: []}), + onChange: () => {}, + onBlur: () => {}, + } + render() { + let value = this.props.value.toJSON(); return <div className='form-search-form'> <div className='form-search-form-header'> <div className='column-name'>Name</div> @@ -47,46 +54,33 @@ class SearchForm extends React.Component { </div>; } - bindValue(e) { - let value = this.props.value; + bindValue(e: any) { + let value = this.props.value.toJSON(); let name = e.target.name; - let index = e.target.getAttribute('data-index'); - let next = { + let index = Number(e.target.getAttribute('data-index')); + let next: typeof value = { default: value.default, - engines: value.engines ? value.engines.slice() : [], + engines: value.engines.slice(), }; if (name === 'name') { next.engines[index][0] = e.target.value; - next.default = this.props.value.engines[index][0]; + next.default = value.engines[index][0]; } else if (name === 'url') { next.engines[index][1] = e.target.value; } else if (name === 'default') { - next.default = this.props.value.engines[index][0]; + next.default = value.engines[index][0]; } else if (name === 'add') { next.engines.push(['', '']); } else if (name === 'delete') { next.engines.splice(index, 1); } - this.props.onChange(next); + this.props.onChange(FormSearch.valueOf(next)); if (name === 'delete' || name === 'default') { this.props.onBlur(); } } } -SearchForm.propTypes = { - value: PropTypes.shape({ - default: PropTypes.string, - engines: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)), - }), - onChange: PropTypes.func, -}; - -SearchForm.defaultProps = { - value: { default: '', engines: []}, - onChange: () => {}, -}; - export default SearchForm; diff --git a/src/settings/components/index.jsx b/src/settings/components/index.jsx deleted file mode 100644 index 4ef59d7..0000000 --- a/src/settings/components/index.jsx +++ /dev/null @@ -1,153 +0,0 @@ -import './site.scss'; -import React from 'react'; -import { connect } from 'react-redux'; -import Input from './ui/Input'; -import SearchForm from './form/SearchForm'; -import KeymapsForm from './form/KeymapsForm'; -import BlacklistForm from './form/BlacklistForm'; -import PropertiesForm from './form/PropertiesForm'; -import * as properties from 'shared/settings/properties'; -import * as settingActions from 'settings/actions/setting'; - -const DO_YOU_WANT_TO_CONTINUE = - 'Some settings in JSON can be lost when migrating. ' + - 'Do you want to continue?'; - -class SettingsComponent extends React.Component { - componentDidMount() { - this.props.dispatch(settingActions.load()); - } - - renderFormFields(form) { - return <div> - <fieldset> - <legend>Keybindings</legend> - <KeymapsForm - value={form.keymaps} - onChange={value => this.bindForm('keymaps', value)} - onBlur={this.save.bind(this)} - /> - </fieldset> - <fieldset> - <legend>Search Engines</legend> - <SearchForm - value={form.search} - onChange={value => this.bindForm('search', value)} - onBlur={this.save.bind(this)} - /> - </fieldset> - <fieldset> - <legend>Blacklist</legend> - <BlacklistForm - value={form.blacklist} - onChange={value => this.bindForm('blacklist', value)} - onBlur={this.save.bind(this)} - /> - </fieldset> - <fieldset> - <legend>Properties</legend> - <PropertiesForm - types={properties.types} - value={form.properties} - onChange={value => this.bindForm('properties', value)} - onBlur={this.save.bind(this)} - /> - </fieldset> - </div>; - } - - renderJsonFields(json, error) { - return <div> - <Input - type='textarea' - name='json' - label='Plain JSON' - spellCheck='false' - error={error} - onChange={this.bindJson.bind(this)} - onBlur={this.save.bind(this)} - value={json} - /> - </div>; - } - - render() { - let fields = null; - let disabled = this.props.error.length > 0; - if (this.props.source === 'form') { - fields = this.renderFormFields(this.props.form); - } else if (this.props.source === 'json') { - fields = this.renderJsonFields(this.props.json, this.props.error); - } - return ( - <div> - <h1>Configure Vim-Vixen</h1> - <form className='vimvixen-settings-form'> - <Input - type='radio' - id='setting-source-form' - name='source' - label='Use form' - checked={this.props.source === 'form'} - value='form' - onChange={this.bindSource.bind(this)} - disabled={disabled} /> - - <Input - type='radio' - name='source' - label='Use plain JSON' - checked={this.props.source === 'json'} - value='json' - onChange={this.bindSource.bind(this)} - disabled={disabled} /> - { fields } - </form> - </div> - ); - } - - bindForm(name, value) { - let settings = { - source: this.props.source, - json: this.props.json, - form: { ...this.props.form }, - }; - settings.form[name] = value; - this.props.dispatch(settingActions.set(settings)); - } - - bindJson(e) { - let settings = { - source: this.props.source, - json: e.target.value, - form: this.props.form, - }; - this.props.dispatch(settingActions.set(settings)); - } - - bindSource(e) { - let from = this.props.source; - let to = e.target.value; - - if (from === 'form' && to === 'json') { - this.props.dispatch(settingActions.switchToJson(this.props.form)); - } else if (from === 'json' && to === 'form') { - let b = window.confirm(DO_YOU_WANT_TO_CONTINUE); - if (!b) { - this.forceUpdate(); - return; - } - this.props.dispatch(settingActions.switchToForm(this.props.json)); - } - } - - save() { - let settings = this.props.store.getState(); - this.props.dispatch(settingActions.save(settings)); - } -} - -const mapStateToProps = state => state; - -export default connect(mapStateToProps)(SettingsComponent); diff --git a/src/settings/components/index.tsx b/src/settings/components/index.tsx new file mode 100644 index 0000000..b4a0866 --- /dev/null +++ b/src/settings/components/index.tsx @@ -0,0 +1,199 @@ +import './site.scss'; +import React from 'react'; +import { connect } from 'react-redux'; +import Input from './ui/Input'; +import SearchForm from './form/SearchForm'; +import KeymapsForm from './form/KeymapsForm'; +import BlacklistForm from './form/BlacklistForm'; +import PropertiesForm from './form/PropertiesForm'; +import * as settingActions from '../../settings/actions/setting'; +import SettingData, { + JSONSettings, FormKeymaps, FormSearch, FormSettings, +} from '../../shared/SettingData'; +import { State as AppState } from '../reducers/setting'; +import * as settings from '../../shared/Settings'; +import * as PropertyDefs from '../../shared/property-defs'; + +const DO_YOU_WANT_TO_CONTINUE = + 'Some settings in JSON can be lost when migrating. ' + + 'Do you want to continue?'; + +type StateProps = ReturnType<typeof mapStateToProps>; +interface DispatchProps { + dispatch: (action: any) => void, +} +type Props = StateProps & DispatchProps & { + // FIXME + store: any; +}; + +class SettingsComponent extends React.Component<Props> { + componentDidMount() { + this.props.dispatch(settingActions.load()); + } + + renderFormFields(form: any) { + let types = PropertyDefs.defs.reduce( + (o: {[key: string]: string}, def) => { + o[def.name] = def.type; + return o; + }, {}); + return <div> + <fieldset> + <legend>Keybindings</legend> + <KeymapsForm + value={form.keymaps} + onChange={this.bindKeymapsForm.bind(this)} + onBlur={this.save.bind(this)} + /> + </fieldset> + <fieldset> + <legend>Search Engines</legend> + <SearchForm + value={form.search} + onChange={this.bindSearchForm.bind(this)} + onBlur={this.save.bind(this)} + /> + </fieldset> + <fieldset> + <legend>Blacklist</legend> + <BlacklistForm + value={form.blacklist} + onChange={this.bindBlacklistForm.bind(this)} + onBlur={this.save.bind(this)} + /> + </fieldset> + <fieldset> + <legend>Properties</legend> + <PropertiesForm + types={types} + value={form.properties} + onChange={this.bindPropertiesForm.bind(this)} + onBlur={this.save.bind(this)} + /> + </fieldset> + </div>; + } + + renderJsonFields(json: JSONSettings, error: string) { + return <div> + <Input + type='textarea' + name='json' + label='Plain JSON' + spellCheck={false} + error={error} + onValueChange={this.bindJson.bind(this)} + onBlur={this.save.bind(this)} + value={json.toJSON()} + /> + </div>; + } + + render() { + let fields = null; + let disabled = this.props.error.length > 0; + if (this.props.source === 'form') { + fields = this.renderFormFields(this.props.form); + } else if (this.props.source === 'json') { + fields = this.renderJsonFields( + this.props.json as JSONSettings, this.props.error); + } + return ( + <div> + <h1>Configure Vim-Vixen</h1> + <form className='vimvixen-settings-form'> + <Input + type='radio' + id='setting-source-form' + name='source' + label='Use form' + checked={this.props.source === 'form'} + value='form' + onValueChange={this.bindSource.bind(this)} + disabled={disabled} /> + + <Input + type='radio' + name='source' + label='Use plain JSON' + checked={this.props.source === 'json'} + value='json' + onValueChange={this.bindSource.bind(this)} + disabled={disabled} /> + { fields } + </form> + </div> + ); + } + + bindKeymapsForm(value: FormKeymaps) { + let data = new SettingData({ + source: this.props.source, + form: (this.props.form as FormSettings).buildWithKeymaps(value), + }); + this.props.dispatch(settingActions.set(data)); + } + + bindSearchForm(value: any) { + let data = new SettingData({ + source: this.props.source, + form: (this.props.form as FormSettings).buildWithSearch( + FormSearch.valueOf(value)), + }); + this.props.dispatch(settingActions.set(data)); + } + + bindBlacklistForm(value: any) { + let data = new SettingData({ + source: this.props.source, + form: (this.props.form as FormSettings).buildWithBlacklist( + settings.blacklistValueOf(value)), + }); + this.props.dispatch(settingActions.set(data)); + } + + bindPropertiesForm(value: any) { + let data = new SettingData({ + source: this.props.source, + form: (this.props.form as FormSettings).buildWithProperties( + settings.propertiesValueOf(value)), + }); + this.props.dispatch(settingActions.set(data)); + } + + bindJson(_name: string, value: string) { + let data = new SettingData({ + source: this.props.source, + json: JSONSettings.valueOf(value), + }); + this.props.dispatch(settingActions.set(data)); + } + + bindSource(_name: string, value: string) { + let from = this.props.source; + if (from === 'form' && value === 'json') { + this.props.dispatch(settingActions.switchToJson( + this.props.form as FormSettings)); + } else if (from === 'json' && value === 'form') { + let b = window.confirm(DO_YOU_WANT_TO_CONTINUE); + if (!b) { + this.forceUpdate(); + return; + } + this.props.dispatch( + settingActions.switchToForm(this.props.json as JSONSettings)); + } + } + + save() { + let { source, json, form } = this.props.store.getState(); + this.props.dispatch(settingActions.save( + new SettingData({ source, json, form }), + )); + } +} + +const mapStateToProps = (state: AppState) => ({ ...state }); + +export default connect(mapStateToProps)(SettingsComponent); diff --git a/src/settings/components/ui/AddButton.jsx b/src/settings/components/ui/AddButton.tsx index 185a03b..0577068 100644 --- a/src/settings/components/ui/AddButton.jsx +++ b/src/settings/components/ui/AddButton.tsx @@ -1,7 +1,10 @@ import './AddButton.scss'; import React from 'react'; -class AddButton extends React.Component { +interface Props extends React.AllHTMLAttributes<HTMLInputElement> { +} + +class AddButton extends React.Component<Props> { render() { return <input className='ui-add-button' type='button' value='✚' diff --git a/src/settings/components/ui/DeleteButton.jsx b/src/settings/components/ui/DeleteButton.tsx index 75811cd..f0ef6c9 100644 --- a/src/settings/components/ui/DeleteButton.jsx +++ b/src/settings/components/ui/DeleteButton.tsx @@ -1,7 +1,10 @@ import './DeleteButton.scss'; import React from 'react'; -class DeleteButton extends React.Component { +interface Props extends React.AllHTMLAttributes<HTMLInputElement> { +} + +class DeleteButton extends React.Component<Props> { render() { return <input className='ui-delete-button' type='button' value='✖' diff --git a/src/settings/components/ui/Input.jsx b/src/settings/components/ui/Input.jsx deleted file mode 100644 index 13a246b..0000000 --- a/src/settings/components/ui/Input.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import './Input.scss'; - -class Input extends React.Component { - - renderText(props) { - let inputClassName = props.error ? 'input-error' : ''; - return <div className='settings-ui-input'> - <label htmlFor={props.id}>{ props.label }</label> - <input type='text' className={inputClassName} {...props} /> - </div>; - } - - renderRadio(props) { - let inputClassName = props.error ? 'input-error' : ''; - return <div className='settings-ui-input'> - <label> - <input type='radio' className={inputClassName} {...props} /> - { props.label } - </label> - </div>; - } - - renderTextArea(props) { - let inputClassName = props.error ? 'input-error' : ''; - return <div className='settings-ui-input'> - <label - htmlFor={props.id} - >{ props.label }</label> - <textarea className={inputClassName} {...props} /> - <p className='settings-ui-input-error'>{ this.props.error }</p> - </div>; - } - - render() { - let { type } = this.props; - - switch (this.props.type) { - case 'text': - return this.renderText(this.props); - case 'radio': - return this.renderRadio(this.props); - case 'textarea': - return this.renderTextArea(this.props); - default: - console.warn(`Unsupported input type ${type}`); - } - return null; - } -} - -Input.propTypes = { - type: PropTypes.string, - error: PropTypes.string, - label: PropTypes.string, - value: PropTypes.string, -}; - -export default Input; diff --git a/src/settings/components/ui/Input.tsx b/src/settings/components/ui/Input.tsx new file mode 100644 index 0000000..b7593b9 --- /dev/null +++ b/src/settings/components/ui/Input.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import './Input.scss'; + +interface Props extends React.AllHTMLAttributes<HTMLElement> { + name: string; + type: string; + error?: string; + label: string; + value: string; + onValueChange?: (name: string, value: string) => void; + onBlur?: (e: React.FocusEvent<Element>) => void; +} + +class Input extends React.Component<Props> { + renderText(props: Props) { + let inputClassName = props.error ? 'input-error' : ''; + let pp = { ...props }; + delete pp.onValueChange; + return <div className='settings-ui-input'> + <label htmlFor={props.id}>{ props.label }</label> + <input + type='text' className={inputClassName} + onChange={this.bindOnChange.bind(this)} + { ...pp } /> + </div>; + } + + renderRadio(props: Props) { + let inputClassName = props.error ? 'input-error' : ''; + let pp = { ...props }; + delete pp.onValueChange; + return <div className='settings-ui-input'> + <label> + <input + type='radio' className={inputClassName} + onChange={this.bindOnChange.bind(this)} + { ...pp } /> + { props.label } + </label> + </div>; + } + + renderTextArea(props: Props) { + let inputClassName = props.error ? 'input-error' : ''; + let pp = { ...props }; + delete pp.onValueChange; + return <div className='settings-ui-input'> + <label + htmlFor={props.id} + >{ props.label }</label> + <textarea + className={inputClassName} + onChange={this.bindOnChange.bind(this)} + { ...pp } /> + <p className='settings-ui-input-error'>{ this.props.error }</p> + </div>; + } + + render() { + let { type } = this.props; + + switch (this.props.type) { + case 'text': + return this.renderText(this.props); + case 'radio': + return this.renderRadio(this.props); + case 'textarea': + return this.renderTextArea(this.props); + default: + console.warn(`Unsupported input type ${type}`); + } + return null; + } + + bindOnChange(e: React.ChangeEvent<HTMLInputElement|HTMLTextAreaElement>) { + if (this.props.onValueChange) { + this.props.onValueChange(e.target.name, e.target.value); + } + } +} + +export default Input; diff --git a/src/settings/index.jsx b/src/settings/index.tsx index 6aec7a0..6aec7a0 100644 --- a/src/settings/index.jsx +++ b/src/settings/index.tsx diff --git a/src/settings/keymaps.js b/src/settings/keymaps.ts index ccfc74c..38045ad 100644 --- a/src/settings/keymaps.js +++ b/src/settings/keymaps.ts @@ -66,9 +66,6 @@ const fields = [ ] ]; -const allowedOps = [].concat(...fields.map(group => group.map(e => e[0]))); - export default { fields, - allowedOps, }; diff --git a/src/settings/reducers/setting.js b/src/settings/reducers/setting.ts index 54033aa..c4a21c7 100644 --- a/src/settings/reducers/setting.js +++ b/src/settings/reducers/setting.ts @@ -1,13 +1,25 @@ -import actions from 'settings/actions'; +import * as actions from '../actions'; +import { + JSONSettings, FormSettings, SettingSource, +} from '../../shared/SettingData'; -const defaultState = { - source: '', - json: '', - form: null, +export interface State { + source: SettingSource; + json?: JSONSettings; + form?: FormSettings; + error: string; +} + +const defaultState: State = { + source: SettingSource.JSON, + json: JSONSettings.valueOf(''), error: '', }; -export default function reducer(state = defaultState, action = {}) { +export default function reducer( + state = defaultState, + action: actions.SettingAction, +): State { switch (action.type) { case actions.SETTING_SET_SETTINGS: return { ...state, @@ -22,12 +34,12 @@ export default function reducer(state = defaultState, action = {}) { case actions.SETTING_SWITCH_TO_FORM: return { ...state, error: '', - source: 'form', + source: SettingSource.Form, form: action.form, }; case actions.SETTING_SWITCH_TO_JSON: return { ...state, error: '', - source: 'json', + source: SettingSource.JSON, json: action.json, }; default: return state; diff --git a/src/settings/storage.ts b/src/settings/storage.ts new file mode 100644 index 0000000..c0005b7 --- /dev/null +++ b/src/settings/storage.ts @@ -0,0 +1,15 @@ +import SettingData, { DefaultSettingData } from '../shared/SettingData'; + +export const load = async(): Promise<SettingData> => { + let { settings } = await browser.storage.local.get('settings'); + if (!settings) { + return DefaultSettingData; + } + return SettingData.valueOf(settings as any); +}; + +export const save = (data: SettingData) => { + return browser.storage.local.set({ + settings: data.toJSON(), + }); +}; diff --git a/src/shared/SettingData.ts b/src/shared/SettingData.ts new file mode 100644 index 0000000..05e21fa --- /dev/null +++ b/src/shared/SettingData.ts @@ -0,0 +1,414 @@ +import * as operations from './operations'; +import Settings, * as settings from './Settings'; + +export class FormKeymaps { + private data: {[op: string]: string}; + + constructor(data: {[op: string]: string}) { + this.data = data; + } + + toKeymaps(): settings.Keymaps { + let keymaps: settings.Keymaps = {}; + for (let name of Object.keys(this.data)) { + let [type, argStr] = name.split('?'); + let args = {}; + if (argStr) { + args = JSON.parse(argStr); + } + let key = this.data[name]; + keymaps[key] = operations.valueOf({ type, ...args }); + } + return keymaps; + } + + toJSON(): {[op: string]: string} { + return this.data; + } + + buildWithOverride(op: string, keys: string): FormKeymaps { + let newData = { + ...this.data, + [op]: keys, + }; + return new FormKeymaps(newData); + } + + static valueOf(o: ReturnType<FormKeymaps['toJSON']>): FormKeymaps { + let data: {[op: string]: string} = {}; + for (let op of Object.keys(o)) { + data[op] = o[op] as string; + } + return new FormKeymaps(data); + } + + static fromKeymaps(keymaps: settings.Keymaps): FormKeymaps { + let data: {[op: string]: string} = {}; + for (let key of Object.keys(keymaps)) { + let op = keymaps[key]; + let args = { ...op }; + delete args.type; + + let name = op.type; + if (Object.keys(args).length > 0) { + name += '?' + JSON.stringify(args); + } + data[name] = key; + } + return new FormKeymaps(data); + } +} + +export class FormSearch { + private default: string; + + private engines: string[][]; + + constructor(defaultEngine: string, engines: string[][]) { + this.default = defaultEngine; + this.engines = engines; + } + + toSearchSettings(): settings.Search { + return { + default: this.default, + engines: this.engines.reduce( + (o: {[key: string]: string}, [name, url]) => { + o[name] = url; + return o; + }, {}), + }; + } + + toJSON(): { + default: string; + engines: string[][]; + } { + return { + default: this.default, + engines: this.engines, + }; + } + + static valueOf(o: ReturnType<FormSearch['toJSON']>): FormSearch { + if (!Object.prototype.hasOwnProperty.call(o, 'default')) { + throw new TypeError(`"default" field not set`); + } + if (!Object.prototype.hasOwnProperty.call(o, 'engines')) { + throw new TypeError(`"engines" field not set`); + } + return new FormSearch(o.default, o.engines); + } + + static fromSearch(search: settings.Search): FormSearch { + let engines = Object.entries(search.engines).reduce( + (o: string[][], [name, url]) => { + return o.concat([[name, url]]); + }, []); + return new FormSearch(search.default, engines); + } +} + +export class JSONSettings { + private json: string; + + constructor(json: any) { + this.json = json; + } + + toSettings(): Settings { + return settings.valueOf(JSON.parse(this.json)); + } + + toJSON(): string { + return this.json; + } + + static valueOf(o: ReturnType<JSONSettings['toJSON']>): JSONSettings { + return new JSONSettings(o); + } + + static fromSettings(data: Settings): JSONSettings { + return new JSONSettings(JSON.stringify(data, undefined, 2)); + } +} + +export class FormSettings { + private keymaps: FormKeymaps; + + private search: FormSearch; + + private properties: settings.Properties; + + private blacklist: string[]; + + constructor( + keymaps: FormKeymaps, + search: FormSearch, + properties: settings.Properties, + blacklist: string[], + ) { + this.keymaps = keymaps; + this.search = search; + this.properties = properties; + this.blacklist = blacklist; + } + + buildWithKeymaps(keymaps: FormKeymaps): FormSettings { + return new FormSettings( + keymaps, + this.search, + this.properties, + this.blacklist, + ); + } + + buildWithSearch(search: FormSearch): FormSettings { + return new FormSettings( + this.keymaps, + search, + this.properties, + this.blacklist, + ); + } + + buildWithProperties(props: settings.Properties): FormSettings { + return new FormSettings( + this.keymaps, + this.search, + props, + this.blacklist, + ); + } + + buildWithBlacklist(blacklist: string[]): FormSettings { + return new FormSettings( + this.keymaps, + this.search, + this.properties, + blacklist, + ); + } + + toSettings(): Settings { + return settings.valueOf({ + keymaps: this.keymaps.toKeymaps(), + search: this.search.toSearchSettings(), + properties: this.properties, + blacklist: this.blacklist, + }); + } + + toJSON(): { + keymaps: ReturnType<FormKeymaps['toJSON']>; + search: ReturnType<FormSearch['toJSON']>; + properties: settings.Properties; + blacklist: string[]; + } { + return { + keymaps: this.keymaps.toJSON(), + search: this.search.toJSON(), + properties: this.properties, + blacklist: this.blacklist, + }; + } + + static valueOf(o: ReturnType<FormSettings['toJSON']>): FormSettings { + for (let name of ['keymaps', 'search', 'properties', 'blacklist']) { + if (!Object.prototype.hasOwnProperty.call(o, name)) { + throw new Error(`"${name}" field not set`); + } + } + return new FormSettings( + FormKeymaps.valueOf(o.keymaps), + FormSearch.valueOf(o.search), + settings.propertiesValueOf(o.properties), + settings.blacklistValueOf(o.blacklist), + ); + } + + static fromSettings(data: Settings): FormSettings { + return new FormSettings( + FormKeymaps.fromKeymaps(data.keymaps), + FormSearch.fromSearch(data.search), + data.properties, + data.blacklist); + } +} + +export enum SettingSource { + JSON = 'json', + Form = 'form', +} + +export default class SettingData { + private source: SettingSource; + + private json?: JSONSettings; + + private form?: FormSettings; + + constructor({ + source, json, form + }: { + source: SettingSource, + json?: JSONSettings, + form?: FormSettings, + }) { + this.source = source; + this.json = json; + this.form = form; + } + + getSource(): SettingSource { + return this.source; + } + + getJSON(): JSONSettings { + if (!this.json) { + throw new TypeError('json settings not set'); + } + return this.json; + } + + getForm(): FormSettings { + if (!this.form) { + throw new TypeError('form settings not set'); + } + return this.form; + } + + toJSON(): any { + switch (this.source) { + case SettingSource.JSON: + return { + source: this.source, + json: (this.json as JSONSettings).toJSON(), + }; + case SettingSource.Form: + return { + source: this.source, + form: (this.form as FormSettings).toJSON(), + }; + } + throw new Error(`unknown settings source: ${this.source}`); + } + + toSettings(): Settings { + switch (this.source) { + case SettingSource.JSON: + return this.getJSON().toSettings(); + case SettingSource.Form: + return this.getForm().toSettings(); + } + throw new Error(`unknown settings source: ${this.source}`); + } + + static valueOf(o: { + source: string; + json?: string; + form?: ReturnType<FormSettings['toJSON']>; + }): SettingData { + switch (o.source) { + case SettingSource.JSON: + return new SettingData({ + source: o.source, + json: JSONSettings.valueOf( + o.json as ReturnType<JSONSettings['toJSON']>), + }); + case SettingSource.Form: + return new SettingData({ + source: o.source, + form: FormSettings.valueOf( + o.form as ReturnType<FormSettings['toJSON']>), + }); + } + throw new Error(`unknown settings source: ${o.source}`); + } +} + +export const DefaultSettingData: SettingData = SettingData.valueOf({ + source: 'json', + json: `{ + "keymaps": { + "0": { "type": "scroll.home" }, + ":": { "type": "command.show" }, + "o": { "type": "command.show.open", "alter": false }, + "O": { "type": "command.show.open", "alter": true }, + "t": { "type": "command.show.tabopen", "alter": false }, + "T": { "type": "command.show.tabopen", "alter": true }, + "w": { "type": "command.show.winopen", "alter": false }, + "W": { "type": "command.show.winopen", "alter": true }, + "b": { "type": "command.show.buffer" }, + "a": { "type": "command.show.addbookmark", "alter": true }, + "k": { "type": "scroll.vertically", "count": -1 }, + "j": { "type": "scroll.vertically", "count": 1 }, + "h": { "type": "scroll.horizonally", "count": -1 }, + "l": { "type": "scroll.horizonally", "count": 1 }, + "<C-U>": { "type": "scroll.pages", "count": -0.5 }, + "<C-D>": { "type": "scroll.pages", "count": 0.5 }, + "<C-B>": { "type": "scroll.pages", "count": -1 }, + "<C-F>": { "type": "scroll.pages", "count": 1 }, + "gg": { "type": "scroll.top" }, + "G": { "type": "scroll.bottom" }, + "$": { "type": "scroll.end" }, + "d": { "type": "tabs.close" }, + "D": { "type": "tabs.close.right" }, + "!d": { "type": "tabs.close.force" }, + "u": { "type": "tabs.reopen" }, + "K": { "type": "tabs.prev" }, + "J": { "type": "tabs.next" }, + "gT": { "type": "tabs.prev" }, + "gt": { "type": "tabs.next" }, + "g0": { "type": "tabs.first" }, + "g$": { "type": "tabs.last" }, + "<C-6>": { "type": "tabs.prevsel" }, + "r": { "type": "tabs.reload", "cache": false }, + "R": { "type": "tabs.reload", "cache": true }, + "zp": { "type": "tabs.pin.toggle" }, + "zd": { "type": "tabs.duplicate" }, + "zi": { "type": "zoom.in" }, + "zo": { "type": "zoom.out" }, + "zz": { "type": "zoom.neutral" }, + "f": { "type": "follow.start", "newTab": false }, + "F": { "type": "follow.start", "newTab": true, "background": false }, + "m": { "type": "mark.set.prefix" }, + "'": { "type": "mark.jump.prefix" }, + "H": { "type": "navigate.history.prev" }, + "L": { "type": "navigate.history.next" }, + "[[": { "type": "navigate.link.prev" }, + "]]": { "type": "navigate.link.next" }, + "gu": { "type": "navigate.parent" }, + "gU": { "type": "navigate.root" }, + "gi": { "type": "focus.input" }, + "gf": { "type": "page.source" }, + "gh": { "type": "page.home" }, + "gH": { "type": "page.home", "newTab": true }, + "y": { "type": "urls.yank" }, + "p": { "type": "urls.paste", "newTab": false }, + "P": { "type": "urls.paste", "newTab": true }, + "/": { "type": "find.start" }, + "n": { "type": "find.next" }, + "N": { "type": "find.prev" }, + "<S-Esc>": { "type": "addon.toggle.enabled" } + }, + "search": { + "default": "google", + "engines": { + "google": "https://google.com/search?q={}", + "yahoo": "https://search.yahoo.com/search?p={}", + "bing": "https://www.bing.com/search?q={}", + "duckduckgo": "https://duckduckgo.com/?q={}", + "twitter": "https://twitter.com/search?q={}", + "wikipedia": "https://en.wikipedia.org/w/index.php?search={}" + } + }, + "properties": { + "hintchars": "abcdefghijklmnopqrstuvwxyz", + "smoothscroll": false, + "complete": "sbh" + }, + "blacklist": [ + ] +}`, +}); diff --git a/src/shared/Settings.ts b/src/shared/Settings.ts new file mode 100644 index 0000000..e35094b --- /dev/null +++ b/src/shared/Settings.ts @@ -0,0 +1,200 @@ +import * as operations from './operations'; +import * as PropertyDefs from './property-defs'; + +export type Keymaps = {[key: string]: operations.Operation}; + +export interface Search { + default: string; + engines: { [key: string]: string }; +} + +export interface Properties { + hintchars: string; + smoothscroll: boolean; + complete: string; +} + +export default interface Settings { + keymaps: Keymaps; + search: Search; + properties: Properties; + blacklist: string[]; + // eslint-disable-next-line semi +} + +const DefaultProperties: Properties = PropertyDefs.defs.reduce( + (o: {[name: string]: PropertyDefs.Type}, def) => { + o[def.name] = def.defaultValue; + return o; + }, {}) as Properties; + + +export const keymapsValueOf = (o: any): Keymaps => { + return Object.keys(o).reduce((keymaps: Keymaps, key: string): Keymaps => { + let op = operations.valueOf(o[key]); + keymaps[key] = op; + return keymaps; + }, {}); +}; + +export const searchValueOf = (o: any): Search => { + if (typeof o.default !== 'string') { + throw new TypeError('string field "default" not set"'); + } + for (let name of Object.keys(o.engines)) { + if ((/\s/).test(name)) { + throw new TypeError( + `While space in the search engine not allowed: "${name}"`); + } + let url = o.engines[name]; + if (typeof url !== 'string') { + throw new TypeError('"engines" not an object of string'); + } + let matches = url.match(/{}/g); + if (matches === null) { + throw new TypeError(`No {}-placeholders in URL of "${name}"`); + } else if (matches.length > 1) { + throw new TypeError(`Multiple {}-placeholders in URL of "${name}"`); + } + + } + if (!Object.prototype.hasOwnProperty.call(o.engines, o.default)) { + throw new TypeError(`Default engine "${o.default}" not found`); + } + return { + default: o.default as string, + engines: { ...o.engines }, + }; +}; + +export const propertiesValueOf = (o: any): Properties => { + let defNames = new Set(PropertyDefs.defs.map(def => def.name)); + let unknownName = Object.keys(o).find(name => !defNames.has(name)); + if (unknownName) { + throw new TypeError(`Unknown property name: "${unknownName}"`); + } + + for (let def of PropertyDefs.defs) { + if (!Object.prototype.hasOwnProperty.call(o, def.name)) { + continue; + } + if (typeof o[def.name] !== def.type) { + throw new TypeError(`property "${def.name}" is not ${def.type}`); + } + } + return { + ...DefaultProperties, + ...o, + }; +}; + +export const blacklistValueOf = (o: any): string[] => { + if (!Array.isArray(o)) { + throw new TypeError(`"blacklist" is not an array of string`); + } + for (let x of o) { + if (typeof x !== 'string') { + throw new TypeError(`"blacklist" is not an array of string`); + } + } + return o as string[]; +}; + +export const valueOf = (o: any): Settings => { + let settings = { ...DefaultSetting }; + if (Object.prototype.hasOwnProperty.call(o, 'keymaps')) { + settings.keymaps = keymapsValueOf(o.keymaps); + } + if (Object.prototype.hasOwnProperty.call(o, 'search')) { + settings.search = searchValueOf(o.search); + } + if (Object.prototype.hasOwnProperty.call(o, 'properties')) { + settings.properties = propertiesValueOf(o.properties); + } + if (Object.prototype.hasOwnProperty.call(o, 'blacklist')) { + settings.blacklist = blacklistValueOf(o.blacklist); + } + return settings; +}; + +export const DefaultSetting: Settings = { + keymaps: { + '0': { 'type': 'scroll.home' }, + ':': { 'type': 'command.show' }, + 'o': { 'type': 'command.show.open', 'alter': false }, + 'O': { 'type': 'command.show.open', 'alter': true }, + 't': { 'type': 'command.show.tabopen', 'alter': false }, + 'T': { 'type': 'command.show.tabopen', 'alter': true }, + 'w': { 'type': 'command.show.winopen', 'alter': false }, + 'W': { 'type': 'command.show.winopen', 'alter': true }, + 'b': { 'type': 'command.show.buffer' }, + 'a': { 'type': 'command.show.addbookmark', 'alter': true }, + 'k': { 'type': 'scroll.vertically', 'count': -1 }, + 'j': { 'type': 'scroll.vertically', 'count': 1 }, + 'h': { 'type': 'scroll.horizonally', 'count': -1 }, + 'l': { 'type': 'scroll.horizonally', 'count': 1 }, + '<C-U>': { 'type': 'scroll.pages', 'count': -0.5 }, + '<C-D>': { 'type': 'scroll.pages', 'count': 0.5 }, + '<C-B>': { 'type': 'scroll.pages', 'count': -1 }, + '<C-F>': { 'type': 'scroll.pages', 'count': 1 }, + 'gg': { 'type': 'scroll.top' }, + 'G': { 'type': 'scroll.bottom' }, + '$': { 'type': 'scroll.end' }, + 'd': { 'type': 'tabs.close' }, + 'D': { 'type': 'tabs.close.right' }, + '!d': { 'type': 'tabs.close.force' }, + 'u': { 'type': 'tabs.reopen' }, + 'K': { 'type': 'tabs.prev' }, + 'J': { 'type': 'tabs.next' }, + 'gT': { 'type': 'tabs.prev' }, + 'gt': { 'type': 'tabs.next' }, + 'g0': { 'type': 'tabs.first' }, + 'g$': { 'type': 'tabs.last' }, + '<C-6>': { 'type': 'tabs.prevsel' }, + 'r': { 'type': 'tabs.reload', 'cache': false }, + 'R': { 'type': 'tabs.reload', 'cache': true }, + 'zp': { 'type': 'tabs.pin.toggle' }, + 'zd': { 'type': 'tabs.duplicate' }, + 'zi': { 'type': 'zoom.in' }, + 'zo': { 'type': 'zoom.out' }, + 'zz': { 'type': 'zoom.neutral' }, + 'f': { 'type': 'follow.start', 'newTab': false, 'background': false }, + 'F': { 'type': 'follow.start', 'newTab': true, 'background': false }, + 'm': { 'type': 'mark.set.prefix' }, + '\'': { 'type': 'mark.jump.prefix' }, + 'H': { 'type': 'navigate.history.prev' }, + 'L': { 'type': 'navigate.history.next' }, + '[[': { 'type': 'navigate.link.prev' }, + ']]': { 'type': 'navigate.link.next' }, + 'gu': { 'type': 'navigate.parent' }, + 'gU': { 'type': 'navigate.root' }, + 'gi': { 'type': 'focus.input' }, + 'gf': { 'type': 'page.source' }, + 'gh': { 'type': 'page.home', 'newTab': false }, + 'gH': { 'type': 'page.home', 'newTab': true }, + 'y': { 'type': 'urls.yank' }, + 'p': { 'type': 'urls.paste', 'newTab': false }, + 'P': { 'type': 'urls.paste', 'newTab': true }, + '/': { 'type': 'find.start' }, + 'n': { 'type': 'find.next' }, + 'N': { 'type': 'find.prev' }, + '<S-Esc>': { 'type': 'addon.toggle.enabled' } + }, + search: { + default: 'google', + engines: { + 'google': 'https://google.com/search?q={}', + 'yahoo': 'https://search.yahoo.com/search?p={}', + 'bing': 'https://www.bing.com/search?q={}', + 'duckduckgo': 'https://duckduckgo.com/?q={}', + 'twitter': 'https://twitter.com/search?q={}', + 'wikipedia': 'https://en.wikipedia.org/w/index.php?search={}' + } + }, + properties: { + hintchars: 'abcdefghijklmnopqrstuvwxyz', + smoothscroll: false, + complete: 'sbh' + }, + blacklist: [] +}; diff --git a/src/shared/blacklists.js b/src/shared/blacklists.ts index 61720c3..61ee4de 100644 --- a/src/shared/blacklists.js +++ b/src/shared/blacklists.ts @@ -1,6 +1,6 @@ -import * as re from 'shared/utils/re'; +import * as re from './utils/re'; -const includes = (blacklist, url) => { +const includes = (blacklist: string[], url: string): boolean => { let u = new URL(url); return blacklist.some((item) => { if (!item.includes('/')) { diff --git a/src/shared/messages.js b/src/shared/messages.js deleted file mode 100644 index ddf3368..0000000 --- a/src/shared/messages.js +++ /dev/null @@ -1,71 +0,0 @@ -const onWebMessage = (listener) => { - window.addEventListener('message', (event) => { - let sender = event.source; - let message = null; - try { - message = JSON.parse(event.data); - } catch (e) { - // ignore unexpected message - return; - } - listener(message, sender); - }); -}; - -const onBackgroundMessage = (listener) => { - browser.runtime.onMessage.addListener(listener); -}; - -const onMessage = (listener) => { - onWebMessage(listener); - onBackgroundMessage(listener); -}; - -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, -}; diff --git a/src/shared/messages.ts b/src/shared/messages.ts new file mode 100644 index 0000000..41b0f0b --- /dev/null +++ b/src/shared/messages.ts @@ -0,0 +1,276 @@ +import * as operations from './operations'; + +export const BACKGROUND_OPERATION = 'background.operation'; + +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; + +// 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.js b/src/shared/operations.js deleted file mode 100644 index 8674f4d..0000000 --- a/src/shared/operations.js +++ /dev/null @@ -1,78 +0,0 @@ -export default { - // 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', -}; diff --git a/src/shared/operations.ts b/src/shared/operations.ts new file mode 100644 index 0000000..688c240 --- /dev/null +++ b/src/shared/operations.ts @@ -0,0 +1,447 @@ +// 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}`); + } +}; + +// 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 TypeError('unknown operation type: ' + o.type); +}; diff --git a/src/shared/properties.ts b/src/shared/properties.ts new file mode 100644 index 0000000..6315030 --- /dev/null +++ b/src/shared/properties.ts @@ -0,0 +1,50 @@ +export type Type = string | number | boolean; + +export class Def { + private name0: string; + + private description0: string; + + private defaultValue0: Type; + + constructor( + name: string, + description: string, + defaultValue: Type, + ) { + this.name0 = name; + this.description0 = description; + this.defaultValue0 = defaultValue; + } + + public get name(): string { + return this.name0; + } + + public get defaultValue(): Type { + return this.defaultValue0; + } + + public get description(): Type { + return this.description0; + } + + public get type(): string { + return typeof this.defaultValue; + } +} + +export const defs: Def[] = [ + new Def( + 'hintchars', + 'hint characters on follow mode', + 'abcdefghijklmnopqrstuvwxyz'), + new Def( + 'smoothscroll', + 'smooth scroll', + false), + new Def( + 'complete', + 'which are completed at the open page', + 'sbh'), +]; diff --git a/src/shared/property-defs.ts b/src/shared/property-defs.ts new file mode 100644 index 0000000..6315030 --- /dev/null +++ b/src/shared/property-defs.ts @@ -0,0 +1,50 @@ +export type Type = string | number | boolean; + +export class Def { + private name0: string; + + private description0: string; + + private defaultValue0: Type; + + constructor( + name: string, + description: string, + defaultValue: Type, + ) { + this.name0 = name; + this.description0 = description; + this.defaultValue0 = defaultValue; + } + + public get name(): string { + return this.name0; + } + + public get defaultValue(): Type { + return this.defaultValue0; + } + + public get description(): Type { + return this.description0; + } + + public get type(): string { + return typeof this.defaultValue; + } +} + +export const defs: Def[] = [ + new Def( + 'hintchars', + 'hint characters on follow mode', + 'abcdefghijklmnopqrstuvwxyz'), + new Def( + 'smoothscroll', + 'smooth scroll', + false), + new Def( + 'complete', + 'which are completed at the open page', + 'sbh'), +]; diff --git a/src/shared/settings/default.js b/src/shared/settings/default.js deleted file mode 100644 index 6523a74..0000000 --- a/src/shared/settings/default.js +++ /dev/null @@ -1,85 +0,0 @@ -export default { - source: 'json', - json: `{ - "keymaps": { - "0": { "type": "scroll.home" }, - ":": { "type": "command.show" }, - "o": { "type": "command.show.open", "alter": false }, - "O": { "type": "command.show.open", "alter": true }, - "t": { "type": "command.show.tabopen", "alter": false }, - "T": { "type": "command.show.tabopen", "alter": true }, - "w": { "type": "command.show.winopen", "alter": false }, - "W": { "type": "command.show.winopen", "alter": true }, - "b": { "type": "command.show.buffer" }, - "a": { "type": "command.show.addbookmark", "alter": true }, - "k": { "type": "scroll.vertically", "count": -1 }, - "j": { "type": "scroll.vertically", "count": 1 }, - "h": { "type": "scroll.horizonally", "count": -1 }, - "l": { "type": "scroll.horizonally", "count": 1 }, - "<C-U>": { "type": "scroll.pages", "count": -0.5 }, - "<C-D>": { "type": "scroll.pages", "count": 0.5 }, - "<C-B>": { "type": "scroll.pages", "count": -1 }, - "<C-F>": { "type": "scroll.pages", "count": 1 }, - "gg": { "type": "scroll.top" }, - "G": { "type": "scroll.bottom" }, - "$": { "type": "scroll.end" }, - "d": { "type": "tabs.close" }, - "D": { "type": "tabs.close.right" }, - "!d": { "type": "tabs.close.force" }, - "u": { "type": "tabs.reopen" }, - "K": { "type": "tabs.prev", "count": 1 }, - "J": { "type": "tabs.next", "count": 1 }, - "gT": { "type": "tabs.prev", "count": 1 }, - "gt": { "type": "tabs.next", "count": 1 }, - "g0": { "type": "tabs.first" }, - "g$": { "type": "tabs.last" }, - "<C-6>": { "type": "tabs.prevsel" }, - "r": { "type": "tabs.reload", "cache": false }, - "R": { "type": "tabs.reload", "cache": true }, - "zp": { "type": "tabs.pin.toggle" }, - "zd": { "type": "tabs.duplicate" }, - "zi": { "type": "zoom.in" }, - "zo": { "type": "zoom.out" }, - "zz": { "type": "zoom.neutral" }, - "f": { "type": "follow.start", "newTab": false }, - "F": { "type": "follow.start", "newTab": true, "background": false }, - "m": { "type": "mark.set.prefix" }, - "'": { "type": "mark.jump.prefix" }, - "H": { "type": "navigate.history.prev" }, - "L": { "type": "navigate.history.next" }, - "[[": { "type": "navigate.link.prev" }, - "]]": { "type": "navigate.link.next" }, - "gu": { "type": "navigate.parent" }, - "gU": { "type": "navigate.root" }, - "gi": { "type": "focus.input" }, - "gf": { "type": "page.source" }, - "gh": { "type": "page.home" }, - "gH": { "type": "page.home", "newTab": true }, - "y": { "type": "urls.yank" }, - "p": { "type": "urls.paste", "newTab": false }, - "P": { "type": "urls.paste", "newTab": true }, - "/": { "type": "find.start" }, - "n": { "type": "find.next" }, - "N": { "type": "find.prev" }, - "<S-Esc>": { "type": "addon.toggle.enabled" } - }, - "search": { - "default": "google", - "engines": { - "google": "https://google.com/search?q={}", - "yahoo": "https://search.yahoo.com/search?p={}", - "bing": "https://www.bing.com/search?q={}", - "duckduckgo": "https://duckduckgo.com/?q={}", - "twitter": "https://twitter.com/search?q={}", - "wikipedia": "https://en.wikipedia.org/w/index.php?search={}" - } - }, - "properties": { - "hintchars": "abcdefghijklmnopqrstuvwxyz", - "smoothscroll": false, - "complete": "sbh" - }, - "blacklist": [ - ] -}`, -}; diff --git a/src/shared/settings/properties.js b/src/shared/settings/properties.js deleted file mode 100644 index f8e61a0..0000000 --- a/src/shared/settings/properties.js +++ /dev/null @@ -1,24 +0,0 @@ -// describe types of a propety as: -// mystr: 'string', -// mynum: 'number', -// mybool: 'boolean', -const types = { - hintchars: 'string', - smoothscroll: 'boolean', - complete: 'string', -}; - -// describe default values of a property -const defaults = { - hintchars: 'abcdefghijklmnopqrstuvwxyz', - smoothscroll: false, - complete: 'sbh', -}; - -const docs = { - hintchars: 'hint characters on follow mode', - smoothscroll: 'smooth scroll', - complete: 'which are completed at the open page', -}; - -export { types, defaults, docs }; diff --git a/src/shared/settings/storage.js b/src/shared/settings/storage.js deleted file mode 100644 index 5dce3b0..0000000 --- a/src/shared/settings/storage.js +++ /dev/null @@ -1,32 +0,0 @@ -import DefaultSettings from './default'; -import * as settingsValues from './values'; - -const loadRaw = async() => { - let { settings } = await browser.storage.local.get('settings'); - if (!settings) { - return DefaultSettings; - } - return { ...DefaultSettings, ...settings }; -}; - -const loadValue = async() => { - let settings = await loadRaw(); - let value = JSON.parse(DefaultSettings.json); - if (settings.source === 'json') { - value = settingsValues.valueFromJson(settings.json); - } else if (settings.source === 'form') { - value = settingsValues.valueFromForm(settings.form); - } - if (!value.properties) { - value.properties = {}; - } - return { ...settingsValues.valueFromJson(DefaultSettings.json), ...value }; -}; - -const save = (settings) => { - return browser.storage.local.set({ - settings, - }); -}; - -export { loadRaw, loadValue, save }; diff --git a/src/shared/settings/validator.js b/src/shared/settings/validator.js deleted file mode 100644 index a800a52..0000000 --- a/src/shared/settings/validator.js +++ /dev/null @@ -1,76 +0,0 @@ -import operations from 'shared/operations'; -import * as properties from './properties'; - -const VALID_TOP_KEYS = ['keymaps', 'search', 'blacklist', 'properties']; -const VALID_OPERATION_VALUES = Object.keys(operations).map((key) => { - return operations[key]; -}); - -const validateInvalidTopKeys = (settings) => { - let invalidKey = Object.keys(settings).find((key) => { - return !VALID_TOP_KEYS.includes(key); - }); - if (invalidKey) { - throw Error(`Unknown key: "${invalidKey}"`); - } -}; - -const validateKeymaps = (keymaps) => { - for (let key of Object.keys(keymaps)) { - let value = keymaps[key]; - if (!VALID_OPERATION_VALUES.includes(value.type)) { - throw Error(`Unknown operation: "${value.type}"`); - } - } -}; - -const validateSearch = (search) => { - let engines = search.engines; - for (let key of Object.keys(engines)) { - if ((/\s/).test(key)) { - throw new Error( - `While space in search engine name is not allowed: "${key}"` - ); - } - let url = engines[key]; - if (!url.match(/{}/)) { - throw new Error(`No {}-placeholders in URL of "${key}"`); - } - if (url.match(/{}/g).length > 1) { - throw new Error(`Multiple {}-placeholders in URL of "${key}"`); - } - } - - if (!search.default) { - throw new Error(`Default engine is not set`); - } - if (!Object.keys(engines).includes(search.default)) { - throw new Error(`Default engine "${search.default}" not found`); - } -}; - -const validateProperties = (props) => { - for (let name of Object.keys(props)) { - if (!properties.types[name]) { - throw new Error(`Unknown property name: "${name}"`); - } - if (typeof props[name] !== properties.types[name]) { - throw new Error(`Invalid type for property: "${name}"`); - } - } -}; - -const validate = (settings) => { - validateInvalidTopKeys(settings); - if (settings.keymaps) { - validateKeymaps(settings.keymaps); - } - if (settings.search) { - validateSearch(settings.search); - } - if (settings.properties) { - validateProperties(settings.properties); - } -}; - -export { validate }; diff --git a/src/shared/settings/values.js b/src/shared/settings/values.js deleted file mode 100644 index 9828af6..0000000 --- a/src/shared/settings/values.js +++ /dev/null @@ -1,108 +0,0 @@ -import * as properties from './properties'; - -const operationFromFormName = (name) => { - let [type, argStr] = name.split('?'); - let args = {}; - if (argStr) { - args = JSON.parse(argStr); - } - return { type, ...args }; -}; - -const operationToFormName = (op) => { - let type = op.type; - let args = { ...op }; - delete args.type; - - if (Object.keys(args).length === 0) { - return type; - } - return op.type + '?' + JSON.stringify(args); -}; - -const valueFromJson = (json) => { - return JSON.parse(json); -}; - -const valueFromForm = (form) => { - let keymaps = undefined; - if (form.keymaps) { - keymaps = {}; - for (let name of Object.keys(form.keymaps)) { - let keys = form.keymaps[name]; - keymaps[keys] = operationFromFormName(name); - } - } - - let search = undefined; - if (form.search) { - search = { default: form.search.default }; - - if (form.search.engines) { - search.engines = {}; - for (let [name, url] of form.search.engines) { - search.engines[name] = url; - } - } - } - - return { - keymaps, - search, - blacklist: form.blacklist, - properties: form.properties - }; -}; - -const jsonFromValue = (value) => { - return JSON.stringify(value, undefined, 2); -}; - -const formFromValue = (value, allowedOps) => { - let keymaps = undefined; - - if (value.keymaps) { - let allowedSet = new Set(allowedOps); - - keymaps = {}; - for (let keys of Object.keys(value.keymaps)) { - let op = operationToFormName(value.keymaps[keys]); - if (allowedSet.has(op)) { - keymaps[op] = keys; - } - } - } - - let search = undefined; - if (value.search) { - search = { default: value.search.default }; - if (value.search.engines) { - search.engines = Object.keys(value.search.engines).map((name) => { - return [name, value.search.engines[name]]; - }); - } - } - - let formProperties = { ...properties.defaults, ...value.properties }; - - return { - keymaps, - search, - blacklist: value.blacklist, - properties: formProperties, - }; -}; - -const jsonFromForm = (form) => { - return jsonFromValue(valueFromForm(form)); -}; - -const formFromJson = (json, allowedOps) => { - let value = valueFromJson(json); - return formFromValue(value, allowedOps); -}; - -export { - valueFromJson, valueFromForm, jsonFromValue, formFromValue, - jsonFromForm, formFromJson -}; diff --git a/src/shared/urls.js b/src/shared/urls.ts index 94b1220..18349c8 100644 --- a/src/shared/urls.js +++ b/src/shared/urls.ts @@ -1,11 +1,11 @@ -const trimStart = (str) => { +const trimStart = (str: string): string => { // NOTE String.trimStart is available on Firefox 61 return str.replace(/^\s+/, ''); }; const SUPPORTED_PROTOCOLS = ['http:', 'https:', 'ftp:', 'mailto:', 'about:']; -const searchUrl = (keywords, searchSettings) => { +const searchUrl = (keywords: string, searchSettings: any): string => { try { let u = new URL(keywords); if (SUPPORTED_PROTOCOLS.includes(u.protocol.toLowerCase())) { @@ -28,7 +28,7 @@ const searchUrl = (keywords, searchSettings) => { return template.replace('{}', encodeURIComponent(query)); }; -const normalizeUrl = (url) => { +const normalizeUrl = (url: string): string => { try { let u = new URL(url); if (SUPPORTED_PROTOCOLS.includes(u.protocol.toLowerCase())) { diff --git a/src/shared/utils/dom.js b/src/shared/utils/dom.ts index 974d534..c1f2190 100644 --- a/src/shared/utils/dom.js +++ b/src/shared/utils/dom.ts @@ -1,16 +1,24 @@ -const isContentEditable = (element) => { - return element.hasAttribute('contenteditable') && ( - element.getAttribute('contenteditable').toLowerCase() === 'true' || - element.getAttribute('contenteditable').toLowerCase() === '' - ); +const isContentEditable = (element: Element): boolean => { + let value = element.getAttribute('contenteditable'); + if (value === null) { + return false; + } + return value.toLowerCase() === 'true' || value.toLowerCase() === ''; }; -const rectangleCoordsRect = (coords) => { +interface Rect { + left: number; + top: number; + right: number; + bottom: number; +} + +const rectangleCoordsRect = (coords: string): Rect => { let [left, top, right, bottom] = coords.split(',').map(n => Number(n)); return { left, top, right, bottom }; }; -const circleCoordsRect = (coords) => { +const circleCoordsRect = (coords: string): Rect => { let [x, y, r] = coords.split(',').map(n => Number(n)); return { left: x - r, @@ -20,7 +28,7 @@ const circleCoordsRect = (coords) => { }; }; -const polygonCoordsRect = (coords) => { +const polygonCoordsRect = (coords: string): Rect => { let params = coords.split(','); let minx = Number(params[0]), maxx = Number(params[0]), @@ -46,18 +54,24 @@ const polygonCoordsRect = (coords) => { return { left: minx, top: miny, right: maxx, bottom: maxy }; }; -const viewportRect = (e) => { +const viewportRect = (e: Element): Rect => { if (e.tagName !== 'AREA') { return e.getBoundingClientRect(); } - let mapElement = e.parentNode; - let imgElement = document.querySelector(`img[usemap="#${mapElement.name}"]`); + let mapElement = e.parentNode as HTMLMapElement; + let imgElement = document.querySelector( + `img[usemap="#${mapElement.name}"]` + ) as HTMLImageElement; let { left: mapLeft, top: mapTop } = imgElement.getBoundingClientRect(); let coords = e.getAttribute('coords'); + if (!coords) { + return e.getBoundingClientRect(); + } + let rect = { left: 0, top: 0, right: 0, bottom: 0 }; switch (e.getAttribute('shape')) { case 'rect': @@ -81,7 +95,7 @@ const viewportRect = (e) => { }; }; -const isVisible = (element) => { +const isVisible = (element: Element): boolean => { let rect = element.getBoundingClientRect(); let style = window.getComputedStyle(element); @@ -94,7 +108,8 @@ const isVisible = (element) => { if (window.innerWidth < rect.left && window.innerHeight < rect.top) { return false; } - if (element.nodeName === 'INPUT' && element.type.toLowerCase() === 'hidden') { + if (element instanceof HTMLInputElement && + element.type.toLowerCase() === 'hidden') { return false; } diff --git a/src/shared/utils/keys.js b/src/shared/utils/keys.ts index f024069..e9b0365 100644 --- a/src/shared/utils/keys.js +++ b/src/shared/utils/keys.ts @@ -1,4 +1,12 @@ -const modifiedKeyName = (name) => { +export interface Key { + key: string; + shiftKey: boolean | undefined; + ctrlKey: boolean | undefined; + altKey: boolean | undefined; + metaKey: boolean | undefined; +} + +const modifiedKeyName = (name: string): string => { if (name === ' ') { return 'Space'; } @@ -10,7 +18,7 @@ const modifiedKeyName = (name) => { return name; }; -const fromKeyboardEvent = (e) => { +const fromKeyboardEvent = (e: KeyboardEvent): Key => { let key = modifiedKeyName(e.key); let shift = e.shiftKey; if (key.length === 1 && key.toUpperCase() === key.toLowerCase()) { @@ -28,7 +36,7 @@ const fromKeyboardEvent = (e) => { }; }; -const fromMapKey = (key) => { +const fromMapKey = (key: string): Key => { if (key.startsWith('<') && key.endsWith('>')) { let inner = key.slice(1, -1); let shift = inner.includes('S-'); @@ -55,8 +63,10 @@ const fromMapKey = (key) => { }; }; -const fromMapKeys = (keys) => { - const fromMapKeysRecursive = (remainings, mappedKeys) => { +const fromMapKeys = (keys: string): Key[] => { + const fromMapKeysRecursive = ( + remainings: string, mappedKeys: Key[], + ): Key[] => { if (remainings.length === 0) { return mappedKeys; } @@ -78,7 +88,7 @@ const fromMapKeys = (keys) => { return fromMapKeysRecursive(keys, []); }; -const equals = (e1, e2) => { +const equals = (e1: Key, e2: Key): boolean => { return e1.key === e2.key && e1.ctrlKey === e2.ctrlKey && e1.metaKey === e2.metaKey && diff --git a/src/shared/utils/re.js b/src/shared/utils/re.ts index 7db9091..34f4fa6 100644 --- a/src/shared/utils/re.js +++ b/src/shared/utils/re.ts @@ -1,4 +1,4 @@ -const fromWildcard = (pattern) => { +const fromWildcard = (pattern: string): RegExp => { let regexStr = '^' + pattern.replace(/\*/g, '.*') + '$'; return new RegExp(regexStr); }; diff --git a/test/background/domains/GlobalMark.test.js b/test/background/domains/GlobalMark.test.js deleted file mode 100644 index ed636e9..0000000 --- a/test/background/domains/GlobalMark.test.js +++ /dev/null @@ -1,11 +0,0 @@ -import GlobalMark from 'background/domains/GlobalMark'; - -describe('background/domains/global-mark', () => { - describe('constructor and getter', () => { - let mark = new GlobalMark(1, 'http://example.com', 10, 30); - expect(mark.tabId).to.equal(1); - expect(mark.url).to.equal('http://example.com'); - expect(mark.x).to.equal(10); - expect(mark.y).to.equal(30); - }); -}); diff --git a/test/background/infrastructures/MemoryStorage.test.js b/test/background/infrastructures/MemoryStorage.test.ts index 95d3780..95d3780 100644 --- a/test/background/infrastructures/MemoryStorage.test.js +++ b/test/background/infrastructures/MemoryStorage.test.ts diff --git a/test/background/repositories/Mark.test.js b/test/background/repositories/Mark.test.ts index 2a5b099..167e512 100644 --- a/test/background/repositories/Mark.test.js +++ b/test/background/repositories/Mark.test.ts @@ -9,12 +9,11 @@ describe('background/repositories/mark', () => { }); it('get and set', async() => { - let mark = new GlobalMark(1, 'http://example.com', 10, 30); + let mark = { tabId: 1, url: 'http://example.com', x: 10, y: 30 }; repository.setMark('A', mark); let got = await repository.getMark('A'); - expect(got).to.be.a('object'); expect(got.tabId).to.equal(1); expect(got.url).to.equal('http://example.com'); expect(got.x).to.equal(10); diff --git a/test/background/repositories/Version.js b/test/background/repositories/Version.js deleted file mode 100644 index c7fa88b..0000000 --- a/test/background/repositories/Version.js +++ /dev/null @@ -1,34 +0,0 @@ -import VersionRepository from 'background/repositories/Version'; - -describe("background/repositories/version", () => { - let versionRepository; - - beforeEach(() => { - versionRepository = new VersionRepository; - }); - - describe('#get', () => { - beforeEach(() => { - return browser.storage.local.remove('version'); - }); - - it('loads saved version', async() => { - await browser.storage.local.set({ version: '1.2.3' }); - let version = await this.versionRepository.get(); - expect(version).to.equal('1.2.3'); - }); - - it('returns undefined if no versions in storage', async() => { - let version = await storage.load(); - expect(version).to.be.a('undefined'); - }); - }); - - describe('#update', () => { - it('saves version string', async() => { - await versionRepository.update('2.3.4'); - let { version } = await browser.storage.local.get('version'); - expect(version).to.equal('2.3.4'); - }); - }); -}); diff --git a/test/background/usecases/filters.test.js b/test/background/usecases/filters.test.ts index bdfb0be..bdfb0be 100644 --- a/test/background/usecases/filters.test.js +++ b/test/background/usecases/filters.test.ts diff --git a/test/background/usecases/parsers.test.js b/test/background/usecases/parsers.test.js deleted file mode 100644 index 17b034b..0000000 --- a/test/background/usecases/parsers.test.js +++ /dev/null @@ -1,47 +0,0 @@ -import * as parsers from 'background/usecases/parsers'; - -describe("shared/commands/parsers", () => { - describe("#parsers.parseSetOption", () => { - it('parse set string', () => { - let [key, value] = parsers.parseSetOption('encoding=utf-8', { encoding: 'string' }); - expect(key).to.equal('encoding'); - expect(value).to.equal('utf-8'); - }); - - it('parse set empty string', () => { - let [key, value] = parsers.parseSetOption('encoding=', { encoding: 'string' }); - expect(key).to.equal('encoding'); - expect(value).to.equal(''); - }); - - it('parse set string', () => { - let [key, value] = parsers.parseSetOption('history=50', { history: 'number' }); - expect(key).to.equal('history'); - expect(value).to.equal(50); - }); - - it('parse set boolean', () => { - let [key, value] = parsers.parseSetOption('paste', { paste: 'boolean' }); - expect(key).to.equal('paste'); - expect(value).to.be.true; - - [key, value] = parsers.parseSetOption('nopaste', { paste: 'boolean' }); - expect(key).to.equal('paste'); - expect(value).to.be.false; - }); - - it('throws error on unknown property', () => { - expect(() => parsers.parseSetOption('charset=utf-8', {})).to.throw(Error, 'Unknown'); - expect(() => parsers.parseSetOption('smoothscroll', {})).to.throw(Error, 'Unknown'); - expect(() => parsers.parseSetOption('nosmoothscroll', {})).to.throw(Error, 'Unknown'); - }) - - it('throws error on invalid property', () => { - expect(() => parsers.parseSetOption('charset=utf-8', { charset: 'number' })).to.throw(Error, 'Not number'); - expect(() => parsers.parseSetOption('charset=utf-8', { charset: 'boolean' })).to.throw(Error, 'Invalid'); - expect(() => parsers.parseSetOption('charset=', { charset: 'boolean' })).to.throw(Error, 'Invalid'); - expect(() => parsers.parseSetOption('smoothscroll', { smoothscroll: 'string' })).to.throw(Error, 'Invalid'); - expect(() => parsers.parseSetOption('smoothscroll', { smoothscroll: 'number' })).to.throw(Error, 'Invalid'); - }) - }); -}); diff --git a/test/background/usecases/parsers.test.ts b/test/background/usecases/parsers.test.ts new file mode 100644 index 0000000..f3a64eb --- /dev/null +++ b/test/background/usecases/parsers.test.ts @@ -0,0 +1,34 @@ +import * as parsers from 'background/usecases/parsers'; + +describe("shared/commands/parsers", () => { + describe("#parsers.parseSetOption", () => { + it('parse set string', () => { + let [key, value] = parsers.parseSetOption('hintchars=abcdefgh'); + expect(key).to.equal('hintchars'); + expect(value).to.equal('abcdefgh'); + }); + + it('parse set empty string', () => { + let [key, value] = parsers.parseSetOption('hintchars='); + expect(key).to.equal('hintchars'); + expect(value).to.equal(''); + }); + + it('parse set boolean', () => { + let [key, value] = parsers.parseSetOption('smoothscroll'); + expect(key).to.equal('smoothscroll'); + expect(value).to.be.true; + + [key, value] = parsers.parseSetOption('nosmoothscroll'); + expect(key).to.equal('smoothscroll'); + expect(value).to.be.false; + }); + + it('throws error on unknown property', () => { + expect(() => parsers.parseSetOption('encoding=utf-8')).to.throw(Error, 'Unknown'); + expect(() => parsers.parseSetOption('paste')).to.throw(Error, 'Unknown'); + expect(() => parsers.parseSetOption('nopaste')).to.throw(Error, 'Unknown'); + expect(() => parsers.parseSetOption('smoothscroll=yes')).to.throw(Error, 'Invalid argument'); + }); + }); +}); diff --git a/test/console/actions/console.test.js b/test/console/actions/console.test.ts index 10cd9fe..e45d008 100644 --- a/test/console/actions/console.test.js +++ 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/components/console/Completion.test.jsx b/test/console/components/console/Completion.test.tsx index 16bf11a..16bf11a 100644 --- a/test/console/components/console/Completion.test.jsx +++ b/test/console/components/console/Completion.test.tsx diff --git a/test/console/reducers/console.test.js b/test/console/reducers/console.test.ts index d5a38cf..47e7daf 100644 --- a/test/console/reducers/console.test.js +++ 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", () => { diff --git a/test/content/actions/follow-controller.test.js b/test/content/actions/follow-controller.test.ts index 718a90a..a4b1710 100644 --- a/test/content/actions/follow-controller.test.js +++ 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.js b/test/content/actions/input.test.ts index fe9db5f..33238a5 100644 --- a/test/content/actions/input.test.js +++ 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.js b/test/content/actions/mark.test.ts index adbf06b..6c6d59e 100644 --- a/test/content/actions/mark.test.js +++ 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.js b/test/content/actions/setting.test.js deleted file mode 100644 index 10f6807..0000000 --- a/test/content/actions/setting.test.js +++ /dev/null @@ -1,35 +0,0 @@ -import actions from 'content/actions'; -import * as settingActions from 'content/actions/setting'; - -describe("setting actions", () => { - describe("set", () => { - it('create SETTING_SET action', () => { - let action = settingActions.set({ red: 'apple', yellow: 'banana' }); - expect(action.type).to.equal(actions.SETTING_SET); - expect(action.value.red).to.equal('apple'); - expect(action.value.yellow).to.equal('banana'); - expect(action.value.keymaps).to.be.empty; - }); - - it('converts keymaps', () => { - let action = settingActions.set({ - keymaps: { - 'dd': 'remove current tab', - 'z<C-A>': 'increment', - } - }); - let keymaps = action.value.keymaps; - let map = new Map(keymaps); - expect(map).to.have.deep.all.keys( - [ - [{ key: 'Esc', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }], - [{ key: '[', shiftKey: false, ctrlKey: true, altKey: false, metaKey: false }], - [{ key: 'd', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, - { key: 'd', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }], - [{ key: 'z', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, - { key: 'a', shiftKey: false, ctrlKey: true, altKey: false, metaKey: false }], - ] - ); - }); - }); -}); diff --git a/test/content/actions/setting.test.ts b/test/content/actions/setting.test.ts new file mode 100644 index 0000000..c831433 --- /dev/null +++ b/test/content/actions/setting.test.ts @@ -0,0 +1,43 @@ +import * as actions from 'content/actions'; +import * as settingActions from 'content/actions/setting'; + +describe("setting actions", () => { + describe("set", () => { + it('create SETTING_SET action', () => { + let action = settingActions.set({ + keymaps: { + 'dd': 'remove current tab', + 'z<C-A>': 'increment', + }, + search: { + default: "google", + engines: { + google: 'https://google.com/search?q={}', + } + }, + properties: { + hintchars: 'abcd1234', + }, + blacklist: [], + }); + expect(action.type).to.equal(actions.SETTING_SET); + expect(action.settings.properties.hintchars).to.equal('abcd1234'); + }); + + it('overrides cancel keys', () => { + let action = settingActions.set({ + keymaps: { + "k": { "type": "scroll.vertically", "count": -1 }, + "j": { "type": "scroll.vertically", "count": 1 }, + } + }); + let keymaps = action.settings.keymaps; + expect(action.settings.keymaps).to.deep.equals({ + "k": { type: "scroll.vertically", count: -1 }, + "j": { type: "scroll.vertically", count: 1 }, + '<Esc>': { type: 'cancel' }, + '<C-[>': { type: 'cancel' }, + }); + }); + }); +}); diff --git a/test/content/components/common/follow.test.js b/test/content/components/common/follow.test.ts index 90d6cf5..90d6cf5 100644 --- a/test/content/components/common/follow.test.js +++ b/test/content/components/common/follow.test.ts diff --git a/test/content/components/common/hint.test.js b/test/content/components/common/hint.test.ts index 42d571f..42d571f 100644 --- a/test/content/components/common/hint.test.js +++ b/test/content/components/common/hint.test.ts diff --git a/test/content/components/common/input.test.js b/test/content/components/common/input.test.ts index 2ba5507..f3a943c 100644 --- a/test/content/components/common/input.test.js +++ 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/hint-key-producer.test.js b/test/content/hint-key-producer.test.ts index dcf477d..dcf477d 100644 --- a/test/content/hint-key-producer.test.js +++ b/test/content/hint-key-producer.test.ts diff --git a/test/content/navigates.test.js b/test/content/navigates.test.ts index 1d73344..1d73344 100644 --- a/test/content/navigates.test.js +++ b/test/content/navigates.test.ts diff --git a/test/content/reducers/addon.test.js b/test/content/reducers/addon.test.ts index d4eb845..fb05244 100644 --- a/test/content/reducers/addon.test.js +++ 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.js b/test/content/reducers/find.test.ts index a8c30d7..66a2c67 100644 --- a/test/content/reducers/find.test.js +++ 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.js b/test/content/reducers/follow-controller.test.ts index 8a4c2d4..39f326c 100644 --- a/test/content/reducers/follow-controller.test.js +++ 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.js b/test/content/reducers/input.test.ts index 0011943..f892201 100644 --- a/test/content/reducers/input.test.js +++ 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.js b/test/content/reducers/mark.test.ts index 76efbf7..1a51c3e 100644 --- a/test/content/reducers/mark.test.js +++ 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.js b/test/content/reducers/setting.test.js deleted file mode 100644 index 4e4c095..0000000 --- a/test/content/reducers/setting.test.js +++ /dev/null @@ -1,17 +0,0 @@ -import actions from 'content/actions'; -import settingReducer from 'content/reducers/setting'; - -describe("content setting reducer", () => { - it('return the initial state', () => { - let state = settingReducer(undefined, {}); - expect(state.keymaps).to.be.empty; - }); - - it('return next state for SETTING_SET', () => { - let newSettings = { red: 'apple', yellow: 'banana' }; - let action = { type: actions.SETTING_SET, value: newSettings }; - let state = settingReducer(undefined, action); - expect(state).to.deep.equal(newSettings); - expect(state).not.to.equal(newSettings); // assert deep copy - }); -}); diff --git a/test/content/reducers/setting.test.ts b/test/content/reducers/setting.test.ts new file mode 100644 index 0000000..9b332aa --- /dev/null +++ b/test/content/reducers/setting.test.ts @@ -0,0 +1,31 @@ +import * as actions from 'content/actions'; +import settingReducer from 'content/reducers/setting'; + +describe("content setting reducer", () => { + it('return the initial state', () => { + let state = settingReducer(undefined, {}); + expect(state.keymaps).to.be.empty; + }); + + it('return next state for SETTING_SET', () => { + let newSettings = { red: 'apple', yellow: 'banana' }; + let action = { + type: actions.SETTING_SET, + settings: { + keymaps: { + "zz": { type: "zoom.neutral" }, + "<S-Esc>": { "type": "addon.toggle.enabled" } + }, + "blacklist": [] + } + } + let state = settingReducer(undefined, action); + expect(state.keymaps).to.have.deep.all.members([ + { key: [{ key: 'z', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + { key: 'z', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }], + op: { type: 'zoom.neutral' }}, + { key: [{ key: 'Esc', shiftKey: true, ctrlKey: false, altKey: false, metaKey: false }], + op: { type: 'addon.toggle.enabled' }}, + ]); + }); +}); diff --git a/test/main.js b/test/main.ts index 3aeae69..3aeae69 100644 --- a/test/main.js +++ b/test/main.ts diff --git a/test/settings/components/form/BlacklistForm.test.jsx b/test/settings/components/form/BlacklistForm.test.tsx index 2be5d96..2be5d96 100644 --- a/test/settings/components/form/BlacklistForm.test.jsx +++ b/test/settings/components/form/BlacklistForm.test.tsx diff --git a/test/settings/components/form/KeymapsForm.test.jsx b/test/settings/components/form/KeymapsForm.test.tsx index 6ac57c9..dc2322b 100644 --- a/test/settings/components/form/KeymapsForm.test.jsx +++ b/test/settings/components/form/KeymapsForm.test.tsx @@ -2,15 +2,17 @@ import React from 'react'; import ReactDOM from 'react-dom'; import ReactTestRenderer from 'react-test-renderer'; import ReactTestUtils from 'react-dom/test-utils'; -import KeymapsForm from 'settings/components/form/KeymapsForm' +import KeymapsForm from '../../../../src/settings/components/form/KeymapsForm' +import { FormKeymaps } from 'shared/SettingData'; +import { expect } from 'chai'; describe("settings/form/KeymapsForm", () => { describe('render', () => { it('renders keymap fields', () => { - let root = ReactTestRenderer.create(<KeymapsForm value={{ + let root = ReactTestRenderer.create(<KeymapsForm value={FormKeymaps.valueOf({ 'scroll.vertically?{"count":1}': 'j', 'scroll.vertically?{"count":-1}': 'k', - }} />).root + })} />).root let inputj = root.findByProps({ id: 'scroll.vertically?{"count":1}' }); let inputk = root.findByProps({ id: 'scroll.vertically?{"count":-1}' }); @@ -46,12 +48,12 @@ describe("settings/form/KeymapsForm", () => { it('invokes onChange event on edit', (done) => { ReactTestUtils.act(() => { ReactDOM.render(<KeymapsForm - value={{ + value={FormKeymaps.valueOf({ 'scroll.vertically?{"count":1}': 'j', 'scroll.vertically?{"count":-1}': 'k', - }} + })} onChange={value => { - expect(value['scroll.vertically?{"count":1}']).to.equal('jjj'); + expect(value.toJSON()['scroll.vertically?{"count":1}']).to.equal('jjj'); done(); }} />, container); }); diff --git a/test/settings/components/form/PropertiesForm.test.jsx b/test/settings/components/form/PropertiesForm.test.tsx index 80f60d2..80f60d2 100644 --- a/test/settings/components/form/PropertiesForm.test.jsx +++ b/test/settings/components/form/PropertiesForm.test.tsx diff --git a/test/settings/components/form/SearchEngineForm.test.jsx b/test/settings/components/form/SearchEngineForm.test.tsx index 06822f2..0e6b17d 100644 --- a/test/settings/components/form/SearchEngineForm.test.jsx +++ b/test/settings/components/form/SearchEngineForm.test.tsx @@ -3,14 +3,15 @@ import ReactDOM from 'react-dom'; import ReactTestRenderer from 'react-test-renderer'; import ReactTestUtils from 'react-dom/test-utils'; import SearchForm from 'settings/components/form/SearchForm' +import { FormSearch } from 'shared/SettingData'; describe("settings/form/SearchForm", () => { describe('render', () => { it('renders SearchForm', () => { - let root = ReactTestRenderer.create(<SearchForm value={{ + let root = ReactTestRenderer.create(<SearchForm value={FormSearch.valueOf({ default: 'google', engines: [['google', 'google.com'], ['yahoo', 'yahoo.com']], - }} />).root; + })} />).root; let names = root.findAllByProps({ name: 'name' }); expect(names).to.have.lengthOf(2); @@ -22,28 +23,6 @@ describe("settings/form/SearchForm", () => { expect(urls[0].props.value).to.equal('google.com'); expect(urls[1].props.value).to.equal('yahoo.com'); }); - - it('renders blank value', () => { - let root = ReactTestRenderer.create(<SearchForm />).root; - - let names = root.findAllByProps({ name: 'name' }); - expect(names).to.be.empty; - - let urls = root.findAllByProps({ name: 'url' }); - expect(urls).to.be.empty; - }); - - it('renders blank engines', () => { - let root = ReactTestRenderer.create( - <SearchForm value={{ default: 'google' }} />, - ).root; - - let names = root.findAllByProps({ name: 'name' }); - expect(names).to.be.empty; - - let urls = root.findAllByProps({ name: 'url' }); - expect(urls).to.be.empty; - }); }); describe('onChange event', () => { @@ -62,14 +41,15 @@ describe("settings/form/SearchForm", () => { it('invokes onChange event on edit', (done) => { ReactTestUtils.act(() => { ReactDOM.render(<SearchForm - value={{ + value={FormSearch.valueOf({ default: 'google', engines: [['google', 'google.com'], ['yahoo', 'yahoo.com']] - }} + })} onChange={value => { - expect(value.default).to.equal('louvre'); - expect(value.engines).to.have.lengthOf(2) - expect(value.engines).to.have.deep.members( + let json = value.toJSON(); + expect(json.default).to.equal('louvre'); + expect(json.engines).to.have.lengthOf(2) + expect(json.engines).to.have.deep.members( [['louvre', 'google.com'], ['yahoo', 'yahoo.com']] ); done(); @@ -87,14 +67,15 @@ describe("settings/form/SearchForm", () => { it('invokes onChange event on delete', (done) => { ReactTestUtils.act(() => { - ReactDOM.render(<SearchForm value={{ + ReactDOM.render(<SearchForm value={FormSearch.valueOf({ default: 'yahoo', engines: [['louvre', 'google.com'], ['yahoo', 'yahoo.com']] - }} + })} onChange={value => { - expect(value.default).to.equal('yahoo'); - expect(value.engines).to.have.lengthOf(1) - expect(value.engines).to.have.deep.members( + let json = value.toJSON(); + expect(json.default).to.equal('yahoo'); + expect(json.engines).to.have.lengthOf(1) + expect(json.engines).to.have.deep.members( [['yahoo', 'yahoo.com']] ); done(); @@ -107,14 +88,15 @@ describe("settings/form/SearchForm", () => { it('invokes onChange event on add', (done) => { ReactTestUtils.act(() => { - ReactDOM.render(<SearchForm value={{ + ReactDOM.render(<SearchForm value={FormSearch.valueOf({ default: 'yahoo', engines: [['google', 'google.com']] - }} + })} onChange={value => { - expect(value.default).to.equal('yahoo'); - expect(value.engines).to.have.lengthOf(2) - expect(value.engines).to.have.deep.members( + let json = value.toJSON(); + expect(json.default).to.equal('yahoo'); + expect(json.engines).to.have.lengthOf(2) + expect(json.engines).to.have.deep.members( [['google', 'google.com'], ['', '']], ); done(); diff --git a/test/settings/components/ui/input.test.jsx b/test/settings/components/ui/input.test.tsx index 432efcb..432efcb 100644 --- a/test/settings/components/ui/input.test.jsx +++ b/test/settings/components/ui/input.test.tsx diff --git a/test/settings/reducers/setting.test.js b/test/settings/reducers/setting.test.ts index c1a1648..376d66e 100644 --- a/test/settings/reducers/setting.test.js +++ b/test/settings/reducers/setting.test.ts @@ -1,11 +1,10 @@ -import actions from 'settings/actions'; +import * as actions from 'settings/actions'; import settingReducer from 'settings/reducers/setting'; describe("settings setting reducer", () => { it('return the initial state', () => { let state = settingReducer(undefined, {}); - expect(state).to.have.deep.property('json', ''); - expect(state).to.have.deep.property('form', null); + expect(state).to.have.deep.property('source', 'json'); expect(state).to.have.deep.property('error', ''); }); diff --git a/test/shared/SettingData.test.ts b/test/shared/SettingData.test.ts new file mode 100644 index 0000000..8736ecb --- /dev/null +++ b/test/shared/SettingData.test.ts @@ -0,0 +1,293 @@ +import SettingData, { + FormKeymaps, JSONSettings, FormSettings, +} from '../../src/shared/SettingData'; +import Settings, { Keymaps } from '../../src/shared/Settings'; +import { expect } from 'chai'; + +describe('shared/SettingData', () => { + describe('FormKeymaps', () => { + describe('#valueOF to #toKeymaps', () => { + it('parses form keymaps and convert to operations', () => { + let data = { + 'scroll.vertically?{"count":1}': 'j', + 'scroll.home': '0', + } + + let keymaps = FormKeymaps.valueOf(data).toKeymaps(); + expect(keymaps).to.deep.equal({ + 'j': { type: 'scroll.vertically', count: 1 }, + '0': { type: 'scroll.home' }, + }); + }); + }); + + describe('#fromKeymaps to #toJSON', () => { + it('create from a Keymaps and create a JSON object', () => { + let data: Keymaps = { + 'j': { type: 'scroll.vertically', count: 1 }, + '0': { type: 'scroll.home' }, + } + + let keymaps = FormKeymaps.fromKeymaps(data).toJSON(); + expect(keymaps).to.deep.equal({ + 'scroll.vertically?{"count":1}': 'j', + 'scroll.home': '0', + }); + }); + }); + }); + + describe('JSONSettings', () => { + describe('#valueOf to #toSettings', () => { + it('parse object and create a Settings', () => { + let o = `{ + "keymaps": {}, + "search": { + "default": "google", + "engines": { + "google": "https://google.com/search?q={}" + } + }, + "properties": { + "hintchars": "abcdefghijklmnopqrstuvwxyz", + "smoothscroll": false, + "complete": "sbh" + }, + "blacklist": [] + }`; + + let settings = JSONSettings.valueOf(o).toSettings(); + expect(settings).to.deep.equal(JSON.parse(o)); + }); + }); + + describe('#fromSettings to #toJSON', () => { + it('create from a Settings and create a JSON string', () => { + let o = { + keymaps: {}, + search: { + default: "google", + engines: { + google: "https://google.com/search?q={}", + }, + }, + properties: { + hintchars: "abcdefghijklmnopqrstuvwxyz", + smoothscroll: false, + complete: "sbh" + }, + blacklist: [], + }; + + let json = JSONSettings.fromSettings(o).toJSON(); + expect(JSON.parse(json)).to.deep.equal(o); + }); + }); + }); + + describe('FormSettings', () => { + describe('#valueOf to #toSettings', () => { + it('parse object and create a Settings', () => { + let data = { + keymaps: { + 'scroll.vertically?{"count":1}': 'j', + 'scroll.home': '0', + }, + search: { + default: "google", + engines: [ + ["google", "https://google.com/search?q={}"], + ] + }, + properties: { + hintchars: "abcdefghijklmnopqrstuvwxyz", + smoothscroll: false, + complete: "sbh" + }, + blacklist: [] + }; + + let settings = FormSettings.valueOf(data).toSettings(); + expect(settings).to.deep.equal({ + keymaps: { + 'j': { type: 'scroll.vertically', count: 1 }, + '0': { type: 'scroll.home' }, + }, + search: { + default: "google", + engines: { + "google": "https://google.com/search?q={}" + } + }, + properties: { + hintchars: "abcdefghijklmnopqrstuvwxyz", + smoothscroll: false, + complete: "sbh" + }, + blacklist: [] + }); + }); + }); + + describe('#fromSettings to #toJSON', () => { + it('create from a Settings and create a JSON string', () => { + let data: Settings = { + keymaps: { + 'j': { type: 'scroll.vertically', count: 1 }, + '0': { type: 'scroll.home' }, + }, + search: { + default: "google", + engines: { + "google": "https://google.com/search?q={}" + } + }, + properties: { + hintchars: "abcdefghijklmnopqrstuvwxyz", + smoothscroll: false, + complete: "sbh" + }, + blacklist: [] + }; + + let json = FormSettings.fromSettings(data).toJSON(); + expect(json).to.deep.equal({ + keymaps: { + 'scroll.vertically?{"count":1}': 'j', + 'scroll.home': '0', + }, + search: { + default: "google", + engines: [ + ["google", "https://google.com/search?q={}"], + ] + }, + properties: { + hintchars: "abcdefghijklmnopqrstuvwxyz", + smoothscroll: false, + complete: "sbh" + }, + blacklist: [], + }); + }); + }); + }); + + describe('SettingData', () => { + describe('#valueOf to #toJSON', () => { + it('parse object from json source', () => { + let data = { + source: 'json', + json: `{ + "keymaps": {}, + "search": { + "default": "google", + "engines": { + "google": "https://google.com/search?q={}" + } + }, + "properties": { + "hintchars": "abcdefghijklmnopqrstuvwxyz", + "smoothscroll": false, + "complete": "sbh" + }, + "blacklist": [] + }`, + }; + + let j = SettingData.valueOf(data).toJSON(); + expect(j.source).to.equal('json'); + expect(j.json).to.be.a('string'); + }); + + it('parse object from form source', () => { + let data = { + source: 'form', + form: { + keymaps: {}, + search: { + default: "yahoo", + engines: [ + ['yahoo', 'https://yahoo.com/search?q={}'], + ], + }, + properties: { + hintchars: "abcdefghijklmnopqrstuvwxyz", + smoothscroll: false, + complete: "sbh" + }, + blacklist: [], + }, + }; + + let j = SettingData.valueOf(data).toJSON(); + expect(j.source).to.equal('form'); + expect(j.form).to.deep.equal({ + keymaps: {}, + search: { + default: "yahoo", + engines: [ + ['yahoo', 'https://yahoo.com/search?q={}'], + ], + }, + properties: { + hintchars: "abcdefghijklmnopqrstuvwxyz", + smoothscroll: false, + complete: "sbh" + }, + blacklist: [], + }); + }); + }); + + describe('#toSettings', () => { + it('parse object from json source', () => { + let data = { + source: 'json', + json: `{ + "keymaps": {}, + "search": { + "default": "google", + "engines": { + "google": "https://google.com/search?q={}" + } + }, + "properties": { + "hintchars": "abcdefghijklmnopqrstuvwxyz", + "smoothscroll": false, + "complete": "sbh" + }, + "blacklist": [] + }`, + }; + + let settings = SettingData.valueOf(data).toSettings(); + expect(settings.search.default).to.equal('google'); + }); + + it('parse object from form source', () => { + let data = { + source: 'form', + form: { + keymaps: {}, + search: { + default: "yahoo", + engines: [ + ['yahoo', 'https://yahoo.com/search?q={}'], + ], + }, + properties: { + hintchars: "abcdefghijklmnopqrstuvwxyz", + smoothscroll: false, + complete: "sbh" + }, + blacklist: [], + }, + }; + + let settings = SettingData.valueOf(data).toSettings(); + expect(settings.search.default).to.equal('yahoo'); + }); + }); + }); +}); diff --git a/test/shared/Settings.test.ts b/test/shared/Settings.test.ts new file mode 100644 index 0000000..02cd022 --- /dev/null +++ b/test/shared/Settings.test.ts @@ -0,0 +1,190 @@ +import * as settings from '../../src/shared/Settings'; +import { expect } from 'chai'; + +describe('Settings', () => { + describe('#keymapsValueOf', () => { + it('returns empty object by empty settings', () => { + let keymaps = settings.keymapsValueOf({}); + expect(keymaps).to.be.empty; + }); + + it('returns keymaps by valid settings', () => { + let keymaps = settings.keymapsValueOf({ + k: { type: "scroll.vertically", count: -1 }, + j: { type: "scroll.vertically", count: 1 }, + }); + + expect(keymaps['k']).to.deep.equal({ type: "scroll.vertically", count: -1 }); + expect(keymaps['j']).to.deep.equal({ type: "scroll.vertically", count: 1 }); + }); + + it('throws a TypeError by invalid settings', () => { + expect(() => settings.keymapsValueOf(null)).to.throw(TypeError); + expect(() => settings.keymapsValueOf({ + k: { type: "invalid.operation" }, + })).to.throw(TypeError); + }); + }); + + describe('#searchValueOf', () => { + it('returns search settings by valid settings', () => { + let search = settings.searchValueOf({ + default: "google", + engines: { + "google": "https://google.com/search?q={}", + "yahoo": "https://search.yahoo.com/search?p={}", + } + }); + + expect(search).to.deep.equal({ + default: "google", + engines: { + "google": "https://google.com/search?q={}", + "yahoo": "https://search.yahoo.com/search?p={}", + } + }); + }); + + it('throws a TypeError by invalid settings', () => { + expect(() => settings.searchValueOf(null)).to.throw(TypeError); + expect(() => settings.searchValueOf({})).to.throw(TypeError); + expect(() => settings.searchValueOf([])).to.throw(TypeError); + expect(() => settings.searchValueOf({ + default: 123, + engines: {} + })).to.throw(TypeError); + expect(() => settings.searchValueOf({ + default: "google", + engines: { + "google": 123456, + } + })).to.throw(TypeError); + expect(() => settings.searchValueOf({ + default: "wikipedia", + engines: { + "google": "https://google.com/search?q={}", + "yahoo": "https://search.yahoo.com/search?p={}", + } + })).to.throw(TypeError); + expect(() => settings.searchValueOf({ + default: "g o o g l e", + engines: { + "g o o g l e": "https://google.com/search?q={}", + } + })).to.throw(TypeError); + expect(() => settings.searchValueOf({ + default: "google", + engines: { + "google": "https://google.com/search", + } + })).to.throw(TypeError); + expect(() => settings.searchValueOf({ + default: "google", + engines: { + "google": "https://google.com/search?q={}&r={}", + } + })).to.throw(TypeError); + }); + }); + + describe('#propertiesValueOf', () => { + it('returns with default properties by empty settings', () => { + let props = settings.propertiesValueOf({}); + expect(props).to.deep.equal({ + hintchars: "abcdefghijklmnopqrstuvwxyz", + smoothscroll: false, + complete: "sbh" + }) + }); + + it('returns properties by valid settings', () => { + let props = settings.propertiesValueOf({ + hintchars: "abcdefgh", + smoothscroll: false, + complete: "sbh" + }); + + expect(props).to.deep.equal({ + hintchars: "abcdefgh", + smoothscroll: false, + complete: "sbh" + }); + }); + + it('throws a TypeError by invalid settings', () => { + expect(() => settings.keymapsValueOf(null)).to.throw(TypeError); + expect(() => settings.keymapsValueOf({ + smoothscroll: 'false', + })).to.throw(TypeError); + expect(() => settings.keymapsValueOf({ + unknown: 'xyz' + })).to.throw(TypeError); + }); + }); + + describe('#blacklistValueOf', () => { + it('returns empty array by empty settings', () => { + let blacklist = settings.blacklistValueOf([]); + expect(blacklist).to.be.empty; + }); + + it('returns blacklist by valid settings', () => { + let blacklist = settings.blacklistValueOf([ + "github.com", + "circleci.com", + ]); + + expect(blacklist).to.deep.equal([ + "github.com", + "circleci.com", + ]); + }); + + it('throws a TypeError by invalid settings', () => { + expect(() => settings.blacklistValueOf(null)).to.throw(TypeError); + expect(() => settings.blacklistValueOf({})).to.throw(TypeError); + expect(() => settings.blacklistValueOf([1,2,3])).to.throw(TypeError); + }); + }); + + describe('#valueOf', () => { + it('returns settings by valid settings', () => { + let x = settings.valueOf({ + keymaps: {}, + "search": { + "default": "google", + "engines": { + "google": "https://google.com/search?q={}", + } + }, + "properties": {}, + "blacklist": [] + }); + + expect(x).to.deep.equal({ + keymaps: {}, + search: { + default: "google", + engines: { + google: "https://google.com/search?q={}", + } + }, + properties: { + hintchars: "abcdefghijklmnopqrstuvwxyz", + smoothscroll: false, + complete: "sbh" + }, + blacklist: [] + }); + }); + + it('sets default settings', () => { + let value = settings.valueOf({}); + expect(value.keymaps).to.not.be.empty; + expect(value.properties).to.not.be.empty; + expect(value.search.default).to.be.a('string'); + expect(value.search.engines).to.be.an('object'); + expect(value.blacklist).to.be.empty; + }); + }); +}); diff --git a/test/shared/blacklists.test.js b/test/shared/blacklists.test.ts index 289ea0f..289ea0f 100644 --- a/test/shared/blacklists.test.js +++ b/test/shared/blacklists.test.ts 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/test/shared/properties.test.js b/test/shared/properties.test.js new file mode 100644 index 0000000..37903d8 --- /dev/null +++ b/test/shared/properties.test.js @@ -0,0 +1,18 @@ +import * as settings from 'shared/settings'; + +describe('properties', () => { + describe('Def class', () => { + it('returns property definitions', () => { + let def = new proerties.Def( + 'smoothscroll', + 'smooth scroll', + false); + + expect(def.name).to.equal('smoothscroll'); + expect(def.describe).to.equal('smooth scroll'); + expect(def.defaultValue).to.equal(false); + expect(def.type).to.equal('boolean'); + }); + }); +}); + diff --git a/test/shared/property-defs.test.js b/test/shared/property-defs.test.js new file mode 100644 index 0000000..37903d8 --- /dev/null +++ b/test/shared/property-defs.test.js @@ -0,0 +1,18 @@ +import * as settings from 'shared/settings'; + +describe('properties', () => { + describe('Def class', () => { + it('returns property definitions', () => { + let def = new proerties.Def( + 'smoothscroll', + 'smooth scroll', + false); + + expect(def.name).to.equal('smoothscroll'); + expect(def.describe).to.equal('smooth scroll'); + expect(def.defaultValue).to.equal(false); + expect(def.type).to.equal('boolean'); + }); + }); +}); + diff --git a/test/shared/settings/validator.test.js b/test/shared/settings/validator.test.js deleted file mode 100644 index 9bbfa3e..0000000 --- a/test/shared/settings/validator.test.js +++ /dev/null @@ -1,81 +0,0 @@ -import { validate } from 'shared/settings/validator'; - -describe("setting validator", () => { - describe("unknown top keys", () => { - it('throws an error for unknown settings', () => { - let settings = { keymaps: {}, poison: 123 }; - let fn = validate.bind(undefined, settings) - expect(fn).to.throw(Error, 'poison'); - }) - }); - - describe("keymaps settings", () => { - it('throws an error for unknown operation', () => { - let settings = { - keymaps: { - a: { 'type': 'scroll.home' }, - b: { 'type': 'poison.dressing' }, - } - }; - let fn = validate.bind(undefined, settings) - expect(fn).to.throw(Error, 'poison.dressing'); - }); - }); - - describe("search settings", () => { - it('throws an error for invalid search engine name', () => { - let settings = { - search: { - default: 'google', - engines: { - 'google': 'https://google.com/search?q={}', - 'cherry pie': 'https://cherypie.com/search?q={}', - } - } - }; - let fn = validate.bind(undefined, settings) - expect(fn).to.throw(Error, 'cherry pie'); - }); - - it('throws an error for no {}-placeholder', () => { - let settings = { - search: { - default: 'google', - engines: { - 'google': 'https://google.com/search?q={}', - 'yahoo': 'https://search.yahoo.com/search', - } - } - }; - let fn = validate.bind(undefined, settings) - expect(fn).to.throw(Error, 'yahoo'); - }); - - it('throws an error for no default engines', () => { - let settings = { - search: { - engines: { - 'google': 'https://google.com/search?q={}', - 'yahoo': 'https://search.yahoo.com/search?q={}', - } - } - }; - let fn = validate.bind(undefined, settings) - expect(fn).to.throw(Error, 'Default engine'); - }); - - it('throws an error for invalid default engine', () => { - let settings = { - search: { - default: 'twitter', - engines: { - 'google': 'https://google.com/search?q={}', - 'yahoo': 'https://search.yahoo.com/search?q={}', - } - } - }; - let fn = validate.bind(undefined, settings) - expect(fn).to.throw(Error, 'twitter'); - }); - }); -}); diff --git a/test/shared/settings/values.test.js b/test/shared/settings/values.test.js deleted file mode 100644 index c72824d..0000000 --- a/test/shared/settings/values.test.js +++ /dev/null @@ -1,138 +0,0 @@ -import * as values from 'shared/settings/values'; - -describe("settings values", () => { - describe('valueFromJson', () => { - it('return object from json string', () => { - let json = `{ - "keymaps": { "0": {"type": "scroll.home"}}, - "search": { "default": "google", "engines": { "google": "https://google.com/search?q={}" }}, - "blacklist": [ "*.slack.com"], - "properties": { - "mystr": "value", - "mynum": 123, - "mybool": true - } - }`; - let value = values.valueFromJson(json); - - expect(value.keymaps).to.deep.equal({ 0: {type: "scroll.home"}}); - expect(value.search).to.deep.equal({ default: "google", engines: { google: "https://google.com/search?q={}"} }); - expect(value.blacklist).to.deep.equal(["*.slack.com"]); - expect(value.properties).to.have.property('mystr', 'value'); - expect(value.properties).to.have.property('mynum', 123); - expect(value.properties).to.have.property('mybool', true); - }); - }); - - describe('valueFromForm', () => { - it('returns value from form', () => { - let form = { - keymaps: { - 'scroll.vertically?{"count":1}': 'j', - 'scroll.home': '0', - }, - search: { - default: 'google', - engines: [['google', 'https://google.com/search?q={}']], - }, - blacklist: ['*.slack.com'], - "properties": { - "mystr": "value", - "mynum": 123, - "mybool": true, - } - }; - let value = values.valueFromForm(form); - - expect(value.keymaps).to.have.deep.property('j', { type: "scroll.vertically", count: 1 }); - expect(value.keymaps).to.have.deep.property('0', { type: "scroll.home" }); - expect(JSON.stringify(value.search)).to.deep.equal(JSON.stringify({ default: "google", engines: { google: "https://google.com/search?q={}"} })); - expect(value.search).to.deep.equal({ default: "google", engines: { google: "https://google.com/search?q={}"} }); - expect(value.blacklist).to.deep.equal(["*.slack.com"]); - expect(value.properties).to.have.property('mystr', 'value'); - expect(value.properties).to.have.property('mynum', 123); - expect(value.properties).to.have.property('mybool', true); - }); - - it('convert from empty form', () => { - let form = {}; - let value = values.valueFromForm(form); - expect(value).to.not.have.key('keymaps'); - expect(value).to.not.have.key('search'); - expect(value).to.not.have.key('blacklist'); - expect(value).to.not.have.key('properties'); - }); - - it('override keymaps', () => { - let form = { - keymaps: { - 'scroll.vertically?{"count":1}': 'j', - 'scroll.vertically?{"count":-1}': 'j', - } - }; - let value = values.valueFromForm(form); - - expect(value.keymaps).to.have.key('j'); - }); - - it('override search engine', () => { - let form = { - search: { - default: 'google', - engines: [ - ['google', 'https://google.com/search?q={}'], - ['google', 'https://google.co.jp/search?q={}'], - ] - } - }; - let value = values.valueFromForm(form); - - expect(value.search.engines).to.have.property('google', 'https://google.co.jp/search?q={}'); - }); - }); - - describe('jsonFromValue', () => { - }); - - describe('formFromValue', () => { - it('convert empty value to form', () => { - let value = {}; - let form = values.formFromValue(value); - - expect(value).to.not.have.key('keymaps'); - expect(value).to.not.have.key('search'); - expect(value).to.not.have.key('blacklist'); - }); - - it('convert value to form', () => { - let value = { - keymaps: { - j: { type: 'scroll.vertically', count: 1 }, - JJ: { type: 'scroll.vertically', count: 100 }, - 0: { type: 'scroll.home' }, - }, - search: { default: 'google', engines: { google: 'https://google.com/search?q={}' }}, - blacklist: [ '*.slack.com'], - properties: { - "mystr": "value", - "mynum": 123, - "mybool": true, - } - }; - let allowed = ['scroll.vertically?{"count":1}', 'scroll.home' ]; - let form = values.formFromValue(value, allowed); - - expect(form.keymaps).to.have.property('scroll.vertically?{"count":1}', 'j'); - expect(form.keymaps).to.not.have.property('scroll.vertically?{"count":100}'); - expect(form.keymaps).to.have.property('scroll.home', '0'); - expect(Object.keys(form.keymaps)).to.have.lengthOf(2); - expect(form.search).to.have.property('default', 'google'); - expect(form.search).to.have.deep.property('engines', [['google', 'https://google.com/search?q={}']]); - expect(form.blacklist).to.have.lengthOf(1); - expect(form.blacklist).to.include('*.slack.com'); - expect(form.properties).to.have.property('mystr', 'value'); - expect(form.properties).to.have.property('mynum', 123); - expect(form.properties).to.have.property('mybool', true); - }); - }); -}); diff --git a/test/shared/urls.test.js b/test/shared/urls.test.ts index f2950b6..f2950b6 100644 --- a/test/shared/urls.test.js +++ b/test/shared/urls.test.ts diff --git a/test/shared/utils/keys.test.js b/test/shared/utils/keys.test.ts index b2ad3cb..b2ad3cb 100644 --- a/test/shared/utils/keys.test.js +++ b/test/shared/utils/keys.test.ts diff --git a/test/shared/utils/re.test.js b/test/shared/utils/re.test.ts index d12ceb7..d12ceb7 100644 --- a/test/shared/utils/re.test.js +++ b/test/shared/utils/re.test.ts diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b61ee23 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "target": "es2017", + "module": "commonjs", + "lib": ["es6", "dom", "es2017"], + "allowJs": true, + "checkJs": true, + "noEmit": true, + "jsx": "react", + "sourceMap": true, + "outDir": "./build", + "removeComments": true, + "importHelpers": true, + + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "alwaysStrict": true, + + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + + "moduleResolution": "node", + "esModuleInterop": true, + + "typeRoots": ["node_modules/@types", "node_modules/web-ext-types"] + }, + "include": [ + "src" + ] +} diff --git a/webpack.config.js b/webpack.config.js index d9c60cc..a845375 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -20,12 +20,16 @@ config = { module: { rules: [ { - test: [ /\.js$/, /\.jsx$/ ], + test: [ /\.js$/, /\.jsx$/, /\.ts$/, /\.tsx$/], exclude: /node_modules/, loader: 'babel-loader', - query: { - presets: ['@babel/react'] - } + options: { + presets: [ + { plugins: ['@babel/plugin-proposal-class-properties'] }, + '@babel/react', + '@babel/preset-typescript' + ] + }, }, { test: /\.css$/, @@ -39,7 +43,7 @@ config = { }, resolve: { - extensions: [ '.js', '.jsx' ], + extensions: [ '.js', '.jsx', '.ts', '.tsx' ], modules: [path.join(__dirname, 'src'), 'node_modules'] }, |