aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorShin'ya Ueoka <ueokande@i-beam.org>2019-05-11 19:43:56 +0900
committerShin'ya Ueoka <ueokande@i-beam.org>2019-05-18 17:28:11 +0900
commitefc48dc7421e3bd48534bc94f84e2b0bd47ae47c (patch)
treedfe80ebc368911c385e6c36aa1096af619b1616b /src
parenta88324acd9fe626b59637541975abe1ee6041aa7 (diff)
Keymaps as a clean architecture [WIP]
Diffstat (limited to 'src')
-rw-r--r--src/content/InputDriver.ts (renamed from src/content/components/common/input.ts)16
-rw-r--r--src/content/client/BackgroundClient.ts11
-rw-r--r--src/content/client/FindMasterClient.ts23
-rw-r--r--src/content/components/common/index.ts4
-rw-r--r--src/content/controllers/KeymapController.ts139
-rw-r--r--src/content/index.ts46
-rw-r--r--src/content/presenters/FocusPresenter.ts25
-rw-r--r--src/content/repositories/KeymapRepository.ts23
-rw-r--r--src/content/usecases/FindSlaveUseCase.ts20
-rw-r--r--src/content/usecases/FocusUseCase.ts15
-rw-r--r--src/content/usecases/KeymapUseCase.ts100
-rw-r--r--src/content/usecases/NavigateUseCase.ts27
-rw-r--r--src/content/usecases/ScrollUseCase.ts58
13 files changed, 491 insertions, 16 deletions
diff --git a/src/content/components/common/input.ts b/src/content/InputDriver.ts
index 1fe34c9..09648c1 100644
--- a/src/content/components/common/input.ts
+++ b/src/content/InputDriver.ts
@@ -1,11 +1,11 @@
-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: KeyboardEvent): boolean => {
return e.key === 'Escape' || e.key === '[' && e.ctrlKey;
};
-export default class InputComponent {
+export default class InputDriver {
private pressed: {[key: string]: string} = {};
private onKeyListeners: ((key: keys.Key) => boolean)[] = [];
@@ -23,7 +23,7 @@ export default class InputComponent {
this.onKeyListeners.push(cb);
}
- onKeyPress(e: KeyboardEvent) {
+ private onKeyPress(e: KeyboardEvent) {
if (this.pressed[e.key] && this.pressed[e.key] !== 'keypress') {
return;
}
@@ -31,7 +31,7 @@ export default class InputComponent {
this.capture(e);
}
- onKeyDown(e: KeyboardEvent) {
+ private onKeyDown(e: KeyboardEvent) {
if (this.pressed[e.key] && this.pressed[e.key] !== 'keydown') {
return;
}
@@ -39,12 +39,12 @@ export default class InputComponent {
this.capture(e);
}
- onKeyUp(e: KeyboardEvent) {
+ private onKeyUp(e: KeyboardEvent) {
delete this.pressed[e.key];
}
// eslint-disable-next-line max-statements
- capture(e: KeyboardEvent) {
+ private capture(e: KeyboardEvent) {
let target = e.target;
if (!(target instanceof HTMLElement)) {
return;
@@ -71,7 +71,7 @@ export default class InputComponent {
}
}
- fromInput(e: Element) {
+ private fromInput(e: Element) {
return e instanceof HTMLInputElement ||
e instanceof HTMLTextAreaElement ||
e instanceof HTMLSelectElement ||
diff --git a/src/content/client/BackgroundClient.ts b/src/content/client/BackgroundClient.ts
new file mode 100644
index 0000000..2fe8d01
--- /dev/null
+++ b/src/content/client/BackgroundClient.ts
@@ -0,0 +1,11 @@
+import * as operations from '../../shared/operations';
+import * as messages from '../../shared/messages';
+
+export default class BackgroundClient {
+ execBackgroundOp(op: operations.Operation): Promise<void> {
+ return browser.runtime.sendMessage({
+ type: messages.BACKGROUND_OPERATION,
+ operation: op,
+ });
+ }
+}
diff --git a/src/content/client/FindMasterClient.ts b/src/content/client/FindMasterClient.ts
new file mode 100644
index 0000000..0481ec1
--- /dev/null
+++ b/src/content/client/FindMasterClient.ts
@@ -0,0 +1,23 @@
+import * as messages from '../../shared/messages';
+
+export default interface FindMasterClient {
+ findNext(): void;
+
+ findPrev(): void;
+
+ // eslint-disable-next-line semi
+}
+
+export class FindMasterClientImpl implements FindMasterClient {
+ findNext(): void {
+ window.top.postMessage(JSON.stringify({
+ type: messages.FIND_NEXT,
+ }), '*');
+ }
+
+ findPrev(): void {
+ window.top.postMessage(JSON.stringify({
+ type: messages.FIND_PREV,
+ }), '*');
+ }
+}
diff --git a/src/content/components/common/index.ts b/src/content/components/common/index.ts
index b2f48a3..c74020e 100644
--- a/src/content/components/common/index.ts
+++ b/src/content/components/common/index.ts
@@ -1,4 +1,4 @@
-import InputComponent from './input';
+import InputDriver from './../../InputDriver';
import FollowComponent from './follow';
import MarkComponent from './mark';
import KeymapperComponent from './keymapper';
@@ -15,7 +15,7 @@ let settingUseCase = new SettingUseCase();
export default class Common {
constructor(win: Window, store: any) {
- const input = new InputComponent(win.document.body);
+ const input = new InputDriver(win.document.body);
const follow = new FollowComponent();
const mark = new MarkComponent(store);
const keymapper = new KeymapperComponent(store);
diff --git a/src/content/controllers/KeymapController.ts b/src/content/controllers/KeymapController.ts
new file mode 100644
index 0000000..09e5b0c
--- /dev/null
+++ b/src/content/controllers/KeymapController.ts
@@ -0,0 +1,139 @@
+import * as operations from '../../shared/operations';
+import KeymapUseCase from '../usecases/KeymapUseCase';
+import AddonEnabledUseCase from '../usecases/AddonEnabledUseCase';
+import FindSlaveUseCase from '../usecases/FindSlaveUseCase';
+import ScrollUseCase from '../usecases/ScrollUseCase';
+import NavigateUseCase from '../usecases/NavigateUseCase';
+import FocusUseCase from '../usecases/FocusUseCase';
+import ClipboardUseCase from '../usecases/ClipboardUseCase';
+import BackgroundClient from '../client/BackgroundClient';
+import { Key } from '../../shared/utils/keys';
+
+export default class KeymapController {
+ private keymapUseCase: KeymapUseCase;
+
+ private addonEnabledUseCase: AddonEnabledUseCase;
+
+ private findSlaveUseCase: FindSlaveUseCase;
+
+ private scrollUseCase: ScrollUseCase;
+
+ private navigateUseCase: NavigateUseCase;
+
+ private focusUseCase: FocusUseCase;
+
+ private clipbaordUseCase: ClipboardUseCase;
+
+ private backgroundClient: BackgroundClient;
+
+ constructor({
+ keymapUseCase = new KeymapUseCase(),
+ addonEnabledUseCase = new AddonEnabledUseCase(),
+ findSlaveUseCase = new FindSlaveUseCase(),
+ scrollUseCase = new ScrollUseCase(),
+ navigateUseCase = new NavigateUseCase(),
+ focusUseCase = new FocusUseCase(),
+ clipbaordUseCase = new ClipboardUseCase(),
+ backgroundClient = new BackgroundClient(),
+ } = {}) {
+ this.keymapUseCase = keymapUseCase;
+ this.addonEnabledUseCase = addonEnabledUseCase;
+ this.findSlaveUseCase = findSlaveUseCase;
+ this.scrollUseCase = scrollUseCase;
+ this.navigateUseCase = navigateUseCase;
+ this.focusUseCase = focusUseCase;
+ this.clipbaordUseCase = clipbaordUseCase;
+ this.backgroundClient = backgroundClient;
+ }
+
+ // eslint-disable-next-line complexity, max-lines-per-function
+ press(key: Key): boolean {
+ let op = this.keymapUseCase.nextOp(key);
+ if (op === null) {
+ return false;
+ }
+
+ // do not await due to return a boolean immediately
+ switch (op.type) {
+ case operations.ADDON_ENABLE:
+ this.addonEnabledUseCase.enable();
+ break;
+ case operations.ADDON_DISABLE:
+ this.addonEnabledUseCase.disable();
+ break;
+ case operations.ADDON_TOGGLE_ENABLED:
+ this.addonEnabledUseCase.toggle();
+ break;
+ case operations.FIND_NEXT:
+ this.findSlaveUseCase.findNext();
+ break;
+ case operations.FIND_PREV:
+ this.findSlaveUseCase.findPrev();
+ break;
+ case operations.SCROLL_VERTICALLY:
+ this.scrollUseCase.scrollVertically(op.count);
+ break;
+ case operations.SCROLL_HORIZONALLY:
+ this.scrollUseCase.scrollHorizonally(op.count);
+ break;
+ case operations.SCROLL_PAGES:
+ this.scrollUseCase.scrollPages(op.count);
+ break;
+ case operations.SCROLL_TOP:
+ this.scrollUseCase.scrollToTop();
+ break;
+ case operations.SCROLL_BOTTOM:
+ this.scrollUseCase.scrollToBottom();
+ break;
+ case operations.SCROLL_HOME:
+ this.scrollUseCase.scrollToHome();
+ break;
+ case operations.SCROLL_END:
+ this.scrollUseCase.scrollToEnd();
+ break;
+ // case operations.FOLLOW_START:
+ // window.top.postMessage(JSON.stringify({
+ // type: messages.FOLLOW_START,
+ // newTab: operation.newTab,
+ // background: operation.background,
+ // }), '*');
+ // break;
+ // case operations.MARK_SET_PREFIX:
+ // return markActions.startSet();
+ // case operations.MARK_JUMP_PREFIX:
+ // return markActions.startJump();
+ case operations.NAVIGATE_HISTORY_PREV:
+ this.navigateUseCase.openHistoryPrev();
+ break;
+ case operations.NAVIGATE_HISTORY_NEXT:
+ this.navigateUseCase.openHistoryNext();
+ break;
+ case operations.NAVIGATE_LINK_PREV:
+ this.navigateUseCase.openLinkPrev();
+ break;
+ case operations.NAVIGATE_LINK_NEXT:
+ this.navigateUseCase.openLinkNext();
+ break;
+ case operations.NAVIGATE_PARENT:
+ this.navigateUseCase.openParent();
+ break;
+ case operations.NAVIGATE_ROOT:
+ this.navigateUseCase.openRoot();
+ break;
+ case operations.FOCUS_INPUT:
+ this.focusUseCase.focusFirstInput();
+ break;
+ case operations.URLS_YANK:
+ this.clipbaordUseCase.yankCurrentURL();
+ break;
+ case operations.URLS_PASTE:
+ this.clipbaordUseCase.openOrSearch(
+ op.newTab ? op.newTab : false,
+ );
+ break;
+ default:
+ this.backgroundClient.execBackgroundOp(op);
+ }
+ return true;
+ }
+}
diff --git a/src/content/index.ts b/src/content/index.ts
index 4024b98..f983f9f 100644
--- a/src/content/index.ts
+++ b/src/content/index.ts
@@ -1,15 +1,20 @@
-import TopContentComponent from './components/top-content';
-import FrameContentComponent from './components/frame-content';
+// import TopContentComponent from './components/top-content';
+// import FrameContentComponent from './components/frame-content';
import consoleFrameStyle from './site-style';
-import { newStore } from './store';
+// import { newStore } from './store';
import MessageListener from './MessageListener';
import FindController from './controllers/FindController';
import * as messages from '../shared/messages';
+import InputDriver from './InputDriver';
+import KeymapController from './controllers/KeymapController';
+import AddonEnabledUseCase from './usecases/AddonEnabledUseCase';
+import SettingUseCase from './usecases/SettingUseCase';
+import * as blacklists from '../shared/blacklists';
-const store = newStore();
+// const store = newStore();
if (window.self === window.top) {
- new TopContentComponent(window, store); // eslint-disable-line no-new
+ // new TopContentComponent(window, store); // eslint-disable-line no-new
let findController = new FindController();
new MessageListener().onWebMessage((message: messages.Message) => {
@@ -24,9 +29,38 @@ if (window.self === window.top) {
return undefined;
});
} else {
- new FrameContentComponent(window, store); // eslint-disable-line no-new
+ // new FrameContentComponent(window, store); // eslint-disable-line no-new
}
+let keymapController = new KeymapController();
+let inputDriver = new InputDriver(document.body);
+// inputDriver.onKey(key => followSlaveController.pressKey(key));
+// inputDriver.onKey(key => markController.pressKey(key));
+inputDriver.onKey(key => keymapController.press(key));
+
let style = window.document.createElement('style');
style.textContent = consoleFrameStyle;
window.document.head.appendChild(style);
+
+// TODO move the following to a class
+const reloadSettings = async() => {
+ let addonEnabledUseCase = new AddonEnabledUseCase();
+ let settingUseCase = new SettingUseCase();
+
+ try {
+ let current = await settingUseCase.reload();
+ let disabled = blacklists.includes(
+ current.blacklist, window.location.href,
+ );
+ if (disabled) {
+ addonEnabledUseCase.disable();
+ } else {
+ addonEnabledUseCase.enable();
+ }
+ } catch (e) {
+ // Sometime sendMessage fails when background script is not ready.
+ console.warn(e);
+ setTimeout(() => reloadSettings(), 500);
+ }
+};
+reloadSettings();
diff --git a/src/content/presenters/FocusPresenter.ts b/src/content/presenters/FocusPresenter.ts
new file mode 100644
index 0000000..4cef5bf
--- /dev/null
+++ b/src/content/presenters/FocusPresenter.ts
@@ -0,0 +1,25 @@
+import * as doms from '../../shared/utils/dom';
+
+export default interface FocusPresenter {
+ focusFirstElement(): boolean;
+
+ // eslint-disable-next-line semi
+}
+
+export class FocusPresenterImpl implements FocusPresenter {
+ focusFirstElement(): boolean {
+ let inputTypes = ['email', 'number', 'search', 'tel', 'text', 'url'];
+ let inputSelector = inputTypes.map(type => `input[type=${type}]`).join(',');
+ let targets = window.document.querySelectorAll(inputSelector + ',textarea');
+ let target = Array.from(targets).find(doms.isVisible);
+ if (target instanceof HTMLInputElement) {
+ target.focus();
+ return true;
+ } else if (target instanceof HTMLTextAreaElement) {
+ target.focus();
+ return true;
+ }
+ return false;
+ }
+}
+
diff --git a/src/content/repositories/KeymapRepository.ts b/src/content/repositories/KeymapRepository.ts
new file mode 100644
index 0000000..081cc54
--- /dev/null
+++ b/src/content/repositories/KeymapRepository.ts
@@ -0,0 +1,23 @@
+import { Key } from '../../shared/utils/keys';
+
+export default interface KeymapRepository {
+ enqueueKey(key: Key): Key[];
+
+ clear(): void;
+
+ // eslint-disable-next-line semi
+}
+
+let current: Key[] = [];
+
+export class KeymapRepositoryImpl {
+
+ enqueueKey(key: Key): Key[] {
+ current.push(key);
+ return current;
+ }
+
+ clear(): void {
+ current = [];
+ }
+}
diff --git a/src/content/usecases/FindSlaveUseCase.ts b/src/content/usecases/FindSlaveUseCase.ts
new file mode 100644
index 0000000..b733cbd
--- /dev/null
+++ b/src/content/usecases/FindSlaveUseCase.ts
@@ -0,0 +1,20 @@
+import FindMasterClient, { FindMasterClientImpl }
+ from '../client/FindMasterClient';
+
+export default class FindSlaveUseCase {
+ private findMasterClient: FindMasterClient;
+
+ constructor({
+ findMasterClient = new FindMasterClientImpl(),
+ } = {}) {
+ this.findMasterClient = findMasterClient;
+ }
+
+ findNext() {
+ this.findMasterClient.findNext();
+ }
+
+ findPrev() {
+ this.findMasterClient.findPrev();
+ }
+}
diff --git a/src/content/usecases/FocusUseCase.ts b/src/content/usecases/FocusUseCase.ts
new file mode 100644
index 0000000..615442d
--- /dev/null
+++ b/src/content/usecases/FocusUseCase.ts
@@ -0,0 +1,15 @@
+import FocusPresenter, { FocusPresenterImpl }
+ from '../presenters/FocusPresenter';
+export default class FocusUseCases {
+ private presenter: FocusPresenter;
+
+ constructor({
+ presenter = new FocusPresenterImpl(),
+ } = {}) {
+ this.presenter = presenter;
+ }
+
+ focusFirstInput() {
+ this.presenter.focusFirstElement();
+ }
+}
diff --git a/src/content/usecases/KeymapUseCase.ts b/src/content/usecases/KeymapUseCase.ts
new file mode 100644
index 0000000..a4f9c36
--- /dev/null
+++ b/src/content/usecases/KeymapUseCase.ts
@@ -0,0 +1,100 @@
+import KeymapRepository, { KeymapRepositoryImpl }
+ from '../repositories/KeymapRepository';
+import SettingRepository, { SettingRepositoryImpl }
+ from '../repositories/SettingRepository';
+import AddonEnabledRepository, { AddonEnabledRepositoryImpl }
+ from '../repositories/AddonEnabledRepository';
+
+import * as operations from '../../shared/operations';
+import { Keymaps } from '../../shared/Settings';
+import * as keyUtils from '../../shared/utils/keys';
+
+type KeymapEntityMap = Map<keyUtils.Key[], operations.Operation>;
+
+const reservedKeymaps: Keymaps = {
+ '<Esc>': { type: operations.CANCEL },
+ '<C-[>': { type: operations.CANCEL },
+};
+
+const mapStartsWith = (
+ mapping: keyUtils.Key[],
+ keys: keyUtils.Key[],
+): boolean => {
+ if (mapping.length < keys.length) {
+ return false;
+ }
+ for (let i = 0; i < keys.length; ++i) {
+ if (!keyUtils.equals(mapping[i], keys[i])) {
+ return false;
+ }
+ }
+ return true;
+};
+
+export default class KeymapUseCase {
+ private repository: KeymapRepository;
+
+ private settingRepository: SettingRepository;
+
+ private addonEnabledRepository: AddonEnabledRepository;
+
+ constructor({
+ repository = new KeymapRepositoryImpl(),
+ settingRepository = new SettingRepositoryImpl(),
+ addonEnabledRepository = new AddonEnabledRepositoryImpl(),
+ } = {}) {
+ this.repository = repository;
+ this.settingRepository = settingRepository;
+ this.addonEnabledRepository = addonEnabledRepository;
+ }
+
+ nextOp(key: keyUtils.Key): operations.Operation | null {
+ let keys = this.repository.enqueueKey(key);
+
+ let keymaps = this.keymapEntityMap();
+ let matched = Array.from(keymaps.keys()).filter(
+ (mapping: keyUtils.Key[]) => {
+ return mapStartsWith(mapping, keys);
+ });
+ if (!this.addonEnabledRepository.get()) {
+ // available keymaps are only ADDON_ENABLE and ADDON_TOGGLE_ENABLED if
+ // the addon disabled
+ matched = matched.filter((keymap) => {
+ let type = (keymaps.get(keymap) as operations.Operation).type;
+ return type === operations.ADDON_ENABLE ||
+ type === operations.ADDON_TOGGLE_ENABLED;
+ });
+ }
+ if (matched.length === 0) {
+ // No operations to match with inputs
+ this.repository.clear();
+ return null;
+ } else if (matched.length > 1 ||
+ matched.length === 1 && keys.length < matched[0].length) {
+ // More than one operations are matched
+ return null;
+ }
+ // Exactly one operation is matched
+ let operation = keymaps.get(matched[0]) as operations.Operation;
+ this.repository.clear();
+ return operation;
+ }
+
+ clear(): void {
+ this.repository.clear();
+ }
+
+ private keymapEntityMap(): KeymapEntityMap {
+ let keymaps = {
+ ...this.settingRepository.get().keymaps,
+ ...reservedKeymaps,
+ };
+ let entries = Object.entries(keymaps).map((entry) => {
+ return [
+ keyUtils.fromMapKeys(entry[0]),
+ entry[1],
+ ];
+ }) as [keyUtils.Key[], operations.Operation][];
+ return new Map<keyUtils.Key[], operations.Operation>(entries);
+ }
+}
diff --git a/src/content/usecases/NavigateUseCase.ts b/src/content/usecases/NavigateUseCase.ts
new file mode 100644
index 0000000..f790212
--- /dev/null
+++ b/src/content/usecases/NavigateUseCase.ts
@@ -0,0 +1,27 @@
+import * as navigates from '../navigates';
+
+export default class NavigateClass {
+ openHistoryPrev(): void {
+ navigates.historyPrev(window);
+ }
+
+ openHistoryNext(): void {
+ navigates.historyNext(window);
+ }
+
+ openLinkPrev(): void {
+ navigates.linkPrev(window);
+ }
+
+ openLinkNext(): void {
+ navigates.linkNext(window);
+ }
+
+ openParent(): void {
+ navigates.parent(window);
+ }
+
+ openRoot(): void {
+ navigates.root(window);
+ }
+}
diff --git a/src/content/usecases/ScrollUseCase.ts b/src/content/usecases/ScrollUseCase.ts
new file mode 100644
index 0000000..6a1f801
--- /dev/null
+++ b/src/content/usecases/ScrollUseCase.ts
@@ -0,0 +1,58 @@
+import ScrollPresenter, { ScrollPresenterImpl }
+ from '../presenters/ScrollPresenter';
+import SettingRepository, { SettingRepositoryImpl }
+ from '../repositories/SettingRepository';
+
+export default class ScrollUseCase {
+ private presenter: ScrollPresenter;
+
+ private settingRepository: SettingRepository;
+
+ constructor({
+ presenter = new ScrollPresenterImpl(),
+ settingRepository = new SettingRepositoryImpl(),
+ } = {}) {
+ this.presenter = presenter;
+ this.settingRepository = settingRepository;
+ }
+
+ scrollVertically(count: number): void {
+ let smooth = this.getSmoothScroll();
+ this.presenter.scrollVertically(count, smooth);
+ }
+
+ scrollHorizonally(count: number): void {
+ let smooth = this.getSmoothScroll();
+ this.presenter.scrollHorizonally(count, smooth);
+ }
+
+ scrollPages(count: number): void {
+ let smooth = this.getSmoothScroll();
+ this.presenter.scrollPages(count, smooth);
+ }
+
+ scrollToTop(): void {
+ let smooth = this.getSmoothScroll();
+ this.presenter.scrollToTop(smooth);
+ }
+
+ scrollToBottom(): void {
+ let smooth = this.getSmoothScroll();
+ this.presenter.scrollToBottom(smooth);
+ }
+
+ scrollToHome(): void {
+ let smooth = this.getSmoothScroll();
+ this.presenter.scrollToHome(smooth);
+ }
+
+ scrollToEnd(): void {
+ let smooth = this.getSmoothScroll();
+ this.presenter.scrollToEnd(smooth);
+ }
+
+ private getSmoothScroll(): boolean {
+ let settings = this.settingRepository.get();
+ return settings.properties.smoothscroll;
+ }
+}