From fcd15f4f09e412cb66f29649572b9d8ae6d50363 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 11 May 2019 13:55:25 +0900 Subject: Find as a controller --- src/content/controllers/FindController.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/content/controllers/FindController.ts (limited to 'src/content/controllers') diff --git a/src/content/controllers/FindController.ts b/src/content/controllers/FindController.ts new file mode 100644 index 0000000..cf27a8d --- /dev/null +++ b/src/content/controllers/FindController.ts @@ -0,0 +1,24 @@ +import * as messages from '../../shared/messages'; +import FindUseCase from '../usecases/FindUseCase'; + +export default class FindController { + private findUseCase: FindUseCase; + + constructor({ + findUseCase = new FindUseCase(), + } = {}) { + this.findUseCase = findUseCase; + } + + async start(m: messages.ConsoleEnterFindMessage): Promise { + await this.findUseCase.startFind(m.text); + } + + async next(_: messages.FindNextMessage): Promise { + await this.findUseCase.findNext(); + } + + async prev(_: messages.FindPrevMessage): Promise { + await this.findUseCase.findPrev(); + } +} -- cgit v1.2.3 From efc48dc7421e3bd48534bc94f84e2b0bd47ae47c Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 11 May 2019 19:43:56 +0900 Subject: Keymaps as a clean architecture [WIP] --- src/content/InputDriver.ts | 80 +++++++++++++++ src/content/client/BackgroundClient.ts | 11 +++ src/content/client/FindMasterClient.ts | 23 +++++ src/content/components/common/index.ts | 4 +- src/content/components/common/input.ts | 80 --------------- src/content/controllers/KeymapController.ts | 139 +++++++++++++++++++++++++++ src/content/index.ts | 46 +++++++-- src/content/presenters/FocusPresenter.ts | 25 +++++ src/content/repositories/KeymapRepository.ts | 23 +++++ src/content/usecases/FindSlaveUseCase.ts | 20 ++++ src/content/usecases/FocusUseCase.ts | 15 +++ src/content/usecases/KeymapUseCase.ts | 100 +++++++++++++++++++ src/content/usecases/NavigateUseCase.ts | 27 ++++++ src/content/usecases/ScrollUseCase.ts | 58 +++++++++++ test/content/InputDriver.test.ts | 129 +++++++++++++++++++++++++ test/content/components/common/input.test.ts | 72 -------------- 16 files changed, 692 insertions(+), 160 deletions(-) create mode 100644 src/content/InputDriver.ts create mode 100644 src/content/client/BackgroundClient.ts create mode 100644 src/content/client/FindMasterClient.ts delete mode 100644 src/content/components/common/input.ts create mode 100644 src/content/controllers/KeymapController.ts create mode 100644 src/content/presenters/FocusPresenter.ts create mode 100644 src/content/repositories/KeymapRepository.ts create mode 100644 src/content/usecases/FindSlaveUseCase.ts create mode 100644 src/content/usecases/FocusUseCase.ts create mode 100644 src/content/usecases/KeymapUseCase.ts create mode 100644 src/content/usecases/NavigateUseCase.ts create mode 100644 src/content/usecases/ScrollUseCase.ts create mode 100644 test/content/InputDriver.test.ts delete mode 100644 test/content/components/common/input.test.ts (limited to 'src/content/controllers') diff --git a/src/content/InputDriver.ts b/src/content/InputDriver.ts new file mode 100644 index 0000000..09648c1 --- /dev/null +++ b/src/content/InputDriver.ts @@ -0,0 +1,80 @@ +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 InputDriver { + private pressed: {[key: string]: string} = {}; + + private onKeyListeners: ((key: keys.Key) => boolean)[] = []; + + constructor(target: HTMLElement) { + this.pressed = {}; + this.onKeyListeners = []; + + target.addEventListener('keypress', this.onKeyPress.bind(this)); + target.addEventListener('keydown', this.onKeyDown.bind(this)); + target.addEventListener('keyup', this.onKeyUp.bind(this)); + } + + onKey(cb: (key: keys.Key) => boolean) { + this.onKeyListeners.push(cb); + } + + private onKeyPress(e: KeyboardEvent) { + if (this.pressed[e.key] && this.pressed[e.key] !== 'keypress') { + return; + } + this.pressed[e.key] = 'keypress'; + this.capture(e); + } + + private onKeyDown(e: KeyboardEvent) { + if (this.pressed[e.key] && this.pressed[e.key] !== 'keydown') { + return; + } + this.pressed[e.key] = 'keydown'; + this.capture(e); + } + + private onKeyUp(e: KeyboardEvent) { + delete this.pressed[e.key]; + } + + // eslint-disable-next-line max-statements + private capture(e: KeyboardEvent) { + let target = e.target; + if (!(target instanceof HTMLElement)) { + return; + } + if (this.fromInput(target)) { + if (cancelKey(e) && target.blur) { + target.blur(); + } + return; + } + if (['Shift', 'Control', 'Alt', 'OS'].includes(e.key)) { + // pressing only meta key is ignored + return; + } + + let key = keys.fromKeyboardEvent(e); + for (let listener of this.onKeyListeners) { + let stop = listener(key); + if (stop) { + e.preventDefault(); + e.stopPropagation(); + break; + } + } + } + + private fromInput(e: Element) { + return e instanceof HTMLInputElement || + e instanceof HTMLTextAreaElement || + e instanceof HTMLSelectElement || + dom.isContentEditable(e); + } +} 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 { + 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/components/common/input.ts b/src/content/components/common/input.ts deleted file mode 100644 index 1fe34c9..0000000 --- a/src/content/components/common/input.ts +++ /dev/null @@ -1,80 +0,0 @@ -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 { - private pressed: {[key: string]: string} = {}; - - private onKeyListeners: ((key: keys.Key) => boolean)[] = []; - - constructor(target: HTMLElement) { - this.pressed = {}; - this.onKeyListeners = []; - - target.addEventListener('keypress', this.onKeyPress.bind(this)); - target.addEventListener('keydown', this.onKeyDown.bind(this)); - target.addEventListener('keyup', this.onKeyUp.bind(this)); - } - - onKey(cb: (key: keys.Key) => boolean) { - this.onKeyListeners.push(cb); - } - - onKeyPress(e: KeyboardEvent) { - if (this.pressed[e.key] && this.pressed[e.key] !== 'keypress') { - return; - } - this.pressed[e.key] = 'keypress'; - this.capture(e); - } - - onKeyDown(e: KeyboardEvent) { - if (this.pressed[e.key] && this.pressed[e.key] !== 'keydown') { - return; - } - this.pressed[e.key] = 'keydown'; - this.capture(e); - } - - onKeyUp(e: KeyboardEvent) { - delete this.pressed[e.key]; - } - - // 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; - } - if (['Shift', 'Control', 'Alt', 'OS'].includes(e.key)) { - // pressing only meta key is ignored - return; - } - - let key = keys.fromKeyboardEvent(e); - for (let listener of this.onKeyListeners) { - let stop = listener(key); - if (stop) { - e.preventDefault(); - e.stopPropagation(); - break; - } - } - } - - fromInput(e: Element) { - return e instanceof HTMLInputElement || - e instanceof HTMLTextAreaElement || - e instanceof HTMLSelectElement || - dom.isContentEditable(e); - } -} 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; + +const reservedKeymaps: Keymaps = { + '': { type: operations.CANCEL }, + '': { 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(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; + } +} diff --git a/test/content/InputDriver.test.ts b/test/content/InputDriver.test.ts new file mode 100644 index 0000000..ac5f95d --- /dev/null +++ b/test/content/InputDriver.test.ts @@ -0,0 +1,129 @@ +import InputDriver from '../../src/content/InputDriver'; +import { expect } from 'chai'; +import { Key } from '../../src/shared/utils/keys'; + +describe('InputDriver', () => { + let target: HTMLElement; + let driver: InputDriver; + + beforeEach(() => { + target = document.createElement('div'); + document.body.appendChild(target); + driver = new InputDriver(target); + }); + + afterEach(() => { + target.remove(); + target = null; + driver = null; + }); + + it('register callbacks', (done) => { + driver.onKey((key: Key): boolean => { + expect(key.key).to.equal('a'); + expect(key.ctrlKey).to.be.true; + expect(key.shiftKey).to.be.false; + expect(key.altKey).to.be.false; + expect(key.metaKey).to.be.false; + done(); + return true; + }); + + target.dispatchEvent(new KeyboardEvent('keydown', { + key: 'a', + ctrlKey: true, + shiftKey: false, + altKey: false, + metaKey: false, + })); + }); + + it('invoke callback once', () => { + let a = 0, b = 0; + driver.onKey((key: Key): boolean => { + if (key.key == 'a') { + ++a; + } else { + key.key == 'b' + ++b; + } + return true; + }); + + let events = [ + new KeyboardEvent('keydown', { key: 'a' }), + new KeyboardEvent('keydown', { key: 'b' }), + new KeyboardEvent('keypress', { key: 'a' }), + new KeyboardEvent('keyup', { key: 'a' }), + new KeyboardEvent('keypress', { key: 'b' }), + new KeyboardEvent('keyup', { key: 'b' }), + ]; + for (let e of events) { + target.dispatchEvent(e); + } + + expect(a).to.equal(1); + expect(b).to.equal(1); + }) + + it('propagates and stop handler chain', () => { + let a = 0, b = 0, c = 0; + driver.onKey((key: Key): boolean => { + a++; + return false; + }); + driver.onKey((key: Key): boolean => { + b++; + return true; + }); + driver.onKey((key: Key): boolean => { + c++; + return true; + }); + + target.dispatchEvent(new KeyboardEvent('keydown', { key: 'b' })); + + expect(a).to.equal(1); + expect(b).to.equal(1); + expect(c).to.equal(0); + }) + + it('does not invoke only meta keys', () => { + driver.onKey((key: Key): boolean=> { + expect.fail(); + return false; + }); + + target.dispatchEvent(new KeyboardEvent('keydown', { key: 'Shift' })); + target.dispatchEvent(new KeyboardEvent('keydown', { key: 'Control' })); + target.dispatchEvent(new KeyboardEvent('keydown', { key: 'Alt' })); + target.dispatchEvent(new KeyboardEvent('keydown', { key: 'OS' })); + }) + + it('ignores events from input elements', () => { + ['input', 'textarea', 'select'].forEach((name) => { + let input = window.document.createElement(name); + let driver = new InputDriver(input); + driver.onKey((key: Key): boolean => { + expect.fail(); + return false; + }); + input.dispatchEvent(new KeyboardEvent('keydown', { key: 'x' })); + }); + }); + + it('ignores events from contenteditable elements', () => { + let div = window.document.createElement('div'); + let driver = new InputDriver(div); + driver.onKey((key: Key): boolean => { + expect.fail(); + return false; + }); + + div.setAttribute('contenteditable', ''); + div.dispatchEvent(new KeyboardEvent('keydown', { key: 'x' })); + + div.setAttribute('contenteditable', 'true'); + div.dispatchEvent(new KeyboardEvent('keydown', { key: 'x' })); + }); +}); diff --git a/test/content/components/common/input.test.ts b/test/content/components/common/input.test.ts deleted file mode 100644 index f3a943c..0000000 --- a/test/content/components/common/input.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import InputComponent from 'content/components/common/input'; - -describe('InputComponent', () => { - it('register callbacks', () => { - let component = new InputComponent(window.document); - let key = { key: 'a', ctrlKey: true, shiftKey: false, altKey: false, metaKey: false }; - component.onKey((key) => { - expect(key).to.deep.equal(key); - }); - component.onKeyDown(key); - }); - - it('invoke callback once', () => { - let component = new InputComponent(window.document); - let a = 0, b = 0; - component.onKey((key) => { - if (key.key == 'a') { - ++a; - } else { - key.key == 'b' - ++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); - }) - - it('does not invoke only meta keys', () => { - let component = new InputComponent(window.document); - component.onKey((key) => { - expect.fail(); - }); - component.onKeyDown({ key: 'Shift' }); - component.onKeyDown({ key: 'Control' }); - component.onKeyDown({ key: 'Alt' }); - component.onKeyDown({ key: 'OS' }); - }) - - it('ignores events from input elements', () => { - ['input', 'textarea', 'select'].forEach((name) => { - let target = window.document.createElement(name); - let component = new InputComponent(target); - component.onKey((key) => { - expect.fail(); - }); - component.onKeyDown({ key: 'x', target }); - }); - }); - - it('ignores events from contenteditable elements', () => { - let target = window.document.createElement('div'); - let component = new InputComponent(target); - component.onKey((key) => { - expect.fail(); - }); - - target.setAttribute('contenteditable', ''); - component.onKeyDown({ key: 'x', target }); - - target.setAttribute('contenteditable', 'true'); - component.onKeyDown({ key: 'x', target }); - }) -}); -- cgit v1.2.3 From 2ec912c262b51fe9523ebf74d5062d0b9bbdab71 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Tue, 14 May 2019 20:28:22 +0900 Subject: Mark keys as a clean archtecture --- src/content/controllers/AddonEnabledController.ts | 19 +++++++++ src/content/controllers/KeymapController.ts | 15 +++++-- src/content/controllers/MarkController.ts | 16 +++++++ src/content/controllers/MarkKeyController.ts | 31 ++++++++++++++ src/content/index.ts | 30 ++++++++++++- src/content/repositories/MarkKeyRepository.ts | 52 +++++++++++++++++++++++ src/content/usecases/MarkKeyUseCase.ts | 36 ++++++++++++++++ src/content/usecases/MarkUseCase.ts | 8 +++- 8 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 src/content/controllers/AddonEnabledController.ts create mode 100644 src/content/controllers/MarkController.ts create mode 100644 src/content/controllers/MarkKeyController.ts create mode 100644 src/content/repositories/MarkKeyRepository.ts create mode 100644 src/content/usecases/MarkKeyUseCase.ts (limited to 'src/content/controllers') diff --git a/src/content/controllers/AddonEnabledController.ts b/src/content/controllers/AddonEnabledController.ts new file mode 100644 index 0000000..4e19b6a --- /dev/null +++ b/src/content/controllers/AddonEnabledController.ts @@ -0,0 +1,19 @@ +import * as messages from '../../shared/messages'; +import AddonEnabledUseCase from '../usecases/AddonEnabledUseCase'; + +export default class AddonEnabledController { + private addonEnabledUseCase: AddonEnabledUseCase; + + constructor({ + addonEnabledUseCase = new AddonEnabledUseCase(), + } = {}) { + this.addonEnabledUseCase = addonEnabledUseCase; + } + + getAddonEnabled( + _message: messages.AddonEnabledQueryMessage, + ): Promise { + let enabled = this.addonEnabledUseCase.getEnabled(); + return Promise.resolve(enabled); + } +} diff --git a/src/content/controllers/KeymapController.ts b/src/content/controllers/KeymapController.ts index 09e5b0c..b7a7bc2 100644 --- a/src/content/controllers/KeymapController.ts +++ b/src/content/controllers/KeymapController.ts @@ -7,6 +7,7 @@ import NavigateUseCase from '../usecases/NavigateUseCase'; import FocusUseCase from '../usecases/FocusUseCase'; import ClipboardUseCase from '../usecases/ClipboardUseCase'; import BackgroundClient from '../client/BackgroundClient'; +import MarkKeyyUseCase from '../usecases/MarkKeyUseCase'; import { Key } from '../../shared/utils/keys'; export default class KeymapController { @@ -26,6 +27,8 @@ export default class KeymapController { private backgroundClient: BackgroundClient; + private markKeyUseCase: MarkKeyyUseCase; + constructor({ keymapUseCase = new KeymapUseCase(), addonEnabledUseCase = new AddonEnabledUseCase(), @@ -35,6 +38,7 @@ export default class KeymapController { focusUseCase = new FocusUseCase(), clipbaordUseCase = new ClipboardUseCase(), backgroundClient = new BackgroundClient(), + markKeyUseCase = new MarkKeyyUseCase(), } = {}) { this.keymapUseCase = keymapUseCase; this.addonEnabledUseCase = addonEnabledUseCase; @@ -44,6 +48,7 @@ export default class KeymapController { this.focusUseCase = focusUseCase; this.clipbaordUseCase = clipbaordUseCase; this.backgroundClient = backgroundClient; + this.markKeyUseCase = markKeyUseCase; } // eslint-disable-next-line complexity, max-lines-per-function @@ -98,10 +103,12 @@ export default class KeymapController { // background: operation.background, // }), '*'); // break; - // case operations.MARK_SET_PREFIX: - // return markActions.startSet(); - // case operations.MARK_JUMP_PREFIX: - // return markActions.startJump(); + case operations.MARK_SET_PREFIX: + this.markKeyUseCase.enableSetMode(); + break; + case operations.MARK_JUMP_PREFIX: + this.markKeyUseCase.enableJumpMode(); + break; case operations.NAVIGATE_HISTORY_PREV: this.navigateUseCase.openHistoryPrev(); break; diff --git a/src/content/controllers/MarkController.ts b/src/content/controllers/MarkController.ts new file mode 100644 index 0000000..365794c --- /dev/null +++ b/src/content/controllers/MarkController.ts @@ -0,0 +1,16 @@ +import * as messages from '../../shared/messages'; +import MarkUseCase from '../usecases/MarkUseCase'; + +export default class MarkController { + private markUseCase: MarkUseCase; + + constructor({ + markUseCase = new MarkUseCase(), + } = {}) { + this.markUseCase = markUseCase; + } + + scrollTo(message: messages.TabScrollToMessage) { + this.markUseCase.scroll(message.x, message.y); + } +} diff --git a/src/content/controllers/MarkKeyController.ts b/src/content/controllers/MarkKeyController.ts new file mode 100644 index 0000000..9406fbf --- /dev/null +++ b/src/content/controllers/MarkKeyController.ts @@ -0,0 +1,31 @@ +import MarkUseCase from '../usecases/MarkUseCase'; +import MarkKeyyUseCase from '../usecases/MarkKeyUseCase'; +import * as keys from '../../shared/utils/keys'; + +export default class MarkKeyController { + private markUseCase: MarkUseCase; + + private markKeyUseCase: MarkKeyyUseCase; + + constructor({ + markUseCase = new MarkUseCase(), + markKeyUseCase = new MarkKeyyUseCase(), + } = {}) { + this.markUseCase = markUseCase; + this.markKeyUseCase = markKeyUseCase; + } + + press(key: keys.Key): boolean { + if (this.markKeyUseCase.isSetMode()) { + this.markUseCase.set(key.key); + this.markKeyUseCase.disableSetMode(); + return true; + } + if (this.markKeyUseCase.isJumpMode()) { + this.markUseCase.jump(key.key); + this.markKeyUseCase.disableJumpMode(); + return true; + } + return false; + } +} diff --git a/src/content/index.ts b/src/content/index.ts index f983f9f..9b3d652 100644 --- a/src/content/index.ts +++ b/src/content/index.ts @@ -4,12 +4,15 @@ import consoleFrameStyle from './site-style'; // import { newStore } from './store'; import MessageListener from './MessageListener'; import FindController from './controllers/FindController'; +import MarkController from './controllers/MarkController'; 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'; +import MarkKeyController from './controllers/MarkKeyController'; +import AddonEnabledController from './controllers/AddonEnabledController'; // const store = newStore(); @@ -28,14 +31,28 @@ if (window.self === window.top) { } return undefined; }); + + let markController = new MarkController(); + let addonEnabledController = new AddonEnabledController(); + + new MessageListener().onBackgroundMessage((message: messages.Message) => { + switch (message.type) { + case messages.ADDON_ENABLED_QUERY: + return addonEnabledController.getAddonEnabled(message); + case messages.TAB_SCROLL_TO: + return markController.scrollTo(message); + } + return undefined; + }); } else { // new FrameContentComponent(window, store); // eslint-disable-line no-new } let keymapController = new KeymapController(); +let markKeyController = new MarkKeyController(); let inputDriver = new InputDriver(document.body); // inputDriver.onKey(key => followSlaveController.pressKey(key)); -// inputDriver.onKey(key => markController.pressKey(key)); +inputDriver.onKey(key => markKeyController.press(key)); inputDriver.onKey(key => keymapController.press(key)); let style = window.document.createElement('style'); @@ -64,3 +81,14 @@ const reloadSettings = async() => { } }; reloadSettings(); + +new MessageListener().onBackgroundMessage((message: messages.Message): any => { + let addonEnabledUseCase = new AddonEnabledUseCase(); + + switch (message.type) { + case messages.SETTINGS_CHANGED: + return reloadSettings(); + case messages.ADDON_TOGGLE_ENABLED: + return addonEnabledUseCase.toggle(); + } +}); diff --git a/src/content/repositories/MarkKeyRepository.ts b/src/content/repositories/MarkKeyRepository.ts new file mode 100644 index 0000000..c24548a --- /dev/null +++ b/src/content/repositories/MarkKeyRepository.ts @@ -0,0 +1,52 @@ +export default interface MarkKeyRepository { + isSetMode(): boolean; + + enableSetMode(): void; + + disabeSetMode(): void; + + isJumpMode(): boolean; + + enableJumpMode(): void; + + disabeJumpMode(): void; + + // eslint-disable-next-line semi +} + +interface Mode { + setMode: boolean; + jumpMode: boolean; +} + +let current: Mode = { + setMode: false, + jumpMode: false, +}; + +export class MarkKeyRepositoryImpl implements MarkKeyRepository { + + isSetMode(): boolean { + return current.setMode; + } + + enableSetMode(): void { + current.setMode = true; + } + + disabeSetMode(): void { + current.setMode = false; + } + + isJumpMode(): boolean { + return current.jumpMode; + } + + enableJumpMode(): void { + current.jumpMode = true; + } + + disabeJumpMode(): void { + current.jumpMode = false; + } +} diff --git a/src/content/usecases/MarkKeyUseCase.ts b/src/content/usecases/MarkKeyUseCase.ts new file mode 100644 index 0000000..c0aa655 --- /dev/null +++ b/src/content/usecases/MarkKeyUseCase.ts @@ -0,0 +1,36 @@ +import MarkKeyRepository, { MarkKeyRepositoryImpl } + from '../repositories/MarkKeyRepository'; + +export default class MarkKeyUseCase { + private repository: MarkKeyRepository; + + constructor({ + repository = new MarkKeyRepositoryImpl() + } = {}) { + this.repository = repository; + } + + isSetMode(): boolean { + return this.repository.isSetMode(); + } + + isJumpMode(): boolean { + return this.repository.isJumpMode(); + } + + enableSetMode(): void { + this.repository.enableSetMode(); + } + + disableSetMode(): void { + this.repository.disabeSetMode(); + } + + enableJumpMode(): void { + this.repository.enableJumpMode(); + } + + disableJumpMode(): void { + this.repository.disabeJumpMode(); + } +} diff --git a/src/content/usecases/MarkUseCase.ts b/src/content/usecases/MarkUseCase.ts index ec63f2b..530f141 100644 --- a/src/content/usecases/MarkUseCase.ts +++ b/src/content/usecases/MarkUseCase.ts @@ -51,11 +51,15 @@ export default class MarkUseCase { if (!pos) { throw new Error('Mark is not set'); } - let smooth = this.settingRepository.get().properties.smoothscroll; - this.scrollPresenter.scrollTo(pos.x, pos.y, smooth); + this.scroll(pos.x, pos.y); } } + scroll(x: number, y: number): void { + let smooth = this.settingRepository.get().properties.smoothscroll; + this.scrollPresenter.scrollTo(x, y, smooth); + } + private globalKey(key: string) { return (/^[A-Z0-9]$/).test(key); } -- cgit v1.2.3 From a5518dce3d101cb1cb65724b82079f66f20c80c8 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 18 May 2019 21:43:56 +0900 Subject: Define Key and KeySequence --- src/content/InputDriver.ts | 6 +- src/content/actions/index.ts | 4 +- src/content/actions/input.ts | 4 +- src/content/client/FollowMasterClient.ts | 2 +- src/content/components/common/follow.ts | 2 +- src/content/components/common/index.ts | 12 +- src/content/components/common/keymapper.ts | 87 -------------- src/content/components/common/mark.ts | 6 +- src/content/controllers/KeymapController.ts | 2 +- src/content/controllers/MarkKeyController.ts | 4 +- src/content/domains/Key.ts | 74 ++++++++++++ src/content/domains/KeySequence.ts | 64 +++++++++++ src/content/reducers/input.ts | 4 +- src/content/repositories/KeymapRepository.ts | 11 +- src/content/usecases/KeymapUseCase.ts | 35 ++---- src/shared/utils/keys.ts | 99 ---------------- test/content/domains/Key.test.ts | 137 ++++++++++++++++++++++ test/content/domains/KeySequence.test.ts | 72 ++++++++++++ test/shared/utils/keys.test.ts | 164 --------------------------- 19 files changed, 387 insertions(+), 402 deletions(-) delete mode 100644 src/content/components/common/keymapper.ts create mode 100644 src/content/domains/Key.ts create mode 100644 src/content/domains/KeySequence.ts delete mode 100644 src/shared/utils/keys.ts create mode 100644 test/content/domains/Key.test.ts create mode 100644 test/content/domains/KeySequence.test.ts delete mode 100644 test/shared/utils/keys.test.ts (limited to 'src/content/controllers') diff --git a/src/content/InputDriver.ts b/src/content/InputDriver.ts index 09648c1..cddc825 100644 --- a/src/content/InputDriver.ts +++ b/src/content/InputDriver.ts @@ -1,5 +1,5 @@ import * as dom from '../shared/utils/dom'; -import * as keys from '../shared/utils/keys'; +import Key, * as keys from './domains/Key'; const cancelKey = (e: KeyboardEvent): boolean => { return e.key === 'Escape' || e.key === '[' && e.ctrlKey; @@ -8,7 +8,7 @@ const cancelKey = (e: KeyboardEvent): boolean => { export default class InputDriver { private pressed: {[key: string]: string} = {}; - private onKeyListeners: ((key: keys.Key) => boolean)[] = []; + private onKeyListeners: ((key: Key) => boolean)[] = []; constructor(target: HTMLElement) { this.pressed = {}; @@ -19,7 +19,7 @@ export default class InputDriver { target.addEventListener('keyup', this.onKeyUp.bind(this)); } - onKey(cb: (key: keys.Key) => boolean) { + onKey(cb: (key: Key) => boolean) { this.onKeyListeners.push(cb); } diff --git a/src/content/actions/index.ts b/src/content/actions/index.ts index eb826fc..49f6484 100644 --- a/src/content/actions/index.ts +++ b/src/content/actions/index.ts @@ -1,5 +1,5 @@ import Redux from 'redux'; -import * as keyUtils from '../../shared/utils/keys'; +import Key from '../domains/Key'; // User input export const INPUT_KEY_PRESS = 'input.key.press'; @@ -25,7 +25,7 @@ export const NOOP = 'noop'; export interface InputKeyPressAction extends Redux.Action { type: typeof INPUT_KEY_PRESS; - key: keyUtils.Key; + key: Key; } export interface InputClearKeysAction extends Redux.Action { diff --git a/src/content/actions/input.ts b/src/content/actions/input.ts index 1df6452..24dbb99 100644 --- a/src/content/actions/input.ts +++ b/src/content/actions/input.ts @@ -1,7 +1,7 @@ import * as actions from './index'; -import * as keyUtils from '../../shared/utils/keys'; +import Key from '../domains/Key'; -const keyPress = (key: keyUtils.Key): actions.InputAction => { +const keyPress = (key: Key): actions.InputAction => { return { type: actions.INPUT_KEY_PRESS, key, diff --git a/src/content/client/FollowMasterClient.ts b/src/content/client/FollowMasterClient.ts index 464b52f..c841902 100644 --- a/src/content/client/FollowMasterClient.ts +++ b/src/content/client/FollowMasterClient.ts @@ -1,5 +1,5 @@ import * as messages from '../../shared/messages'; -import { Key } from '../../shared/utils/keys'; +import Key from '../domains/Key'; export default interface FollowMasterClient { startFollow(newTab: boolean, background: boolean): void; diff --git a/src/content/components/common/follow.ts b/src/content/components/common/follow.ts index e0003e3..413244e 100644 --- a/src/content/components/common/follow.ts +++ b/src/content/components/common/follow.ts @@ -1,7 +1,7 @@ import MessageListener from '../../MessageListener'; import { LinkHint, InputHint } from '../../presenters/Hint'; import * as messages from '../../../shared/messages'; -import { Key } from '../../../shared/utils/keys'; +import Key from '../../domains/Key'; import TabsClient, { TabsClientImpl } from '../../client/TabsClient'; import FollowMasterClient, { FollowMasterClientImpl } from '../../client/FollowMasterClient'; diff --git a/src/content/components/common/index.ts b/src/content/components/common/index.ts index c74020e..1aacf51 100644 --- a/src/content/components/common/index.ts +++ b/src/content/components/common/index.ts @@ -1,11 +1,11 @@ import InputDriver from './../../InputDriver'; import FollowComponent from './follow'; import MarkComponent from './mark'; -import KeymapperComponent from './keymapper'; +// import KeymapperComponent from './keymapper'; import * as messages from '../../../shared/messages'; import MessageListener from '../../MessageListener'; import * as blacklists from '../../../shared/blacklists'; -import * as keys from '../../../shared/utils/keys'; +import Key from '../../domains/Key'; import AddonEnabledUseCase from '../../usecases/AddonEnabledUseCase'; import SettingUseCase from '../../usecases/SettingUseCase'; @@ -18,11 +18,11 @@ export default class Common { const input = new InputDriver(win.document.body); const follow = new FollowComponent(); const mark = new MarkComponent(store); - const keymapper = new KeymapperComponent(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)); + input.onKey((key: Key) => follow.key(key)); + input.onKey((key: Key) => mark.key(key)); + // input.onKey((key: Key) => keymapper.key(key)); this.reloadSettings(); diff --git a/src/content/components/common/keymapper.ts b/src/content/components/common/keymapper.ts deleted file mode 100644 index c901ffe..0000000 --- a/src/content/components/common/keymapper.ts +++ /dev/null @@ -1,87 +0,0 @@ -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'; - -import AddonEnabledUseCase from '../../usecases/AddonEnabledUseCase'; -import { SettingRepositoryImpl } from '../../repositories/SettingRepository'; -import { Keymaps } from '../../../shared/Settings'; - -type KeymapEntityMap = Map; - -let addonEnabledUseCase = new AddonEnabledUseCase(); -let settingRepository = new SettingRepositoryImpl(); - -const reservedKeymaps: Keymaps = { - '': { type: operations.CANCEL }, - '': { 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 KeymapperComponent { - private store: any; - - constructor(store: any) { - this.store = store; - } - - key(key: keyUtils.Key): boolean { - this.store.dispatch(inputActions.keyPress(key)); - - let input = this.store.getState().input; - let keymaps = this.keymapEntityMap(); - let matched = Array.from(keymaps.keys()).filter( - (mapping: keyUtils.Key[]) => { - return mapStartsWith(mapping, input.keys); - }); - if (!addonEnabledUseCase.getEnabled()) { - // available keymaps are only ADDON_ENABLE and ADDON_TOGGLE_ENABLED if - // the addon disabled - matched = matched.filter((keys) => { - let type = (keymaps.get(keys) as operations.Operation).type; - return type === operations.ADDON_ENABLE || - type === operations.ADDON_TOGGLE_ENABLED; - }); - } - if (matched.length === 0) { - this.store.dispatch(inputActions.clearKeys()); - return false; - } else if (matched.length > 1 || - matched.length === 1 && input.keys.length < matched[0].length) { - return true; - } - let operation = keymaps.get(matched[0]) as operations.Operation; - let act = operationActions.exec(operation); - this.store.dispatch(act); - this.store.dispatch(inputActions.clearKeys()); - return true; - } - - private keymapEntityMap(): KeymapEntityMap { - let keymaps = { - ...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(entries); - } -} diff --git a/src/content/components/common/mark.ts b/src/content/components/common/mark.ts index eec95d6..058b873 100644 --- a/src/content/components/common/mark.ts +++ b/src/content/components/common/mark.ts @@ -1,12 +1,12 @@ import * as markActions from '../../actions/mark'; import * as consoleFrames from '../..//console-frames'; -import * as keyUtils from '../../../shared/utils/keys'; +import Key from '../../domains/Key'; import MarkUseCase from '../../usecases/MarkUseCase'; let markUseCase = new MarkUseCase(); -const cancelKey = (key: keyUtils.Key): boolean => { +const cancelKey = (key: Key): boolean => { return key.key === 'Esc' || key.key === '[' && Boolean(key.ctrlKey); }; @@ -18,7 +18,7 @@ export default class MarkComponent { } // eslint-disable-next-line max-statements - key(key: keyUtils.Key) { + key(key: Key) { let { mark: markState } = this.store.getState(); if (!markState.setMode && !markState.jumpMode) { diff --git a/src/content/controllers/KeymapController.ts b/src/content/controllers/KeymapController.ts index b7a7bc2..424292c 100644 --- a/src/content/controllers/KeymapController.ts +++ b/src/content/controllers/KeymapController.ts @@ -8,7 +8,7 @@ import FocusUseCase from '../usecases/FocusUseCase'; import ClipboardUseCase from '../usecases/ClipboardUseCase'; import BackgroundClient from '../client/BackgroundClient'; import MarkKeyyUseCase from '../usecases/MarkKeyUseCase'; -import { Key } from '../../shared/utils/keys'; +import Key from '../domains/Key'; export default class KeymapController { private keymapUseCase: KeymapUseCase; diff --git a/src/content/controllers/MarkKeyController.ts b/src/content/controllers/MarkKeyController.ts index 9406fbf..395dee3 100644 --- a/src/content/controllers/MarkKeyController.ts +++ b/src/content/controllers/MarkKeyController.ts @@ -1,6 +1,6 @@ import MarkUseCase from '../usecases/MarkUseCase'; import MarkKeyyUseCase from '../usecases/MarkKeyUseCase'; -import * as keys from '../../shared/utils/keys'; +import Key from '../domains/Key'; export default class MarkKeyController { private markUseCase: MarkUseCase; @@ -15,7 +15,7 @@ export default class MarkKeyController { this.markKeyUseCase = markKeyUseCase; } - press(key: keys.Key): boolean { + press(key: Key): boolean { if (this.markKeyUseCase.isSetMode()) { this.markUseCase.set(key.key); this.markKeyUseCase.disableSetMode(); diff --git a/src/content/domains/Key.ts b/src/content/domains/Key.ts new file mode 100644 index 0000000..fbbb4bb --- /dev/null +++ b/src/content/domains/Key.ts @@ -0,0 +1,74 @@ +export default interface Key { + key: string; + shiftKey?: boolean; + ctrlKey?: boolean; + altKey?: boolean; + metaKey?: boolean; + + // eslint-disable-next-line semi +} + +const modifiedKeyName = (name: string): string => { + if (name === ' ') { + return 'Space'; + } + if (name.length === 1) { + return name; + } else if (name === 'Escape') { + return 'Esc'; + } + return name; +}; + +export const fromKeyboardEvent = (e: KeyboardEvent): Key => { + let key = modifiedKeyName(e.key); + let shift = e.shiftKey; + if (key.length === 1 && key.toUpperCase() === key.toLowerCase()) { + // make shift false for symbols to enable key bindings by symbold keys. + // But this limits key bindings by symbol keys with Shift (such as Shift+$>. + shift = false; + } + + return { + key: modifiedKeyName(e.key), + shiftKey: shift, + ctrlKey: e.ctrlKey, + altKey: e.altKey, + metaKey: e.metaKey, + }; +}; + +export const fromMapKey = (key: string): Key => { + if (key.startsWith('<') && key.endsWith('>')) { + let inner = key.slice(1, -1); + let shift = inner.includes('S-'); + let base = inner.slice(inner.lastIndexOf('-') + 1); + if (shift && base.length === 1) { + base = base.toUpperCase(); + } else if (!shift && base.length === 1) { + base = base.toLowerCase(); + } + return { + key: base, + shiftKey: inner.includes('S-'), + ctrlKey: inner.includes('C-'), + altKey: inner.includes('A-'), + metaKey: inner.includes('M-'), + }; + } + return { + key: key, + shiftKey: key.toLowerCase() !== key, + ctrlKey: false, + altKey: false, + metaKey: false, + }; +}; + +export const equals = (e1: Key, e2: Key): boolean => { + return e1.key === e2.key && + e1.ctrlKey === e2.ctrlKey && + e1.metaKey === e2.metaKey && + e1.altKey === e2.altKey && + e1.shiftKey === e2.shiftKey; +}; diff --git a/src/content/domains/KeySequence.ts b/src/content/domains/KeySequence.ts new file mode 100644 index 0000000..6a05c2f --- /dev/null +++ b/src/content/domains/KeySequence.ts @@ -0,0 +1,64 @@ +import Key, * as keyUtils from './Key'; + +export default class KeySequence { + private keys: Key[]; + + private constructor(keys: Key[]) { + this.keys = keys; + } + + static from(keys: Key[]): KeySequence { + return new KeySequence(keys); + } + + push(key: Key): number { + return this.keys.push(key); + } + + length(): number { + return this.keys.length; + } + + startsWith(o: KeySequence): boolean { + if (this.keys.length < o.keys.length) { + return false; + } + for (let i = 0; i < o.keys.length; ++i) { + if (!keyUtils.equals(this.keys[i], o.keys[i])) { + return false; + } + } + return true; + } + + getKeyArray(): Key[] { + return this.keys; + } +} + +export const fromMapKeys = (keys: string): KeySequence => { + const fromMapKeysRecursive = ( + remainings: string, mappedKeys: Key[], + ): Key[] => { + if (remainings.length === 0) { + return mappedKeys; + } + + let nextPos = 1; + if (remainings.startsWith('<')) { + let ltPos = remainings.indexOf('>'); + if (ltPos > 0) { + nextPos = ltPos + 1; + } + } + + return fromMapKeysRecursive( + remainings.slice(nextPos), + mappedKeys.concat([keyUtils.fromMapKey(remainings.slice(0, nextPos))]) + ); + }; + + let data = fromMapKeysRecursive(keys, []); + return KeySequence.from(data); +}; + diff --git a/src/content/reducers/input.ts b/src/content/reducers/input.ts index 35b9075..800a8f0 100644 --- a/src/content/reducers/input.ts +++ b/src/content/reducers/input.ts @@ -1,8 +1,8 @@ import * as actions from '../actions'; -import * as keyUtils from '../../shared/utils/keys'; +import Key from '../domains/Key'; export interface State { - keys: keyUtils.Key[], + keys: Key[], } const defaultState: State = { diff --git a/src/content/repositories/KeymapRepository.ts b/src/content/repositories/KeymapRepository.ts index 081cc54..770ba0b 100644 --- a/src/content/repositories/KeymapRepository.ts +++ b/src/content/repositories/KeymapRepository.ts @@ -1,23 +1,24 @@ -import { Key } from '../../shared/utils/keys'; +import Key from '../domains/Key'; +import KeySequence from '../domains/KeySequence'; export default interface KeymapRepository { - enqueueKey(key: Key): Key[]; + enqueueKey(key: Key): KeySequence; clear(): void; // eslint-disable-next-line semi } -let current: Key[] = []; +let current: KeySequence = KeySequence.from([]); export class KeymapRepositoryImpl { - enqueueKey(key: Key): Key[] { + enqueueKey(key: Key): KeySequence { current.push(key); return current; } clear(): void { - current = []; + current = KeySequence.from([]); } } diff --git a/src/content/usecases/KeymapUseCase.ts b/src/content/usecases/KeymapUseCase.ts index a4f9c36..af0ad77 100644 --- a/src/content/usecases/KeymapUseCase.ts +++ b/src/content/usecases/KeymapUseCase.ts @@ -7,29 +7,16 @@ import AddonEnabledRepository, { AddonEnabledRepositoryImpl } import * as operations from '../../shared/operations'; import { Keymaps } from '../../shared/Settings'; -import * as keyUtils from '../../shared/utils/keys'; +import Key from '../domains/Key'; +import KeySequence, * as keySequenceUtils from '../domains/KeySequence'; -type KeymapEntityMap = Map; +type KeymapEntityMap = Map; const reservedKeymaps: Keymaps = { '': { type: operations.CANCEL }, '': { 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; @@ -48,13 +35,13 @@ export default class KeymapUseCase { this.addonEnabledRepository = addonEnabledRepository; } - nextOp(key: keyUtils.Key): operations.Operation | null { - let keys = this.repository.enqueueKey(key); + nextOp(key: Key): operations.Operation | null { + let sequence = this.repository.enqueueKey(key); let keymaps = this.keymapEntityMap(); let matched = Array.from(keymaps.keys()).filter( - (mapping: keyUtils.Key[]) => { - return mapStartsWith(mapping, keys); + (mapping: KeySequence) => { + return mapping.startsWith(sequence); }); if (!this.addonEnabledRepository.get()) { // available keymaps are only ADDON_ENABLE and ADDON_TOGGLE_ENABLED if @@ -70,7 +57,7 @@ export default class KeymapUseCase { this.repository.clear(); return null; } else if (matched.length > 1 || - matched.length === 1 && keys.length < matched[0].length) { + matched.length === 1 && sequence.length() < matched[0].length()) { // More than one operations are matched return null; } @@ -91,10 +78,10 @@ export default class KeymapUseCase { }; let entries = Object.entries(keymaps).map((entry) => { return [ - keyUtils.fromMapKeys(entry[0]), + keySequenceUtils.fromMapKeys(entry[0]), entry[1], ]; - }) as [keyUtils.Key[], operations.Operation][]; - return new Map(entries); + }) as [KeySequence, operations.Operation][]; + return new Map(entries); } } diff --git a/src/shared/utils/keys.ts b/src/shared/utils/keys.ts deleted file mode 100644 index e9b0365..0000000 --- a/src/shared/utils/keys.ts +++ /dev/null @@ -1,99 +0,0 @@ -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'; - } - if (name.length === 1) { - return name; - } else if (name === 'Escape') { - return 'Esc'; - } - return name; -}; - -const fromKeyboardEvent = (e: KeyboardEvent): Key => { - let key = modifiedKeyName(e.key); - let shift = e.shiftKey; - if (key.length === 1 && key.toUpperCase() === key.toLowerCase()) { - // make shift false for symbols to enable key bindings by symbold keys. - // But this limits key bindings by symbol keys with Shift (such as Shift+$>. - shift = false; - } - - return { - key: modifiedKeyName(e.key), - shiftKey: shift, - ctrlKey: e.ctrlKey, - altKey: e.altKey, - metaKey: e.metaKey, - }; -}; - -const fromMapKey = (key: string): Key => { - if (key.startsWith('<') && key.endsWith('>')) { - let inner = key.slice(1, -1); - let shift = inner.includes('S-'); - let base = inner.slice(inner.lastIndexOf('-') + 1); - if (shift && base.length === 1) { - base = base.toUpperCase(); - } else if (!shift && base.length === 1) { - base = base.toLowerCase(); - } - return { - key: base, - shiftKey: inner.includes('S-'), - ctrlKey: inner.includes('C-'), - altKey: inner.includes('A-'), - metaKey: inner.includes('M-'), - }; - } - return { - key: key, - shiftKey: key.toLowerCase() !== key, - ctrlKey: false, - altKey: false, - metaKey: false, - }; -}; - -const fromMapKeys = (keys: string): Key[] => { - const fromMapKeysRecursive = ( - remainings: string, mappedKeys: Key[], - ): Key[] => { - if (remainings.length === 0) { - return mappedKeys; - } - - let nextPos = 1; - if (remainings.startsWith('<')) { - let ltPos = remainings.indexOf('>'); - if (ltPos > 0) { - nextPos = ltPos + 1; - } - } - - return fromMapKeysRecursive( - remainings.slice(nextPos), - mappedKeys.concat([fromMapKey(remainings.slice(0, nextPos))]) - ); - }; - - return fromMapKeysRecursive(keys, []); -}; - -const equals = (e1: Key, e2: Key): boolean => { - return e1.key === e2.key && - e1.ctrlKey === e2.ctrlKey && - e1.metaKey === e2.metaKey && - e1.altKey === e2.altKey && - e1.shiftKey === e2.shiftKey; -}; - -export { fromKeyboardEvent, fromMapKey, fromMapKeys, equals }; diff --git a/test/content/domains/Key.test.ts b/test/content/domains/Key.test.ts new file mode 100644 index 0000000..b3f9fb6 --- /dev/null +++ b/test/content/domains/Key.test.ts @@ -0,0 +1,137 @@ +import Key, * as keys from '../../../src/content/domains/Key'; +import { expect } from 'chai' + +describe("Key", () => { + describe('fromKeyboardEvent', () => { + it('returns from keyboard input Ctrl+X', () => { + let k = keys.fromKeyboardEvent(new KeyboardEvent('keydown', { + key: 'x', shiftKey: false, ctrlKey: true, altKey: false, metaKey: true, + })); + expect(k.key).to.equal('x'); + expect(k.shiftKey).to.be.false; + expect(k.ctrlKey).to.be.true; + expect(k.altKey).to.be.false; + expect(k.metaKey).to.be.true; + }); + + it('returns from keyboard input Shift+Esc', () => { + let k = keys.fromKeyboardEvent(new KeyboardEvent('keydown', { + key: 'Escape', shiftKey: true, ctrlKey: false, altKey: false, metaKey: true + })); + expect(k.key).to.equal('Esc'); + expect(k.shiftKey).to.be.true; + expect(k.ctrlKey).to.be.false; + expect(k.altKey).to.be.false; + expect(k.metaKey).to.be.true; + }); + + it('returns from keyboard input Ctrl+$', () => { + // $ required shift pressing on most keyboards + let k = keys.fromKeyboardEvent(new KeyboardEvent('keydown', { + key: '$', shiftKey: true, ctrlKey: true, altKey: false, metaKey: false + })); + expect(k.key).to.equal('$'); + expect(k.shiftKey).to.be.false; + expect(k.ctrlKey).to.be.true; + expect(k.altKey).to.be.false; + expect(k.metaKey).to.be.false; + }); + + it('returns from keyboard input Crtl+Space', () => { + let k = keys.fromKeyboardEvent(new KeyboardEvent('keydown', { + key: ' ', shiftKey: false, ctrlKey: true, altKey: false, metaKey: false + })); + expect(k.key).to.equal('Space'); + expect(k.shiftKey).to.be.false; + expect(k.ctrlKey).to.be.true; + expect(k.altKey).to.be.false; + expect(k.metaKey).to.be.false; + }); + }); + + describe('fromMapKey', () => { + it('return for X', () => { + let key = keys.fromMapKey('x'); + expect(key.key).to.equal('x'); + expect(key.shiftKey).to.be.false; + expect(key.ctrlKey).to.be.false; + expect(key.altKey).to.be.false; + expect(key.metaKey).to.be.false; + }); + + it('return for Shift+X', () => { + let key = keys.fromMapKey('X'); + expect(key.key).to.equal('X'); + expect(key.shiftKey).to.be.true; + expect(key.ctrlKey).to.be.false; + expect(key.altKey).to.be.false; + expect(key.metaKey).to.be.false; + }); + + it('return for Ctrl+X', () => { + let key = keys.fromMapKey(''); + expect(key.key).to.equal('x'); + expect(key.shiftKey).to.be.false; + expect(key.ctrlKey).to.be.true; + expect(key.altKey).to.be.false; + expect(key.metaKey).to.be.false; + }); + + it('returns for Ctrl+Meta+X', () => { + let key = keys.fromMapKey(''); + expect(key.key).to.equal('x'); + expect(key.shiftKey).to.be.false; + expect(key.ctrlKey).to.be.true; + expect(key.altKey).to.be.false; + expect(key.metaKey).to.be.true; + }); + + it('returns for Ctrl+Shift+x', () => { + let key = keys.fromMapKey(''); + expect(key.key).to.equal('X'); + expect(key.shiftKey).to.be.true; + expect(key.ctrlKey).to.be.true; + expect(key.altKey).to.be.false; + expect(key.metaKey).to.be.false; + }); + + it('returns for Shift+Esc', () => { + let key = keys.fromMapKey(''); + expect(key.key).to.equal('Esc'); + expect(key.shiftKey).to.be.true; + expect(key.ctrlKey).to.be.false; + expect(key.altKey).to.be.false; + expect(key.metaKey).to.be.false; + }); + + it('returns for Ctrl+Esc', () => { + let key = keys.fromMapKey(''); + expect(key.key).to.equal('Esc'); + expect(key.shiftKey).to.be.false; + expect(key.ctrlKey).to.be.true; + expect(key.altKey).to.be.false; + expect(key.metaKey).to.be.false; + }); + + it('returns for Ctrl+Esc', () => { + let key = keys.fromMapKey(''); + expect(key.key).to.equal('Space'); + expect(key.shiftKey).to.be.false; + expect(key.ctrlKey).to.be.true; + expect(key.altKey).to.be.false; + expect(key.metaKey).to.be.false; + }); + }); + + describe('equals', () => { + expect(keys.equals( + { key: 'x', ctrlKey: true, }, + { key: 'x', ctrlKey: true, }, + )).to.be.true; + + expect(keys.equals( + { key: 'X', shiftKey: true, }, + { key: 'x', ctrlKey: true, }, + )).to.be.false; + }); +}); diff --git a/test/content/domains/KeySequence.test.ts b/test/content/domains/KeySequence.test.ts new file mode 100644 index 0000000..7387c06 --- /dev/null +++ b/test/content/domains/KeySequence.test.ts @@ -0,0 +1,72 @@ +import KeySequence, * as utils from '../../../src/content/domains/KeySequence'; +import { expect } from 'chai' + +describe("KeySequence", () => { + describe('#push', () => { + it('append a key to the sequence', () => { + let seq = KeySequence.from([]); + seq.push({ key: 'g' }); + seq.push({ key: 'u', shiftKey: true }); + + let array = seq.getKeyArray(); + expect(array[0]).to.deep.equal({ key: 'g' }); + expect(array[1]).to.deep.equal({ key: 'u', shiftKey: true }); + }) + }); + + describe('#startsWith', () => { + it('returns true if the key sequence starts with param', () => { + let seq = KeySequence.from([ + { key: 'g' }, + { key: 'u', shiftKey: true }, + ]); + + expect(seq.startsWith(KeySequence.from([ + ]))).to.be.true; + expect(seq.startsWith(KeySequence.from([ + { key: 'g' }, + ]))).to.be.true; + expect(seq.startsWith(KeySequence.from([ + { key: 'g' }, { key: 'u', shiftKey: true }, + ]))).to.be.true; + expect(seq.startsWith(KeySequence.from([ + { key: 'g' }, { key: 'u', shiftKey: true }, { key: 'x' }, + ]))).to.be.false; + expect(seq.startsWith(KeySequence.from([ + { key: 'h' }, + ]))).to.be.false; + }) + + it('returns true if the empty sequence starts with an empty sequence', () => { + let seq = KeySequence.from([]); + + expect(seq.startsWith(KeySequence.from([]))).to.be.true; + expect(seq.startsWith(KeySequence.from([ + { key: 'h' }, + ]))).to.be.false; + }) + }); + + describe('#fromMapKeys', () => { + it('returns mapped keys for Shift+Esc', () => { + let keyArray = utils.fromMapKeys('').getKeyArray(); + expect(keyArray).to.have.lengthOf(1); + expect(keyArray[0].key).to.equal('Esc'); + expect(keyArray[0].shiftKey).to.be.true; + }); + + it('returns mapped keys for ad', () => { + let keyArray = utils.fromMapKeys('ad').getKeyArray(); + expect(keyArray).to.have.lengthOf(5); + expect(keyArray[0].key).to.equal('a'); + expect(keyArray[1].ctrlKey).to.be.true; + expect(keyArray[1].key).to.equal('b'); + expect(keyArray[2].altKey).to.be.true; + expect(keyArray[2].key).to.equal('c'); + expect(keyArray[3].key).to.equal('d'); + expect(keyArray[4].metaKey).to.be.true; + expect(keyArray[4].key).to.equal('e'); + }); + }) + +}); diff --git a/test/shared/utils/keys.test.ts b/test/shared/utils/keys.test.ts deleted file mode 100644 index b2ad3cb..0000000 --- a/test/shared/utils/keys.test.ts +++ /dev/null @@ -1,164 +0,0 @@ -import * as keys from 'shared/utils/keys'; - -describe("keys util", () => { - describe('fromKeyboardEvent', () => { - it('returns from keyboard input Ctrl+X', () => { - let k = keys.fromKeyboardEvent({ - key: 'x', shiftKey: false, ctrlKey: true, altKey: false, metaKey: true - }); - expect(k.key).to.equal('x'); - expect(k.shiftKey).to.be.false; - expect(k.ctrlKey).to.be.true; - expect(k.altKey).to.be.false; - expect(k.metaKey).to.be.true; - }); - - it('returns from keyboard input Shift+Esc', () => { - let k = keys.fromKeyboardEvent({ - key: 'Escape', shiftKey: true, ctrlKey: false, altKey: false, metaKey: true - }); - expect(k.key).to.equal('Esc'); - expect(k.shiftKey).to.be.true; - expect(k.ctrlKey).to.be.false; - expect(k.altKey).to.be.false; - expect(k.metaKey).to.be.true; - }); - - it('returns from keyboard input Ctrl+$', () => { - // $ required shift pressing on most keyboards - let k = keys.fromKeyboardEvent({ - key: '$', shiftKey: true, ctrlKey: true, altKey: false, metaKey: false - }); - expect(k.key).to.equal('$'); - expect(k.shiftKey).to.be.false; - expect(k.ctrlKey).to.be.true; - expect(k.altKey).to.be.false; - expect(k.metaKey).to.be.false; - }); - - it('returns from keyboard input Crtl+Space', () => { - let k = keys.fromKeyboardEvent({ - key: ' ', shiftKey: false, ctrlKey: true, altKey: false, metaKey: false - }); - expect(k.key).to.equal('Space'); - expect(k.shiftKey).to.be.false; - expect(k.ctrlKey).to.be.true; - expect(k.altKey).to.be.false; - expect(k.metaKey).to.be.false; - }); - }); - - describe('fromMapKey', () => { - it('return for X', () => { - let key = keys.fromMapKey('x'); - expect(key.key).to.equal('x'); - expect(key.shiftKey).to.be.false; - expect(key.ctrlKey).to.be.false; - expect(key.altKey).to.be.false; - expect(key.metaKey).to.be.false; - }); - - it('return for Shift+X', () => { - let key = keys.fromMapKey('X'); - expect(key.key).to.equal('X'); - expect(key.shiftKey).to.be.true; - expect(key.ctrlKey).to.be.false; - expect(key.altKey).to.be.false; - expect(key.metaKey).to.be.false; - }); - - it('return for Ctrl+X', () => { - let key = keys.fromMapKey(''); - expect(key.key).to.equal('x'); - expect(key.shiftKey).to.be.false; - expect(key.ctrlKey).to.be.true; - expect(key.altKey).to.be.false; - expect(key.metaKey).to.be.false; - }); - - it('returns for Ctrl+Meta+X', () => { - let key = keys.fromMapKey(''); - expect(key.key).to.equal('x'); - expect(key.shiftKey).to.be.false; - expect(key.ctrlKey).to.be.true; - expect(key.altKey).to.be.false; - expect(key.metaKey).to.be.true; - }); - - it('returns for Ctrl+Shift+x', () => { - let key = keys.fromMapKey(''); - expect(key.key).to.equal('X'); - expect(key.shiftKey).to.be.true; - expect(key.ctrlKey).to.be.true; - expect(key.altKey).to.be.false; - expect(key.metaKey).to.be.false; - }); - - it('returns for Shift+Esc', () => { - let key = keys.fromMapKey(''); - expect(key.key).to.equal('Esc'); - expect(key.shiftKey).to.be.true; - expect(key.ctrlKey).to.be.false; - expect(key.altKey).to.be.false; - expect(key.metaKey).to.be.false; - }); - - it('returns for Ctrl+Esc', () => { - let key = keys.fromMapKey(''); - expect(key.key).to.equal('Esc'); - expect(key.shiftKey).to.be.false; - expect(key.ctrlKey).to.be.true; - expect(key.altKey).to.be.false; - expect(key.metaKey).to.be.false; - }); - - it('returns for Ctrl+Esc', () => { - let key = keys.fromMapKey(''); - expect(key.key).to.equal('Space'); - expect(key.shiftKey).to.be.false; - expect(key.ctrlKey).to.be.true; - expect(key.altKey).to.be.false; - expect(key.metaKey).to.be.false; - }); - }); - - describe('fromMapKeys', () => { - it('returns mapped keys for Shift+Esc', () => { - let keyArray = keys.fromMapKeys(''); - expect(keyArray).to.have.lengthOf(1); - expect(keyArray[0].key).to.equal('Esc'); - expect(keyArray[0].shiftKey).to.be.true; - }); - - it('returns mapped keys for ad', () => { - let keyArray = keys.fromMapKeys('ad'); - expect(keyArray).to.have.lengthOf(5); - expect(keyArray[0].key).to.equal('a'); - expect(keyArray[1].ctrlKey).to.be.true; - expect(keyArray[1].key).to.equal('b'); - expect(keyArray[2].altKey).to.be.true; - expect(keyArray[2].key).to.equal('c'); - expect(keyArray[3].key).to.equal('d'); - expect(keyArray[4].metaKey).to.be.true; - expect(keyArray[4].key).to.equal('e'); - }); - }) - - describe('equals', () => { - expect(keys.equals({ - key: 'x', - ctrlKey: true, - }, { - key: 'x', - ctrlKey: true, - })).to.be.true; - - expect(keys.equals({ - key: 'X', - shiftKey: true, - }, { - key: 'x', - ctrlKey: true, - })).to.be.false; - }); -}); -- cgit v1.2.3 From e0c4182f14f908d13c8c814c7bc2b48a1791f881 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 19 May 2019 09:26:52 +0900 Subject: Follow as a clean architecture --- src/content/controllers/FollowKeyController.ts | 21 +++ src/content/controllers/FollowMasterController.ts | 31 +++++ src/content/controllers/FollowSlaveController.ts | 32 +++++ src/content/controllers/KeymapController.ts | 16 ++- src/content/index.ts | 35 ++++- src/content/repositories/FollowKeyRepository.ts | 35 +++++ src/content/repositories/FollowMasterRepository.ts | 59 ++++++++ src/content/repositories/FollowSlaveRepository.ts | 31 +++++ src/content/usecases/FollowMasterUseCase.ts | 150 +++++++++++++++++++++ src/content/usecases/FollowSlaveUseCase.ts | 91 +++++++++++++ test/content/InputDriver.test.ts | 2 +- 11 files changed, 493 insertions(+), 10 deletions(-) create mode 100644 src/content/controllers/FollowKeyController.ts create mode 100644 src/content/controllers/FollowMasterController.ts create mode 100644 src/content/controllers/FollowSlaveController.ts create mode 100644 src/content/repositories/FollowKeyRepository.ts create mode 100644 src/content/repositories/FollowMasterRepository.ts create mode 100644 src/content/repositories/FollowSlaveRepository.ts create mode 100644 src/content/usecases/FollowMasterUseCase.ts create mode 100644 src/content/usecases/FollowSlaveUseCase.ts (limited to 'src/content/controllers') diff --git a/src/content/controllers/FollowKeyController.ts b/src/content/controllers/FollowKeyController.ts new file mode 100644 index 0000000..eb45e01 --- /dev/null +++ b/src/content/controllers/FollowKeyController.ts @@ -0,0 +1,21 @@ +import FollowSlaveUseCase from '../usecases/FollowSlaveUseCase'; +import Key from '../domains/Key'; + +export default class FollowKeyController { + private followSlaveUseCase: FollowSlaveUseCase; + + constructor({ + followSlaveUseCase = new FollowSlaveUseCase(), + } = {}) { + this.followSlaveUseCase = followSlaveUseCase; + } + + press(key: Key): boolean { + if (!this.followSlaveUseCase.isFollowMode()) { + return false; + } + + this.followSlaveUseCase.sendKey(key); + return true; + } +} diff --git a/src/content/controllers/FollowMasterController.ts b/src/content/controllers/FollowMasterController.ts new file mode 100644 index 0000000..89294ff --- /dev/null +++ b/src/content/controllers/FollowMasterController.ts @@ -0,0 +1,31 @@ +import FollowMasterUseCase from '../usecases/FollowMasterUseCase'; +import * as messages from '../../shared/messages'; + +export default class FollowMasterController { + private followMasterUseCase: FollowMasterUseCase; + + constructor({ + followMasterUseCase = new FollowMasterUseCase(), + } = {}) { + this.followMasterUseCase = followMasterUseCase; + } + + followStart(m: messages.FollowStartMessage): void { + this.followMasterUseCase.startFollow(m.newTab, m.background); + } + + responseCountTargets( + m: messages.FollowResponseCountTargetsMessage, sender: Window, + ): void { + this.followMasterUseCase.createSlaveHints(m.count, sender); + } + + keyPress(message: messages.FollowKeyPressMessage): void { + if (message.key === '[' && message.ctrlKey) { + this.followMasterUseCase.cancelFollow(); + } else { + this.followMasterUseCase.enqueue(message.key); + } + } +} + diff --git a/src/content/controllers/FollowSlaveController.ts b/src/content/controllers/FollowSlaveController.ts new file mode 100644 index 0000000..88dccf3 --- /dev/null +++ b/src/content/controllers/FollowSlaveController.ts @@ -0,0 +1,32 @@ +import * as messages from '../../shared/messages'; +import FollowSlaveUseCase from '../usecases/FollowSlaveUseCase'; + +export default class FollowSlaveController { + private usecase: FollowSlaveUseCase; + + constructor({ + usecase = new FollowSlaveUseCase(), + } = {}) { + this.usecase = usecase; + } + + countTargets(m: messages.FollowRequestCountTargetsMessage): void { + this.usecase.countTargets(m.viewSize, m.framePosition); + } + + createHints(m: messages.FollowCreateHintsMessage): void { + this.usecase.createHints(m.viewSize, m.framePosition, m.tags); + } + + showHints(m: messages.FollowShowHintsMessage): void { + this.usecase.showHints(m.prefix); + } + + activate(m: messages.FollowActivateMessage): void { + this.usecase.activate(m.tag, m.newTab, m.background); + } + + clear(_m: messages.FollowRemoveHintsMessage) { + this.usecase.clear(); + } +} diff --git a/src/content/controllers/KeymapController.ts b/src/content/controllers/KeymapController.ts index 424292c..20c24c0 100644 --- a/src/content/controllers/KeymapController.ts +++ b/src/content/controllers/KeymapController.ts @@ -8,6 +8,8 @@ import FocusUseCase from '../usecases/FocusUseCase'; import ClipboardUseCase from '../usecases/ClipboardUseCase'; import BackgroundClient from '../client/BackgroundClient'; import MarkKeyyUseCase from '../usecases/MarkKeyUseCase'; +import FollowMasterClient, { FollowMasterClientImpl } + from '../client/FollowMasterClient'; import Key from '../domains/Key'; export default class KeymapController { @@ -29,6 +31,8 @@ export default class KeymapController { private markKeyUseCase: MarkKeyyUseCase; + private followMasterClient: FollowMasterClient; + constructor({ keymapUseCase = new KeymapUseCase(), addonEnabledUseCase = new AddonEnabledUseCase(), @@ -39,6 +43,7 @@ export default class KeymapController { clipbaordUseCase = new ClipboardUseCase(), backgroundClient = new BackgroundClient(), markKeyUseCase = new MarkKeyyUseCase(), + followMasterClient = new FollowMasterClientImpl(window.top), } = {}) { this.keymapUseCase = keymapUseCase; this.addonEnabledUseCase = addonEnabledUseCase; @@ -49,6 +54,7 @@ export default class KeymapController { this.clipbaordUseCase = clipbaordUseCase; this.backgroundClient = backgroundClient; this.markKeyUseCase = markKeyUseCase; + this.followMasterClient = followMasterClient; } // eslint-disable-next-line complexity, max-lines-per-function @@ -96,13 +102,9 @@ export default class KeymapController { 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.FOLLOW_START: + this.followMasterClient.startFollow(op.newTab, op.background); + break; case operations.MARK_SET_PREFIX: this.markKeyUseCase.enableSetMode(); break; diff --git a/src/content/index.ts b/src/content/index.ts index d644095..08bdf6b 100644 --- a/src/content/index.ts +++ b/src/content/index.ts @@ -6,6 +6,9 @@ import consoleFrameStyle from './site-style'; import MessageListener from './MessageListener'; import FindController from './controllers/FindController'; import MarkController from './controllers/MarkController'; +import FollowMasterController from './controllers/FollowMasterController'; +import FollowSlaveController from './controllers/FollowSlaveController'; +import FollowKeyController from './controllers/FollowKeyController'; import * as messages from '../shared/messages'; import InputDriver from './InputDriver'; import KeymapController from './controllers/KeymapController'; @@ -17,11 +20,14 @@ import AddonEnabledController from './controllers/AddonEnabledController'; // const store = newStore(); +let listener = new MessageListener(); if (window.self === window.top) { // new TopContentComponent(window, store); // eslint-disable-line no-new let findController = new FindController(); - new MessageListener().onWebMessage((message: messages.Message) => { + + let followMasterController = new FollowMasterController(); + listener.onWebMessage((message: messages.Message, sender: Window) => { switch (message.type) { case messages.CONSOLE_ENTER_FIND: return findController.start(message); @@ -32,6 +38,13 @@ if (window.self === window.top) { case messages.CONSOLE_UNFOCUS: window.focus(); consoleFrames.blur(window.document); + break; + case messages.FOLLOW_START: + return followMasterController.followStart(message); + case messages.FOLLOW_RESPONSE_COUNT_TARGETS: + return followMasterController.responseCountTargets(message, sender); + case messages.FOLLOW_KEY_PRESS: + return followMasterController.keyPress(message); } return undefined; }); @@ -54,10 +67,28 @@ if (window.self === window.top) { // new FrameContentComponent(window, store); // eslint-disable-line no-new } +let followSlaveController = new FollowSlaveController(); +listener.onWebMessage((message: messages.Message) => { + switch (message.type) { + case messages.FOLLOW_REQUEST_COUNT_TARGETS: + return followSlaveController.countTargets(message); + case messages.FOLLOW_CREATE_HINTS: + return followSlaveController.createHints(message); + case messages.FOLLOW_SHOW_HINTS: + return followSlaveController.showHints(message); + case messages.FOLLOW_ACTIVATE: + return followSlaveController.activate(message); + case messages.FOLLOW_REMOVE_HINTS: + return followSlaveController.clear(message); + } + return undefined; +}); + let keymapController = new KeymapController(); let markKeyController = new MarkKeyController(); +let followKeyController = new FollowKeyController(); let inputDriver = new InputDriver(document.body); -// inputDriver.onKey(key => followSlaveController.pressKey(key)); +inputDriver.onKey(key => followKeyController.press(key)); inputDriver.onKey(key => markKeyController.press(key)); inputDriver.onKey(key => keymapController.press(key)); diff --git a/src/content/repositories/FollowKeyRepository.ts b/src/content/repositories/FollowKeyRepository.ts new file mode 100644 index 0000000..a671b5c --- /dev/null +++ b/src/content/repositories/FollowKeyRepository.ts @@ -0,0 +1,35 @@ +export default interface FollowKeyRepository { + getKeys(): string[]; + + pushKey(key: string): void; + + popKey(): void; + + clearKeys(): void; + + // eslint-disable-next-line semi +} + +const current: { + keys: string[]; +} = { + keys: [], +}; + +export class FollowKeyRepositoryImpl implements FollowKeyRepository { + getKeys(): string[] { + return current.keys; + } + + pushKey(key: string): void { + current.keys.push(key); + } + + popKey(): void { + current.keys.pop(); + } + + clearKeys(): void { + current.keys = []; + } +} diff --git a/src/content/repositories/FollowMasterRepository.ts b/src/content/repositories/FollowMasterRepository.ts new file mode 100644 index 0000000..a964953 --- /dev/null +++ b/src/content/repositories/FollowMasterRepository.ts @@ -0,0 +1,59 @@ +export default interface FollowMasterRepository { + setCurrentFollowMode(newTab: boolean, background: boolean): void; + + getTags(): string[]; + + getTagsByPrefix(prefix: string): string[]; + + addTag(tag: string): void; + + clearTags(): void; + + getCurrentNewTabMode(): boolean; + + getCurrentBackgroundMode(): boolean; + + // eslint-disable-next-line semi +} + +const current: { + newTab: boolean; + background: boolean; + tags: string[]; +} = { + newTab: false, + background: false, + tags: [], +}; + +export class FollowMasterRepositoryImpl implements FollowMasterRepository { + setCurrentFollowMode(newTab: boolean, background: boolean): void { + current.newTab = newTab; + current.background = background; + } + + getTags(): string[] { + return current.tags; + } + + getTagsByPrefix(prefix: string): string[] { + return current.tags.filter(t => t.startsWith(prefix)); + } + + addTag(tag: string): void { + current.tags.push(tag); + } + + clearTags(): void { + current.tags = []; + } + + getCurrentNewTabMode(): boolean { + return current.newTab; + } + + getCurrentBackgroundMode(): boolean { + return current.background; + } +} + diff --git a/src/content/repositories/FollowSlaveRepository.ts b/src/content/repositories/FollowSlaveRepository.ts new file mode 100644 index 0000000..4c2de72 --- /dev/null +++ b/src/content/repositories/FollowSlaveRepository.ts @@ -0,0 +1,31 @@ +export default interface FollowSlaveRepository { + enableFollowMode(): void; + + disableFollowMode(): void; + + isFollowMode(): boolean; + + // eslint-disable-next-line semi +} + +const current: { + enabled: boolean; +} = { + enabled: false, +}; + +export class FollowSlaveRepositoryImpl implements FollowSlaveRepository { + enableFollowMode(): void { + current.enabled = true; + } + + disableFollowMode(): void { + current.enabled = false; + } + + isFollowMode(): boolean { + return current.enabled; + } +} + + diff --git a/src/content/usecases/FollowMasterUseCase.ts b/src/content/usecases/FollowMasterUseCase.ts new file mode 100644 index 0000000..c2c6835 --- /dev/null +++ b/src/content/usecases/FollowMasterUseCase.ts @@ -0,0 +1,150 @@ +import FollowKeyRepository, { FollowKeyRepositoryImpl } + from '../repositories/FollowKeyRepository'; +import FollowMasterRepository, { FollowMasterRepositoryImpl } + from '../repositories/FollowMasterRepository'; +import FollowSlaveClient, { FollowSlaveClientImpl } + from '../client/FollowSlaveClient'; +import HintKeyProducer from '../hint-key-producer'; +import SettingRepository, { SettingRepositoryImpl } + from '../repositories/SettingRepository'; + +export default class FollowMasterUseCase { + private followKeyRepository: FollowKeyRepository; + + private followMasterRepository: FollowMasterRepository; + + private settingRepository: SettingRepository; + + // TODO Make repository + private producer: HintKeyProducer | null; + + constructor({ + followKeyRepository = new FollowKeyRepositoryImpl(), + followMasterRepository = new FollowMasterRepositoryImpl(), + settingRepository = new SettingRepositoryImpl(), + } = {}) { + this.followKeyRepository = followKeyRepository; + this.followMasterRepository = followMasterRepository; + this.settingRepository = settingRepository; + this.producer = null; + } + + startFollow(newTab: boolean, background: boolean): void { + let hintchars = this.settingRepository.get().properties.hintchars; + this.producer = new HintKeyProducer(hintchars); + + this.followKeyRepository.clearKeys(); + this.followMasterRepository.setCurrentFollowMode(newTab, background); + + let viewWidth = window.top.innerWidth; + let viewHeight = window.top.innerHeight; + new FollowSlaveClientImpl(window.top).requestHintCount( + { width: viewWidth, height: viewHeight }, + { x: 0, y: 0 }, + ); + + let frameElements = window.document.querySelectorAll('iframe'); + for (let i = 0; i < frameElements.length; ++i) { + let ele = frameElements[i] as HTMLFrameElement | HTMLIFrameElement; + let { left: frameX, top: frameY } = ele.getBoundingClientRect(); + new FollowSlaveClientImpl(ele.contentWindow!!).requestHintCount( + { width: viewWidth, height: viewHeight }, + { x: frameX, y: frameY }, + ); + } + } + + // eslint-disable-next-line max-statements + createSlaveHints(count: number, sender: Window): void { + let produced = []; + for (let i = 0; i < count; ++i) { + let tag = this.producer!!.produce(); + produced.push(tag); + this.followMasterRepository.addTag(tag); + } + + let doc = window.document; + let viewWidth = window.innerWidth || doc.documentElement.clientWidth; + let viewHeight = window.innerHeight || doc.documentElement.clientHeight; + let pos = { x: 0, y: 0 }; + if (sender !== window) { + let frameElements = window.document.querySelectorAll('iframe'); + let ele = Array.from(frameElements).find(e => e.contentWindow === sender); + if (!ele) { + // elements of the sender is gone + return; + } + let { left: frameX, top: frameY } = ele.getBoundingClientRect(); + pos = { x: frameX, y: frameY }; + } + new FollowSlaveClientImpl(sender).createHints( + { width: viewWidth, height: viewHeight }, + pos, + produced, + ); + } + + cancelFollow(): void { + this.followMasterRepository.clearTags(); + this.broadcastToSlaves((client) => { + client.clearHints(); + }); + } + + filter(prefix: string): void { + this.broadcastToSlaves((client) => { + client.filterHints(prefix); + }); + } + + activate(tag: string): void { + this.followMasterRepository.clearTags(); + + let newTab = this.followMasterRepository.getCurrentNewTabMode(); + let background = this.followMasterRepository.getCurrentBackgroundMode(); + this.broadcastToSlaves((client) => { + client.activateIfExists(tag, newTab, background); + client.clearHints(); + }); + } + + enqueue(key: string): void { + switch (key) { + case 'Enter': + this.activate(this.getCurrentTag()); + return; + case 'Esc': + this.cancelFollow(); + return; + case 'Backspace': + case 'Delete': + this.followKeyRepository.popKey(); + this.filter(this.getCurrentTag()); + return; + } + + this.followKeyRepository.pushKey(key); + + let tag = this.getCurrentTag(); + let matched = this.followMasterRepository.getTagsByPrefix(tag); + if (matched.length === 0) { + this.cancelFollow(); + } else if (matched.length === 1) { + this.activate(tag); + } else { + this.filter(tag); + } + } + + private broadcastToSlaves(handler: (client: FollowSlaveClient) => void) { + let allFrames = [window.self].concat(Array.from(window.frames as any)); + let clients = allFrames.map(frame => new FollowSlaveClientImpl(frame)); + for (let client of clients) { + handler(client); + } + } + + private getCurrentTag(): string { + return this.followKeyRepository.getKeys().join(''); + } +} diff --git a/src/content/usecases/FollowSlaveUseCase.ts b/src/content/usecases/FollowSlaveUseCase.ts new file mode 100644 index 0000000..eb011de --- /dev/null +++ b/src/content/usecases/FollowSlaveUseCase.ts @@ -0,0 +1,91 @@ +import FollowSlaveRepository, { FollowSlaveRepositoryImpl } + from '../repositories/FollowSlaveRepository'; +import FollowPresenter, { FollowPresenterImpl } + from '../presenters/FollowPresenter'; +import TabsClient, { TabsClientImpl } from '../client/TabsClient'; +import { LinkHint, InputHint } from '../presenters/Hint'; +import FollowMasterClient, { FollowMasterClientImpl } + from '../client/FollowMasterClient'; +import Key from '../domains/Key'; + +interface Size { + width: number; + height: number; +} + +interface Point { + x: number; + y: number; +} + +export default class FollowSlaveUseCase { + private presenter: FollowPresenter; + + private tabsClient: TabsClient; + + private followMasterClient: FollowMasterClient; + + private followSlaveRepository: FollowSlaveRepository; + + constructor({ + presenter = new FollowPresenterImpl(), + tabsClient = new TabsClientImpl(), + followMasterClient = new FollowMasterClientImpl(window.top), + followSlaveRepository = new FollowSlaveRepositoryImpl(), + } = {}) { + this.presenter = presenter; + this.tabsClient = tabsClient; + this.followMasterClient = followMasterClient; + this.followSlaveRepository = followSlaveRepository; + } + + countTargets(viewSize: Size, framePosition: Point): void { + let count = this.presenter.getTargetCount(viewSize, framePosition); + this.followMasterClient.responseHintCount(count); + } + + createHints(viewSize: Size, framePosition: Point, tags: string[]): void { + this.followSlaveRepository.enableFollowMode(); + this.presenter.createHints(viewSize, framePosition, tags); + } + + showHints(prefix: string) { + this.presenter.filterHints(prefix); + } + + sendKey(key: Key): void { + this.followMasterClient.sendKey(key); + } + + isFollowMode(): boolean { + return this.followSlaveRepository.isFollowMode(); + } + + async activate(tag: string, newTab: boolean, background: boolean) { + let hint = this.presenter.getHint(tag); + if (!hint) { + return; + } + + if (hint instanceof LinkHint) { + let url = hint.getLink(); + // ignore taget='_blank' + if (!newTab && hint.getLinkTarget() === '_blank') { + hint.click(); + return; + } + // eslint-disable-next-line no-script-url + if (!url || url === '#' || url.toLowerCase().startsWith('javascript:')) { + return; + } + await this.tabsClient.openUrl(url, newTab, background); + } else if (hint instanceof InputHint) { + hint.activate(); + } + } + + clear(): void { + this.followSlaveRepository.disableFollowMode(); + this.presenter.clearHints(); + } +} diff --git a/test/content/InputDriver.test.ts b/test/content/InputDriver.test.ts index ac5f95d..b9f2c28 100644 --- a/test/content/InputDriver.test.ts +++ b/test/content/InputDriver.test.ts @@ -1,6 +1,6 @@ import InputDriver from '../../src/content/InputDriver'; import { expect } from 'chai'; -import { Key } from '../../src/shared/utils/keys'; +import Key from '../../src/content/domains/Key'; describe('InputDriver', () => { let target: HTMLElement; -- cgit v1.2.3 From c81b82ee39cc05d06597e7cd024de448894efe43 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 19 May 2019 10:50:49 +0900 Subject: Make routes --- src/content/console-frames.ts | 16 --- src/content/controllers/ConsoleFrameController.ts | 16 +++ src/content/controllers/SettingController.ts | 41 ++++++++ src/content/index.ts | 116 +--------------------- src/content/presenters/ConsoleFramePresenter.ts | 25 +++++ src/content/routes.ts | 97 ++++++++++++++++++ src/content/usecases/ConsoleFrameUseCase.ts | 17 ++++ 7 files changed, 201 insertions(+), 127 deletions(-) delete mode 100644 src/content/console-frames.ts create mode 100644 src/content/controllers/ConsoleFrameController.ts create mode 100644 src/content/controllers/SettingController.ts create mode 100644 src/content/presenters/ConsoleFramePresenter.ts create mode 100644 src/content/routes.ts create mode 100644 src/content/usecases/ConsoleFrameUseCase.ts (limited to 'src/content/controllers') diff --git a/src/content/console-frames.ts b/src/content/console-frames.ts deleted file mode 100644 index b1b9bf6..0000000 --- a/src/content/console-frames.ts +++ /dev/null @@ -1,16 +0,0 @@ -const initialize = (doc: Document): HTMLIFrameElement => { - let iframe = doc.createElement('iframe'); - iframe.src = browser.runtime.getURL('build/console.html'); - iframe.id = 'vimvixen-console-frame'; - iframe.className = 'vimvixen-console-frame'; - doc.body.append(iframe); - - return iframe; -}; - -const blur = (doc: Document) => { - let ele = doc.getElementById('vimvixen-console-frame') as HTMLIFrameElement; - ele.blur(); -}; - -export { initialize, blur }; diff --git a/src/content/controllers/ConsoleFrameController.ts b/src/content/controllers/ConsoleFrameController.ts new file mode 100644 index 0000000..fafadf4 --- /dev/null +++ b/src/content/controllers/ConsoleFrameController.ts @@ -0,0 +1,16 @@ +import ConsoleFrameUseCase from '../usecases/ConsoleFrameUseCase'; +import * as messages from '../../shared/messages'; + +export default class ConsoleFrameController { + private consoleFrameUseCase: ConsoleFrameUseCase; + + constructor({ + consoleFrameUseCase = new ConsoleFrameUseCase(), + } = {}) { + this.consoleFrameUseCase = consoleFrameUseCase; + } + + unfocus(_message: messages.Message) { + this.consoleFrameUseCase.unfocus(); + } +} diff --git a/src/content/controllers/SettingController.ts b/src/content/controllers/SettingController.ts new file mode 100644 index 0000000..f0e770b --- /dev/null +++ b/src/content/controllers/SettingController.ts @@ -0,0 +1,41 @@ +import AddonEnabledUseCase from '../usecases/AddonEnabledUseCase'; +import SettingUseCase from '../usecases/SettingUseCase'; +import * as blacklists from '../../shared/blacklists'; + +import * as messages from '../../shared/messages'; + +export default class SettingController { + private addonEnabledUseCase: AddonEnabledUseCase; + + private settingUseCase: SettingUseCase; + + constructor({ + addonEnabledUseCase = new AddonEnabledUseCase(), + settingUseCase = new SettingUseCase(), + } = {}) { + this.addonEnabledUseCase = addonEnabledUseCase; + this.settingUseCase = settingUseCase; + } + + async initSettings(): Promise { + try { + let current = await this.settingUseCase.reload(); + let disabled = blacklists.includes( + current.blacklist, window.location.href, + ); + if (disabled) { + this.addonEnabledUseCase.disable(); + } else { + this.addonEnabledUseCase.enable(); + } + } catch (e) { + // Sometime sendMessage fails when background script is not ready. + console.warn(e); + setTimeout(() => this.initSettings(), 500); + } + } + + async reloadSettings(_message: messages.Message): Promise { + await this.settingUseCase.reload(); + } +} diff --git a/src/content/index.ts b/src/content/index.ts index 06bb34f..660ebf5 100644 --- a/src/content/index.ts +++ b/src/content/index.ts @@ -1,122 +1,16 @@ -import * as consoleFrames from './console-frames'; +import { ConsoleFramePresenterImpl } from './presenters/ConsoleFramePresenter'; import consoleFrameStyle from './site-style'; -import MessageListener from './MessageListener'; -import FindController from './controllers/FindController'; -import MarkController from './controllers/MarkController'; -import FollowMasterController from './controllers/FollowMasterController'; -import FollowSlaveController from './controllers/FollowSlaveController'; -import FollowKeyController from './controllers/FollowKeyController'; -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'; -import MarkKeyController from './controllers/MarkKeyController'; -import AddonEnabledController from './controllers/AddonEnabledController'; +import * as routes from './routes'; -let listener = new MessageListener(); if (window.self === window.top) { - let findController = new FindController(); + routes.routeMasterComponents(); - let followMasterController = new FollowMasterController(); - listener.onWebMessage((message: messages.Message, sender: Window) => { - switch (message.type) { - case messages.CONSOLE_ENTER_FIND: - return findController.start(message); - case messages.FIND_NEXT: - return findController.next(message); - case messages.FIND_PREV: - return findController.prev(message); - case messages.CONSOLE_UNFOCUS: - window.focus(); - consoleFrames.blur(window.document); - break; - case messages.FOLLOW_START: - return followMasterController.followStart(message); - case messages.FOLLOW_RESPONSE_COUNT_TARGETS: - return followMasterController.responseCountTargets(message, sender); - case messages.FOLLOW_KEY_PRESS: - return followMasterController.keyPress(message); - } - return undefined; - }); - - let markController = new MarkController(); - let addonEnabledController = new AddonEnabledController(); - - new MessageListener().onBackgroundMessage((message: messages.Message) => { - switch (message.type) { - case messages.ADDON_ENABLED_QUERY: - return addonEnabledController.getAddonEnabled(message); - case messages.TAB_SCROLL_TO: - return markController.scrollTo(message); - } - return undefined; - }); - - consoleFrames.initialize(window.document); + new ConsoleFramePresenterImpl().initialize(); } -let followSlaveController = new FollowSlaveController(); -listener.onWebMessage((message: messages.Message) => { - switch (message.type) { - case messages.FOLLOW_REQUEST_COUNT_TARGETS: - return followSlaveController.countTargets(message); - case messages.FOLLOW_CREATE_HINTS: - return followSlaveController.createHints(message); - case messages.FOLLOW_SHOW_HINTS: - return followSlaveController.showHints(message); - case messages.FOLLOW_ACTIVATE: - return followSlaveController.activate(message); - case messages.FOLLOW_REMOVE_HINTS: - return followSlaveController.clear(message); - } - return undefined; -}); +routes.routeComponents(); -let keymapController = new KeymapController(); -let markKeyController = new MarkKeyController(); -let followKeyController = new FollowKeyController(); -let inputDriver = new InputDriver(document.body); -inputDriver.onKey(key => followKeyController.press(key)); -inputDriver.onKey(key => markKeyController.press(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(); - -new MessageListener().onBackgroundMessage((message: messages.Message): any => { - let addonEnabledUseCase = new AddonEnabledUseCase(); - - switch (message.type) { - case messages.SETTINGS_CHANGED: - return reloadSettings(); - case messages.ADDON_TOGGLE_ENABLED: - return addonEnabledUseCase.toggle(); - } -}); diff --git a/src/content/presenters/ConsoleFramePresenter.ts b/src/content/presenters/ConsoleFramePresenter.ts new file mode 100644 index 0000000..3c7477b --- /dev/null +++ b/src/content/presenters/ConsoleFramePresenter.ts @@ -0,0 +1,25 @@ +export default interface ConsoleFramePresenter { + initialize(): void; + + blur(): void; + + // eslint-disable-next-line semi +} + +export class ConsoleFramePresenterImpl implements ConsoleFramePresenter { + initialize(): void { + let iframe = document.createElement('iframe'); + iframe.src = browser.runtime.getURL('build/console.html'); + iframe.id = 'vimvixen-console-frame'; + iframe.className = 'vimvixen-console-frame'; + document.body.append(iframe); + } + + blur(): void { + let ele = document.getElementById('vimvixen-console-frame'); + if (!ele) { + throw new Error('console frame not created'); + } + ele.blur(); + } +} diff --git a/src/content/routes.ts b/src/content/routes.ts new file mode 100644 index 0000000..0bce4f5 --- /dev/null +++ b/src/content/routes.ts @@ -0,0 +1,97 @@ +import MessageListener from './MessageListener'; +import FindController from './controllers/FindController'; +import MarkController from './controllers/MarkController'; +import FollowMasterController from './controllers/FollowMasterController'; +import FollowSlaveController from './controllers/FollowSlaveController'; +import FollowKeyController from './controllers/FollowKeyController'; +import InputDriver from './InputDriver'; +import KeymapController from './controllers/KeymapController'; +import AddonEnabledUseCase from './usecases/AddonEnabledUseCase'; +import MarkKeyController from './controllers/MarkKeyController'; +import AddonEnabledController from './controllers/AddonEnabledController'; +import SettingController from './controllers/SettingController'; +import ConsoleFrameController from './controllers/ConsoleFrameController'; +import * as messages from '../shared/messages'; + +export const routeComponents = () => { + let listener = new MessageListener(); + + let followSlaveController = new FollowSlaveController(); + listener.onWebMessage((message: messages.Message) => { + switch (message.type) { + case messages.FOLLOW_REQUEST_COUNT_TARGETS: + return followSlaveController.countTargets(message); + case messages.FOLLOW_CREATE_HINTS: + return followSlaveController.createHints(message); + case messages.FOLLOW_SHOW_HINTS: + return followSlaveController.showHints(message); + case messages.FOLLOW_ACTIVATE: + return followSlaveController.activate(message); + case messages.FOLLOW_REMOVE_HINTS: + return followSlaveController.clear(message); + } + return undefined; + }); + + let keymapController = new KeymapController(); + let markKeyController = new MarkKeyController(); + let followKeyController = new FollowKeyController(); + let inputDriver = new InputDriver(document.body); + inputDriver.onKey(key => followKeyController.press(key)); + inputDriver.onKey(key => markKeyController.press(key)); + inputDriver.onKey(key => keymapController.press(key)); + + let settingController = new SettingController(); + settingController.initSettings(); + + listener.onBackgroundMessage((message: messages.Message): any => { + let addonEnabledUseCase = new AddonEnabledUseCase(); + + switch (message.type) { + case messages.SETTINGS_CHANGED: + return settingController.reloadSettings(message); + case messages.ADDON_TOGGLE_ENABLED: + return addonEnabledUseCase.toggle(); + } + }); +}; + +export const routeMasterComponents = () => { + let listener = new MessageListener(); + + let findController = new FindController(); + let followMasterController = new FollowMasterController(); + let markController = new MarkController(); + let addonEnabledController = new AddonEnabledController(); + let consoleFrameController = new ConsoleFrameController(); + + listener.onWebMessage((message: messages.Message, sender: Window) => { + switch (message.type) { + case messages.CONSOLE_ENTER_FIND: + return findController.start(message); + case messages.FIND_NEXT: + return findController.next(message); + case messages.FIND_PREV: + return findController.prev(message); + case messages.CONSOLE_UNFOCUS: + return consoleFrameController.unfocus(message); + case messages.FOLLOW_START: + return followMasterController.followStart(message); + case messages.FOLLOW_RESPONSE_COUNT_TARGETS: + return followMasterController.responseCountTargets(message, sender); + case messages.FOLLOW_KEY_PRESS: + return followMasterController.keyPress(message); + } + return undefined; + }); + + listener.onBackgroundMessage((message: messages.Message) => { + switch (message.type) { + case messages.ADDON_ENABLED_QUERY: + return addonEnabledController.getAddonEnabled(message); + case messages.TAB_SCROLL_TO: + return markController.scrollTo(message); + } + return undefined; + }); +}; diff --git a/src/content/usecases/ConsoleFrameUseCase.ts b/src/content/usecases/ConsoleFrameUseCase.ts new file mode 100644 index 0000000..b4c756c --- /dev/null +++ b/src/content/usecases/ConsoleFrameUseCase.ts @@ -0,0 +1,17 @@ +import ConsoleFramePresenter, { ConsoleFramePresenterImpl } + from '../presenters/ConsoleFramePresenter'; + +export default class ConsoleFrameUseCase { + private consoleFramePresenter: ConsoleFramePresenter; + + constructor({ + consoleFramePresenter = new ConsoleFramePresenterImpl(), + } = {}) { + this.consoleFramePresenter = consoleFramePresenter; + } + + unfocus() { + window.focus(); + this.consoleFramePresenter.blur(); + } +} -- cgit v1.2.3