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/shared/messages.ts | 66 +++++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) (limited to 'src/shared') diff --git a/src/shared/messages.ts b/src/shared/messages.ts index 41b0f0b..75df798 100644 --- a/src/shared/messages.ts +++ b/src/shared/messages.ts @@ -42,162 +42,162 @@ export const SETTINGS_QUERY = 'settings.query'; export const CONSOLE_FRAME_MESSAGE = 'console.frame.message'; -interface BackgroundOperationMessage { +export interface BackgroundOperationMessage { type: typeof BACKGROUND_OPERATION; operation: operations.Operation; } -interface ConsoleUnfocusMessage { +export interface ConsoleUnfocusMessage { type: typeof CONSOLE_UNFOCUS; } -interface ConsoleEnterCommandMessage { +export interface ConsoleEnterCommandMessage { type: typeof CONSOLE_ENTER_COMMAND; text: string; } -interface ConsoleEnterFindMessage { +export interface ConsoleEnterFindMessage { type: typeof CONSOLE_ENTER_FIND; - text: string; + text?: string; } -interface ConsoleQueryCompletionsMessage { +export interface ConsoleQueryCompletionsMessage { type: typeof CONSOLE_QUERY_COMPLETIONS; text: string; } -interface ConsoleShowCommandMessage { +export interface ConsoleShowCommandMessage { type: typeof CONSOLE_SHOW_COMMAND; command: string; } -interface ConsoleShowErrorMessage { +export interface ConsoleShowErrorMessage { type: typeof CONSOLE_SHOW_ERROR; text: string; } -interface ConsoleShowInfoMessage { +export interface ConsoleShowInfoMessage { type: typeof CONSOLE_SHOW_INFO; text: string; } -interface ConsoleShowFindMessage { +export interface ConsoleShowFindMessage { type: typeof CONSOLE_SHOW_FIND; } -interface ConsoleHideMessage { +export interface ConsoleHideMessage { type: typeof CONSOLE_HIDE; } -interface FollowStartMessage { +export interface FollowStartMessage { type: typeof FOLLOW_START; newTab: boolean; background: boolean; } -interface FollowRequestCountTargetsMessage { +export interface FollowRequestCountTargetsMessage { type: typeof FOLLOW_REQUEST_COUNT_TARGETS; viewSize: { width: number, height: number }; framePosition: { x: number, y: number }; } -interface FollowResponseCountTargetsMessage { +export interface FollowResponseCountTargetsMessage { type: typeof FOLLOW_RESPONSE_COUNT_TARGETS; count: number; } -interface FollowCreateHintsMessage { +export interface FollowCreateHintsMessage { type: typeof FOLLOW_CREATE_HINTS; keysArray: string[]; newTab: boolean; background: boolean; } -interface FollowShowHintsMessage { +export interface FollowShowHintsMessage { type: typeof FOLLOW_SHOW_HINTS; keys: string; } -interface FollowRemoveHintsMessage { +export interface FollowRemoveHintsMessage { type: typeof FOLLOW_REMOVE_HINTS; } -interface FollowActivateMessage { +export interface FollowActivateMessage { type: typeof FOLLOW_ACTIVATE; keys: string; } -interface FollowKeyPressMessage { +export interface FollowKeyPressMessage { type: typeof FOLLOW_KEY_PRESS; key: string; ctrlKey: boolean; } -interface MarkSetGlobalMessage { +export interface MarkSetGlobalMessage { type: typeof MARK_SET_GLOBAL; key: string; x: number; y: number; } -interface MarkJumpGlobalMessage { +export interface MarkJumpGlobalMessage { type: typeof MARK_JUMP_GLOBAL; key: string; } -interface TabScrollToMessage { +export interface TabScrollToMessage { type: typeof TAB_SCROLL_TO; x: number; y: number; } -interface FindNextMessage { +export interface FindNextMessage { type: typeof FIND_NEXT; } -interface FindPrevMessage { +export interface FindPrevMessage { type: typeof FIND_PREV; } -interface FindGetKeywordMessage { +export interface FindGetKeywordMessage { type: typeof FIND_GET_KEYWORD; } -interface FindSetKeywordMessage { +export interface FindSetKeywordMessage { type: typeof FIND_SET_KEYWORD; keyword: string; found: boolean; } -interface AddonEnabledQueryMessage { +export interface AddonEnabledQueryMessage { type: typeof ADDON_ENABLED_QUERY; } -interface AddonEnabledResponseMessage { +export interface AddonEnabledResponseMessage { type: typeof ADDON_ENABLED_RESPONSE; enabled: boolean; } -interface AddonToggleEnabledMessage { +export interface AddonToggleEnabledMessage { type: typeof ADDON_TOGGLE_ENABLED; } -interface OpenUrlMessage { +export interface OpenUrlMessage { type: typeof OPEN_URL; url: string; newTab: boolean; background: boolean; } -interface SettingsChangedMessage { +export interface SettingsChangedMessage { type: typeof SETTINGS_CHANGED; } -interface SettingsQueryMessage { +export interface SettingsQueryMessage { type: typeof SETTINGS_QUERY; } -interface ConsoleFrameMessageMessage { +export interface ConsoleFrameMessageMessage { type: typeof CONSOLE_FRAME_MESSAGE; message: any; } -- cgit v1.2.3 From 8cef5981b808bc1713170627c88dc26ca81063c1 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 11 May 2019 17:45:58 +0900 Subject: Clipbaord as a clean architecture --- src/content/actions/operation.ts | 12 ++-- src/content/client/TabsClient.ts | 18 ++++++ src/content/repositories/ClipboardRepository.ts | 46 +++++++++++++++ src/content/urls.ts | 41 ------------- src/content/usecases/ClipboardUseCase.ts | 44 ++++++++++++++ src/shared/urls.ts | 10 ++-- test/content/usecases/ClipboardUseCase.test.ts | 76 +++++++++++++++++++++++++ 7 files changed, 195 insertions(+), 52 deletions(-) create mode 100644 src/content/client/TabsClient.ts create mode 100644 src/content/repositories/ClipboardRepository.ts delete mode 100644 src/content/urls.ts create mode 100644 src/content/usecases/ClipboardUseCase.ts create mode 100644 test/content/usecases/ClipboardUseCase.test.ts (limited to 'src/shared') diff --git a/src/content/actions/operation.ts b/src/content/actions/operation.ts index b264e36..28192d7 100644 --- a/src/content/actions/operation.ts +++ b/src/content/actions/operation.ts @@ -3,15 +3,15 @@ import * as actions from './index'; import * as messages from '../../shared/messages'; import * as navigates from '../navigates'; import * as focuses from '../focuses'; -import * as urls from '../urls'; -import * as consoleFrames from '../console-frames'; import * as markActions from './mark'; import AddonEnabledUseCase from '../usecases/AddonEnabledUseCase'; +import ClipboardUseCase from '../usecases/ClipboardUseCase'; import { SettingRepositoryImpl } from '../repositories/SettingRepository'; import { ScrollPresenterImpl } from '../presenters/ScrollPresenter'; let addonEnabledUseCase = new AddonEnabledUseCase(); +let clipbaordUseCase = new ClipboardUseCase(); let settingRepository = new SettingRepositoryImpl(); let scrollPresenter = new ScrollPresenterImpl(); @@ -95,13 +95,11 @@ const exec = async( focuses.focusInput(); break; case operations.URLS_YANK: - urls.yank(window); - consoleFrames.postInfo('Yanked ' + window.location.href); + await clipbaordUseCase.yankCurrentURL(); break; case operations.URLS_PASTE: - urls.paste( - window, operation.newTab ? operation.newTab : false, - settings.search, + await clipbaordUseCase.openOrSearch( + operation.newTab ? operation.newTab : false, ); break; default: diff --git a/src/content/client/TabsClient.ts b/src/content/client/TabsClient.ts new file mode 100644 index 0000000..fe72e11 --- /dev/null +++ b/src/content/client/TabsClient.ts @@ -0,0 +1,18 @@ +import * as messages from '../../shared/messages'; + +export default interface TabsClient { + openUrl(url: string, newTab: boolean): Promise; + + // eslint-disable-next-line semi +} + +export class TabsClientImpl { + async openUrl(url: string, newTab: boolean): Promise { + await browser.runtime.sendMessage({ + type: messages.OPEN_URL, + url, + newTab, + }); + } +} + diff --git a/src/content/repositories/ClipboardRepository.ts b/src/content/repositories/ClipboardRepository.ts new file mode 100644 index 0000000..747ae6a --- /dev/null +++ b/src/content/repositories/ClipboardRepository.ts @@ -0,0 +1,46 @@ +export default interface ClipboardRepository { + read(): string; + + write(text: string): void; + + // eslint-disable-next-line semi +} + +export class ClipboardRepositoryImpl { + read(): string { + let textarea = window.document.createElement('textarea'); + window.document.body.append(textarea); + + textarea.style.position = 'fixed'; + textarea.style.top = '-100px'; + textarea.contentEditable = 'true'; + textarea.focus(); + + let ok = window.document.execCommand('paste'); + let value = textarea.textContent!!; + textarea.remove(); + + if (!ok) { + throw new Error('failed to access clipbaord'); + } + + return value; + } + + write(text: string): void { + let input = window.document.createElement('input'); + window.document.body.append(input); + + input.style.position = 'fixed'; + input.style.top = '-100px'; + input.value = text; + input.select(); + + let ok = window.document.execCommand('copy'); + input.remove(); + + if (!ok) { + throw new Error('failed to access clipbaord'); + } + } +} diff --git a/src/content/urls.ts b/src/content/urls.ts deleted file mode 100644 index 035b9bb..0000000 --- a/src/content/urls.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as messages from '../shared/messages'; -import * as urls from '../shared/urls'; -import { Search } from '../shared/Settings'; - -const yank = (win: Window) => { - let input = win.document.createElement('input'); - win.document.body.append(input); - - input.style.position = 'fixed'; - input.style.top = '-100px'; - input.value = win.location.href; - input.select(); - - win.document.execCommand('copy'); - - input.remove(); -}; - -const paste = (win: Window, newTab: boolean, search: Search) => { - let textarea = win.document.createElement('textarea'); - win.document.body.append(textarea); - - textarea.style.position = 'fixed'; - textarea.style.top = '-100px'; - textarea.contentEditable = 'true'; - textarea.focus(); - - if (win.document.execCommand('paste')) { - let value = textarea.textContent as string; - let url = urls.searchUrl(value, search); - browser.runtime.sendMessage({ - type: messages.OPEN_URL, - url, - newTab, - }); - } - - textarea.remove(); -}; - -export { yank, paste }; diff --git a/src/content/usecases/ClipboardUseCase.ts b/src/content/usecases/ClipboardUseCase.ts new file mode 100644 index 0000000..b2ece2f --- /dev/null +++ b/src/content/usecases/ClipboardUseCase.ts @@ -0,0 +1,44 @@ +import * as urls from '../../shared/urls'; +import ClipboardRepository, { ClipboardRepositoryImpl } + from '../repositories/ClipboardRepository'; +import SettingRepository, { SettingRepositoryImpl } + from '../repositories/SettingRepository'; +import TabsClient, { TabsClientImpl } + from '../client/TabsClient'; +import ConsoleClient, { ConsoleClientImpl } from '../client/ConsoleClient'; + +export default class ClipboardUseCase { + private repository: ClipboardRepository; + + private settingRepository: SettingRepository; + + private client: TabsClient; + + private consoleClient: ConsoleClient; + + constructor({ + repository = new ClipboardRepositoryImpl(), + settingRepository = new SettingRepositoryImpl(), + client = new TabsClientImpl(), + consoleClient = new ConsoleClientImpl(), + } = {}) { + this.repository = repository; + this.settingRepository = settingRepository; + this.client = client; + this.consoleClient = consoleClient; + } + + async yankCurrentURL(): Promise { + let url = window.location.href; + this.repository.write(url); + await this.consoleClient.info('Yanked ' + url); + return Promise.resolve(url); + } + + async openOrSearch(newTab: boolean): Promise { + let search = this.settingRepository.get().search; + let text = this.repository.read(); + let url = urls.searchUrl(text, search); + await this.client.openUrl(url, newTab); + } +} diff --git a/src/shared/urls.ts b/src/shared/urls.ts index 18349c8..bbdb1ea 100644 --- a/src/shared/urls.ts +++ b/src/shared/urls.ts @@ -1,3 +1,5 @@ +import { Search } from './Settings'; + const trimStart = (str: string): string => { // NOTE String.trimStart is available on Firefox 61 return str.replace(/^\s+/, ''); @@ -5,7 +7,7 @@ const trimStart = (str: string): string => { const SUPPORTED_PROTOCOLS = ['http:', 'https:', 'ftp:', 'mailto:', 'about:']; -const searchUrl = (keywords: string, searchSettings: any): string => { +const searchUrl = (keywords: string, search: Search): string => { try { let u = new URL(keywords); if (SUPPORTED_PROTOCOLS.includes(u.protocol.toLowerCase())) { @@ -17,12 +19,12 @@ const searchUrl = (keywords: string, searchSettings: any): string => { if (keywords.includes('.') && !keywords.includes(' ')) { return 'http://' + keywords; } - let template = searchSettings.engines[searchSettings.default]; + let template = search.engines[search.default]; let query = keywords; let first = trimStart(keywords).split(' ')[0]; - if (Object.keys(searchSettings.engines).includes(first)) { - template = searchSettings.engines[first]; + if (Object.keys(search.engines).includes(first)) { + template = search.engines[first]; query = trimStart(trimStart(keywords).slice(first.length)); } return template.replace('{}', encodeURIComponent(query)); diff --git a/test/content/usecases/ClipboardUseCase.test.ts b/test/content/usecases/ClipboardUseCase.test.ts new file mode 100644 index 0000000..862ee8a --- /dev/null +++ b/test/content/usecases/ClipboardUseCase.test.ts @@ -0,0 +1,76 @@ +import ClipboardRepository from '../../../src/content/repositories/ClipboardRepository'; +import TabsClient from '../../../src/content/client/TabsClient'; +import MockConsoleClient from '../mock/MockConsoleClient'; +import ClipboardUseCase from '../../../src/content/usecases/ClipboardUseCase'; +import { expect } from 'chai'; + +class MockClipboardRepository implements ClipboardRepository { + public clipboard: string; + + constructor() { + this.clipboard = ''; + } + + read(): string { + return this.clipboard; + } + + write(text: string): void { + this.clipboard = text; + } +} + +class MockTabsClient implements TabsClient { + public last: string; + + constructor() { + this.last = ''; + } + + openUrl(url: string, _newTab: boolean): Promise { + this.last = url; + return Promise.resolve(); + } +} + +describe('ClipboardUseCase', () => { + let repository: MockClipboardRepository; + let client: MockTabsClient; + let consoleClient: MockConsoleClient; + let sut: ClipboardUseCase; + + beforeEach(() => { + repository = new MockClipboardRepository(); + client = new MockTabsClient(); + consoleClient = new MockConsoleClient(); + sut = new ClipboardUseCase({ repository, client: client, consoleClient }); + }); + + describe('#yankCurrentURL', () => { + it('yanks current url', async () => { + let yanked = await sut.yankCurrentURL(); + + expect(yanked).to.equal(window.location.href); + expect(repository.clipboard).to.equal(yanked); + expect(consoleClient.text).to.equal('Yanked ' + yanked); + }); + }); + + describe('#openOrSearch', () => { + it('opens url from the clipboard', async () => { + let url = 'https://github.com/ueokande/vim-vixen' + repository.clipboard = url; + await sut.openOrSearch(true); + + expect(client.last).to.equal(url); + }); + + it('opens search results from the clipboard', async () => { + repository.clipboard = 'banana'; + await sut.openOrSearch(true); + + expect(client.last).to.equal('https://google.com/search?q=banana'); + }); + }); +}); + -- cgit v1.2.3 From 17dc2bb5ec6a53c67e1b6df2b82410239eee95fc Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Fri, 17 May 2019 23:12:34 +0900 Subject: Send properties on activate --- src/content/components/common/follow.ts | 21 ++++++--------------- .../components/top-content/follow-controller.ts | 2 ++ src/shared/messages.ts | 4 ++-- 3 files changed, 10 insertions(+), 17 deletions(-) (limited to 'src/shared') diff --git a/src/content/components/common/follow.ts b/src/content/components/common/follow.ts index a30a3d5..9a62613 100644 --- a/src/content/components/common/follow.ts +++ b/src/content/components/common/follow.ts @@ -68,18 +68,12 @@ const isAriaHiddenOrAriaDisabled = (win: Window, element: Element): boolean => { export default class Follow { private win: Window; - private newTab: boolean; - - private background: boolean; - private hints: {[key: string]: Hint }; private targets: HTMLElement[] = []; constructor(win: Window) { this.win = win; - this.newTab = false; - this.background = false; this.hints = {}; this.targets = []; @@ -106,13 +100,11 @@ export default class Follow { }), '*'); } - createHints(keysArray: string[], newTab: boolean, background: boolean) { + createHints(keysArray: string[]) { if (keysArray.length !== this.targets.length) { throw new Error('illegal hint count'); } - this.newTab = newTab; - this.background = background; this.hints = {}; for (let i = 0; i < keysArray.length; ++i) { let keys = keysArray[i]; @@ -141,7 +133,7 @@ export default class Follow { this.targets = []; } - async activateHints(keys: string): Promise { + async activateHints(keys: string, newTab: boolean, background: boolean): Promise { let hint = this.hints[keys]; if (!hint) { return; @@ -150,7 +142,7 @@ export default class Follow { if (hint instanceof LinkHint) { let url = hint.getLink(); // ignore taget='_blank' - if (!this.newTab && hint.getLinkTarget() !== '_blank') { + if (!newTab && hint.getLinkTarget() !== '_blank') { hint.click(); return; } @@ -158,7 +150,7 @@ export default class Follow { if (!url || url === '#' || url.toLowerCase().startsWith('javascript:')) { return; } - await tabsClient.openUrl(url, this.newTab, this.background); + await tabsClient.openUrl(url, newTab, background); } else if (hint instanceof InputHint) { hint.activate(); } @@ -169,12 +161,11 @@ export default class Follow { case messages.FOLLOW_REQUEST_COUNT_TARGETS: return this.countHints(sender, message.viewSize, message.framePosition); case messages.FOLLOW_CREATE_HINTS: - return this.createHints( - message.keysArray, message.newTab, message.background); + return this.createHints(message.keysArray); case messages.FOLLOW_SHOW_HINTS: return this.showHints(message.keys); case messages.FOLLOW_ACTIVATE: - return this.activateHints(message.keys); + return this.activateHints(message.keys, message.newTab, message.background); case messages.FOLLOW_REMOVE_HINTS: return this.removeHints(); } diff --git a/src/content/components/top-content/follow-controller.ts b/src/content/components/top-content/follow-controller.ts index 2fcf365..2a242c2 100644 --- a/src/content/components/top-content/follow-controller.ts +++ b/src/content/components/top-content/follow-controller.ts @@ -87,6 +87,8 @@ export default class FollowController { broadcastMessage(this.win, { type: messages.FOLLOW_ACTIVATE, keys: this.state.keys as string, + newTab: this.state.newTab!!, + background: this.state.background!!, }); } diff --git a/src/shared/messages.ts b/src/shared/messages.ts index 75df798..816eba2 100644 --- a/src/shared/messages.ts +++ b/src/shared/messages.ts @@ -109,8 +109,6 @@ export interface FollowResponseCountTargetsMessage { export interface FollowCreateHintsMessage { type: typeof FOLLOW_CREATE_HINTS; keysArray: string[]; - newTab: boolean; - background: boolean; } export interface FollowShowHintsMessage { @@ -125,6 +123,8 @@ export interface FollowRemoveHintsMessage { export interface FollowActivateMessage { type: typeof FOLLOW_ACTIVATE; keys: string; + newTab: boolean; + background: boolean; } export interface FollowKeyPressMessage { -- cgit v1.2.3 From a88324acd9fe626b59637541975abe1ee6041aa7 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sat, 18 May 2019 13:06:37 +0900 Subject: Define client and presenter for follow --- src/content/MessageListener.ts | 6 +- src/content/actions/operation.ts | 8 +- src/content/client/FollowMasterClient.ts | 47 ++++++ src/content/client/FollowSlaveClient.ts | 76 ++++++++++ src/content/components/common/follow.ts | 163 +++++---------------- src/content/components/common/index.ts | 2 +- .../components/top-content/follow-controller.ts | 95 ++++++------ src/content/presenters/FollowPresenter.ts | 134 +++++++++++++++++ src/shared/messages.ts | 8 +- 9 files changed, 359 insertions(+), 180 deletions(-) create mode 100644 src/content/client/FollowMasterClient.ts create mode 100644 src/content/client/FollowSlaveClient.ts create mode 100644 src/content/presenters/FollowPresenter.ts (limited to 'src/shared') diff --git a/src/content/MessageListener.ts b/src/content/MessageListener.ts index 1d7a479..e545cab 100644 --- a/src/content/MessageListener.ts +++ b/src/content/MessageListener.ts @@ -1,14 +1,16 @@ import { Message, valueOf } from '../shared/messages'; -export type WebMessageSender = Window | MessagePort | ServiceWorker | null; export type WebExtMessageSender = browser.runtime.MessageSender; export default class MessageListener { onWebMessage( - listener: (msg: Message, sender: WebMessageSender) => void, + listener: (msg: Message, sender: Window) => void, ) { window.addEventListener('message', (event: MessageEvent) => { let sender = event.source; + if (!(sender instanceof Window)) { + return; + } let message = null; try { message = JSON.parse(event.data); diff --git a/src/content/actions/operation.ts b/src/content/actions/operation.ts index 28192d7..657cf47 100644 --- a/src/content/actions/operation.ts +++ b/src/content/actions/operation.ts @@ -9,11 +9,13 @@ import AddonEnabledUseCase from '../usecases/AddonEnabledUseCase'; import ClipboardUseCase from '../usecases/ClipboardUseCase'; import { SettingRepositoryImpl } from '../repositories/SettingRepository'; import { ScrollPresenterImpl } from '../presenters/ScrollPresenter'; +import { FollowMasterClientImpl } from '../client/FollowMasterClient'; let addonEnabledUseCase = new AddonEnabledUseCase(); let clipbaordUseCase = new ClipboardUseCase(); let settingRepository = new SettingRepositoryImpl(); let scrollPresenter = new ScrollPresenterImpl(); +let followMasterClient = new FollowMasterClientImpl(window.top); // eslint-disable-next-line complexity, max-lines-per-function const exec = async( @@ -63,11 +65,7 @@ const exec = async( scrollPresenter.scrollToEnd(smoothscroll); break; case operations.FOLLOW_START: - window.top.postMessage(JSON.stringify({ - type: messages.FOLLOW_START, - newTab: operation.newTab, - background: operation.background, - }), '*'); + followMasterClient.startFollow(operation.newTab, operation.background); break; case operations.MARK_SET_PREFIX: return markActions.startSet(); diff --git a/src/content/client/FollowMasterClient.ts b/src/content/client/FollowMasterClient.ts new file mode 100644 index 0000000..464b52f --- /dev/null +++ b/src/content/client/FollowMasterClient.ts @@ -0,0 +1,47 @@ +import * as messages from '../../shared/messages'; +import { Key } from '../../shared/utils/keys'; + +export default interface FollowMasterClient { + startFollow(newTab: boolean, background: boolean): void; + + responseHintCount(count: number): void; + + sendKey(key: Key): void; + + // eslint-disable-next-line semi +} + +export class FollowMasterClientImpl implements FollowMasterClient { + private window: Window; + + constructor(window: Window) { + this.window = window; + } + + startFollow(newTab: boolean, background: boolean): void { + this.postMessage({ + type: messages.FOLLOW_START, + newTab, + background, + }); + } + + responseHintCount(count: number): void { + this.postMessage({ + type: messages.FOLLOW_RESPONSE_COUNT_TARGETS, + count, + }); + } + + sendKey(key: Key): void { + this.postMessage({ + type: messages.FOLLOW_KEY_PRESS, + key: key.key, + ctrlKey: key.ctrlKey || false, + }); + } + + private postMessage(msg: messages.Message): void { + this.window.postMessage(JSON.stringify(msg), '*'); + } +} diff --git a/src/content/client/FollowSlaveClient.ts b/src/content/client/FollowSlaveClient.ts new file mode 100644 index 0000000..0905cd9 --- /dev/null +++ b/src/content/client/FollowSlaveClient.ts @@ -0,0 +1,76 @@ +import * as messages from '../../shared/messages'; + +interface Size { + width: number; + height: number; +} + +interface Point { + x: number; + y: number; +} + +export default interface FollowSlaveClient { + filterHints(prefix: string): void; + + requestHintCount(viewSize: Size, framePosition: Point): void; + + createHints(viewSize: Size, framePosition: Point, tags: string[]): void; + + clearHints(): void; + + activateIfExists(tag: string, newTab: boolean, background: boolean): void; + + // eslint-disable-next-line semi +} + +export class FollowSlaveClientImpl implements FollowSlaveClient { + private target: Window; + + constructor(target: Window) { + this.target = target; + } + + filterHints(prefix: string): void { + this.postMessage({ + type: messages.FOLLOW_SHOW_HINTS, + prefix, + }); + } + + requestHintCount(viewSize: Size, framePosition: Point): void { + this.postMessage({ + type: messages.FOLLOW_REQUEST_COUNT_TARGETS, + viewSize, + framePosition, + }); + } + + createHints(viewSize: Size, framePosition: Point, tags: string[]): void { + this.postMessage({ + type: messages.FOLLOW_CREATE_HINTS, + viewSize, + framePosition, + tags, + }); + } + + clearHints(): void { + this.postMessage({ + type: messages.FOLLOW_REMOVE_HINTS, + }); + } + + activateIfExists(tag: string, newTab: boolean, background: boolean): void { + this.postMessage({ + type: messages.FOLLOW_ACTIVATE, + tag, + newTab, + background, + }); + } + + private postMessage(msg: messages.Message): void { + this.target.postMessage(JSON.stringify(msg), '*'); + } +} diff --git a/src/content/components/common/follow.ts b/src/content/components/common/follow.ts index 9a62613..e0003e3 100644 --- a/src/content/components/common/follow.ts +++ b/src/content/components/common/follow.ts @@ -1,17 +1,18 @@ import MessageListener from '../../MessageListener'; -import Hint, { LinkHint, InputHint } from '../../presenters/Hint'; -import * as dom from '../../../shared/utils/dom'; +import { LinkHint, InputHint } from '../../presenters/Hint'; import * as messages from '../../../shared/messages'; -import * as keyUtils from '../../../shared/utils/keys'; +import { Key } from '../../../shared/utils/keys'; import TabsClient, { TabsClientImpl } from '../../client/TabsClient'; +import FollowMasterClient, { FollowMasterClientImpl } + from '../../client/FollowMasterClient'; +import FollowPresenter, { FollowPresenterImpl } + from '../../presenters/FollowPresenter'; let tabsClient: TabsClient = new TabsClientImpl(); - -const TARGET_SELECTOR = [ - 'a', 'button', 'input', 'textarea', 'area', - '[contenteditable=true]', '[contenteditable=""]', '[tabindex]', - '[role="button"]', 'summary' -].join(','); +let followMasterClient: FollowMasterClient = + new FollowMasterClientImpl(window.top); +let followPresenter: FollowPresenter = + new FollowPresenterImpl(); interface Size { width: number; @@ -23,118 +24,46 @@ interface Point { y: number; } -const inViewport = ( - win: Window, - element: Element, - viewSize: Size, - framePosition: Point, -): boolean => { - let { - top, left, bottom, right - } = dom.viewportRect(element); - let doc = win.document; - let frameWidth = doc.documentElement.clientWidth; - let frameHeight = doc.documentElement.clientHeight; - - if (right < 0 || bottom < 0 || top > frameHeight || left > frameWidth) { - // out of frame - return false; - } - if (right + framePosition.x < 0 || bottom + framePosition.y < 0 || - left + framePosition.x > viewSize.width || - top + framePosition.y > viewSize.height) { - // out of viewport - return false; - } - return true; -}; - -const isAriaHiddenOrAriaDisabled = (win: Window, element: Element): boolean => { - if (!element || win.document.documentElement === element) { - return false; - } - for (let attr of ['aria-hidden', 'aria-disabled']) { - let value = element.getAttribute(attr); - if (value !== null) { - let hidden = value.toLowerCase(); - if (hidden === '' || hidden === 'true') { - return true; - } - } - } - return isAriaHiddenOrAriaDisabled(win, element.parentElement as Element); -}; - export default class Follow { - private win: Window; + private enabled: boolean; - private hints: {[key: string]: Hint }; - - private targets: HTMLElement[] = []; - - constructor(win: Window) { - this.win = win; - this.hints = {}; - this.targets = []; + constructor() { + this.enabled = false; new MessageListener().onWebMessage(this.onMessage.bind(this)); } - key(key: keyUtils.Key): boolean { - if (Object.keys(this.hints).length === 0) { + key(key: Key): boolean { + if (!this.enabled) { return false; } - this.win.parent.postMessage(JSON.stringify({ - type: messages.FOLLOW_KEY_PRESS, - key: key.key, - ctrlKey: key.ctrlKey, - }), '*'); + followMasterClient.sendKey(key); return true; } - countHints(sender: any, viewSize: Size, framePosition: Point) { - this.targets = Follow.getTargetElements(this.win, viewSize, framePosition); - sender.postMessage(JSON.stringify({ - type: messages.FOLLOW_RESPONSE_COUNT_TARGETS, - count: this.targets.length, - }), '*'); + countHints(viewSize: Size, framePosition: Point) { + let count = followPresenter.getTargetCount(viewSize, framePosition); + followMasterClient.responseHintCount(count); } - createHints(keysArray: string[]) { - if (keysArray.length !== this.targets.length) { - throw new Error('illegal hint count'); - } - - this.hints = {}; - for (let i = 0; i < keysArray.length; ++i) { - let keys = keysArray[i]; - let target = this.targets[i]; - if (target instanceof HTMLAnchorElement || - target instanceof HTMLAreaElement) { - this.hints[keys] = new LinkHint(target, keys); - } else { - this.hints[keys] = new InputHint(target, keys); - } - } + createHints(viewSize: Size, framePosition: Point, tags: string[]) { + this.enabled = true; + followPresenter.createHints(viewSize, framePosition, tags); } - showHints(keys: string) { - Object.keys(this.hints).filter(key => key.startsWith(keys)) - .forEach(key => this.hints[key].show()); - Object.keys(this.hints).filter(key => !key.startsWith(keys)) - .forEach(key => this.hints[key].hide()); + showHints(prefix: string) { + followPresenter.filterHints(prefix); } removeHints() { - Object.keys(this.hints).forEach((key) => { - this.hints[key].remove(); - }); - this.hints = {}; - this.targets = []; + followPresenter.clearHints(); + this.enabled = false; } - async activateHints(keys: string, newTab: boolean, background: boolean): Promise { - let hint = this.hints[keys]; + async activateHints( + tag: string, newTab: boolean, background: boolean, + ): Promise { + let hint = followPresenter.getHint(tag); if (!hint) { return; } @@ -156,38 +85,20 @@ export default class Follow { } } - onMessage(message: messages.Message, sender: any) { + onMessage(message: messages.Message, _sender: Window) { switch (message.type) { case messages.FOLLOW_REQUEST_COUNT_TARGETS: - return this.countHints(sender, message.viewSize, message.framePosition); + return this.countHints(message.viewSize, message.framePosition); case messages.FOLLOW_CREATE_HINTS: - return this.createHints(message.keysArray); + return this.createHints( + message.viewSize, message.framePosition, message.tags); case messages.FOLLOW_SHOW_HINTS: - return this.showHints(message.keys); + return this.showHints(message.prefix); case messages.FOLLOW_ACTIVATE: - return this.activateHints(message.keys, message.newTab, message.background); + return this.activateHints( + message.tag, message.newTab, message.background); case messages.FOLLOW_REMOVE_HINTS: return this.removeHints(); } } - - static getTargetElements( - win: Window, - viewSize: - Size, framePosition: Point, - ): HTMLElement[] { - let all = win.document.querySelectorAll(TARGET_SELECTOR); - let filtered = Array.prototype.filter.call(all, (element: HTMLElement) => { - let style = win.getComputedStyle(element); - - // AREA's 'display' in Browser style is 'none' - return (element.tagName === 'AREA' || style.display !== 'none') && - style.visibility !== 'hidden' && - (element as HTMLInputElement).type !== 'hidden' && - element.offsetHeight > 0 && - !isAriaHiddenOrAriaDisabled(win, element) && - inViewport(win, element, viewSize, framePosition); - }); - return filtered; - } } diff --git a/src/content/components/common/index.ts b/src/content/components/common/index.ts index 899953d..b2f48a3 100644 --- a/src/content/components/common/index.ts +++ b/src/content/components/common/index.ts @@ -16,7 +16,7 @@ let settingUseCase = new SettingUseCase(); export default class Common { constructor(win: Window, store: any) { const input = new InputComponent(win.document.body); - const follow = new FollowComponent(win); + const follow = new FollowComponent(); const mark = new MarkComponent(store); const keymapper = new KeymapperComponent(store); diff --git a/src/content/components/top-content/follow-controller.ts b/src/content/components/top-content/follow-controller.ts index 2a242c2..43c917e 100644 --- a/src/content/components/top-content/follow-controller.ts +++ b/src/content/components/top-content/follow-controller.ts @@ -1,18 +1,14 @@ import * as followControllerActions from '../../actions/follow-controller'; import * as messages from '../../../shared/messages'; -import MessageListener, { WebMessageSender } from '../../MessageListener'; +import MessageListener from '../../MessageListener'; import HintKeyProducer from '../../hint-key-producer'; import { SettingRepositoryImpl } from '../../repositories/SettingRepository'; +import FollowSlaveClient, { FollowSlaveClientImpl } + from '../../client/FollowSlaveClient'; let settingRepository = new SettingRepositoryImpl(); -const broadcastMessage = (win: Window, message: messages.Message): void => { - let json = JSON.stringify(message); - let frames = [win.self].concat(Array.from(win.frames as any)); - frames.forEach(frame => frame.postMessage(json, '*')); -}; - export default class FollowController { private win: Window; @@ -43,7 +39,7 @@ export default class FollowController { }); } - onMessage(message: messages.Message, sender: WebMessageSender) { + onMessage(message: messages.Message, sender: Window) { switch (message.type) { case messages.FOLLOW_START: return this.store.dispatch( @@ -77,18 +73,17 @@ export default class FollowController { this.store.dispatch(followControllerActions.disable()); } - broadcastMessage(this.win, { - type: messages.FOLLOW_SHOW_HINTS, - keys: this.state.keys as string, + this.broadcastMessage((c: FollowSlaveClient) => { + c.filterHints(this.state.keys!!); }); } activate(): void { - broadcastMessage(this.win, { - type: messages.FOLLOW_ACTIVATE, - keys: this.state.keys as string, - newTab: this.state.newTab!!, - background: this.state.background!!, + this.broadcastMessage((c: FollowSlaveClient) => { + c.activateIfExists( + this.state.keys!!, + this.state.newTab!!, + this.state.background!!); }); } @@ -123,50 +118,64 @@ export default class FollowController { let doc = this.win.document; let viewWidth = this.win.innerWidth || doc.documentElement.clientWidth; let viewHeight = this.win.innerHeight || doc.documentElement.clientHeight; - let frameElements = this.win.document.querySelectorAll('frame,iframe'); - - this.win.postMessage(JSON.stringify({ - type: messages.FOLLOW_REQUEST_COUNT_TARGETS, - viewSize: { width: viewWidth, height: viewHeight }, - framePosition: { x: 0, y: 0 }, - }), '*'); - frameElements.forEach((ele) => { + let frameElements = this.win.document.querySelectorAll('iframe'); + + new FollowSlaveClientImpl(this.win).requestHintCount( + { width: viewWidth, height: viewHeight }, + { x: 0, y: 0 }); + + for (let ele of Array.from(frameElements)) { let { left: frameX, top: frameY } = ele.getBoundingClientRect(); - let message = JSON.stringify({ - type: messages.FOLLOW_REQUEST_COUNT_TARGETS, - viewSize: { width: viewWidth, height: viewHeight }, - framePosition: { x: frameX, y: frameY }, - }); - if (ele instanceof HTMLFrameElement && ele.contentWindow || - ele instanceof HTMLIFrameElement && ele.contentWindow) { - ele.contentWindow.postMessage(message, '*'); - } - }); + new FollowSlaveClientImpl(ele.contentWindow!!).requestHintCount( + { width: viewWidth, height: viewHeight }, + { x: frameX, y: frameY }, + ); + } } - create(count: number, sender: WebMessageSender) { + create(count: number, sender: Window) { let produced = []; for (let i = 0; i < count; ++i) { produced.push((this.producer as HintKeyProducer).produce()); } this.keys = this.keys.concat(produced); - (sender as Window).postMessage(JSON.stringify({ - type: messages.FOLLOW_CREATE_HINTS, - keysArray: produced, - newTab: this.state.newTab, - background: this.state.background, - }), '*'); + let doc = this.win.document; + let viewWidth = this.win.innerWidth || doc.documentElement.clientWidth; + let viewHeight = this.win.innerHeight || doc.documentElement.clientHeight; + let pos = { x: 0, y: 0 }; + if (sender !== window) { + let frameElements = this.win.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, + ); } remove() { this.keys = []; - broadcastMessage(this.win, { - type: messages.FOLLOW_REMOVE_HINTS, + this.broadcastMessage((c: FollowSlaveClient) => { + c.clearHints(); }); } private hintchars() { return settingRepository.get().properties.hintchars; } + + private broadcastMessage(f: (clinet: FollowSlaveClient) => void) { + let windows = [window.self].concat(Array.from(window.frames as any)); + windows + .map(w => new FollowSlaveClientImpl(w)) + .forEach(c => f(c)); + } } diff --git a/src/content/presenters/FollowPresenter.ts b/src/content/presenters/FollowPresenter.ts new file mode 100644 index 0000000..f0d115c --- /dev/null +++ b/src/content/presenters/FollowPresenter.ts @@ -0,0 +1,134 @@ +import Hint, { InputHint, LinkHint } from './Hint'; +import * as doms from '../../shared/utils/dom'; + +const TARGET_SELECTOR = [ + 'a', 'button', 'input', 'textarea', 'area', + '[contenteditable=true]', '[contenteditable=""]', '[tabindex]', + '[role="button"]', 'summary' +].join(','); + +interface Size { + width: number; + height: number; +} + +interface Point { + x: number; + y: number; +} + +const inViewport = ( + win: Window, + element: Element, + viewSize: Size, + framePosition: Point, +): boolean => { + let { + top, left, bottom, right + } = doms.viewportRect(element); + let doc = win.document; + let frameWidth = doc.documentElement.clientWidth; + let frameHeight = doc.documentElement.clientHeight; + + if (right < 0 || bottom < 0 || top > frameHeight || left > frameWidth) { + // out of frame + return false; + } + if (right + framePosition.x < 0 || bottom + framePosition.y < 0 || + left + framePosition.x > viewSize.width || + top + framePosition.y > viewSize.height) { + // out of viewport + return false; + } + return true; +}; + +const isAriaHiddenOrAriaDisabled = (win: Window, element: Element): boolean => { + if (!element || win.document.documentElement === element) { + return false; + } + for (let attr of ['aria-hidden', 'aria-disabled']) { + let value = element.getAttribute(attr); + if (value !== null) { + let hidden = value.toLowerCase(); + if (hidden === '' || hidden === 'true') { + return true; + } + } + } + return isAriaHiddenOrAriaDisabled(win, element.parentElement as Element); +}; + +export default interface FollowPresenter { + getTargetCount(viewSize: Size, framePosition: Point): number; + + createHints(viewSize: Size, framePosition: Point, tags: string[]): void; + + filterHints(prefix: string): void; + + clearHints(): void; + + getHint(tag: string): Hint | undefined; + + // eslint-disable-next-line semi +} + +export class FollowPresenterImpl implements FollowPresenter { + private hints: Hint[] + + constructor() { + this.hints = []; + } + + getTargetCount(viewSize: Size, framePosition: Point): number { + let targets = this.getTargets(viewSize, framePosition); + return targets.length; + } + + createHints(viewSize: Size, framePosition: Point, tags: string[]): void { + let targets = this.getTargets(viewSize, framePosition); + let min = Math.min(targets.length, tags.length); + for (let i = 0; i < min; ++i) { + let target = targets[i]; + if (target instanceof HTMLAnchorElement || + target instanceof HTMLAreaElement) { + this.hints.push(new LinkHint(target, tags[i])); + } else { + this.hints.push(new InputHint(target, tags[i])); + } + } + } + + filterHints(prefix: string): void { + let shown = this.hints.filter(h => h.getTag().startsWith(prefix)); + let hidden = this.hints.filter(h => !h.getTag().startsWith(prefix)); + + shown.forEach(h => h.show()); + hidden.forEach(h => h.hide()); + } + + clearHints(): void { + this.hints.forEach(h => h.remove()); + this.hints = []; + } + + getHint(tag: string): Hint | undefined { + return this.hints.find(h => h.getTag() === tag); + } + + private getTargets(viewSize: Size, framePosition: Point): HTMLElement[] { + let all = window.document.querySelectorAll(TARGET_SELECTOR); + let filtered = Array.prototype.filter.call(all, (element: HTMLElement) => { + let style = window.getComputedStyle(element); + + // AREA's 'display' in Browser style is 'none' + return (element.tagName === 'AREA' || style.display !== 'none') && + style.visibility !== 'hidden' && + (element as HTMLInputElement).type !== 'hidden' && + element.offsetHeight > 0 && + !isAriaHiddenOrAriaDisabled(window, element) && + inViewport(window, element, viewSize, framePosition); + }); + return filtered; + } +} diff --git a/src/shared/messages.ts b/src/shared/messages.ts index 816eba2..fbd3478 100644 --- a/src/shared/messages.ts +++ b/src/shared/messages.ts @@ -108,12 +108,14 @@ export interface FollowResponseCountTargetsMessage { export interface FollowCreateHintsMessage { type: typeof FOLLOW_CREATE_HINTS; - keysArray: string[]; + tags: string[]; + viewSize: { width: number, height: number }; + framePosition: { x: number, y: number }; } export interface FollowShowHintsMessage { type: typeof FOLLOW_SHOW_HINTS; - keys: string; + prefix: string; } export interface FollowRemoveHintsMessage { @@ -122,7 +124,7 @@ export interface FollowRemoveHintsMessage { export interface FollowActivateMessage { type: typeof FOLLOW_ACTIVATE; - keys: string; + tag: string; newTab: boolean; background: boolean; } -- 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/shared') 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