parent
a88324acd9
commit
efc48dc742
15 changed files with 620 additions and 88 deletions
@ -0,0 +1,11 @@ |
|||||||
|
import * as operations from '../../shared/operations'; |
||||||
|
import * as messages from '../../shared/messages'; |
||||||
|
|
||||||
|
export default class BackgroundClient { |
||||||
|
execBackgroundOp(op: operations.Operation): Promise<void> { |
||||||
|
return browser.runtime.sendMessage({ |
||||||
|
type: messages.BACKGROUND_OPERATION, |
||||||
|
operation: op, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -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, |
||||||
|
}), '*'); |
||||||
|
} |
||||||
|
} |
@ -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; |
||||||
|
} |
||||||
|
} |
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
|
|
@ -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 = []; |
||||||
|
} |
||||||
|
} |
@ -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(); |
||||||
|
} |
||||||
|
} |
@ -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(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,100 @@ |
|||||||
|
import KeymapRepository, { KeymapRepositoryImpl } |
||||||
|
from '../repositories/KeymapRepository'; |
||||||
|
import SettingRepository, { SettingRepositoryImpl } |
||||||
|
from '../repositories/SettingRepository'; |
||||||
|
import AddonEnabledRepository, { AddonEnabledRepositoryImpl } |
||||||
|
from '../repositories/AddonEnabledRepository'; |
||||||
|
|
||||||
|
import * as operations from '../../shared/operations'; |
||||||
|
import { Keymaps } from '../../shared/Settings'; |
||||||
|
import * as keyUtils from '../../shared/utils/keys'; |
||||||
|
|
||||||
|
type KeymapEntityMap = Map<keyUtils.Key[], operations.Operation>; |
||||||
|
|
||||||
|
const reservedKeymaps: Keymaps = { |
||||||
|
'<Esc>': { type: operations.CANCEL }, |
||||||
|
'<C-[>': { type: operations.CANCEL }, |
||||||
|
}; |
||||||
|
|
||||||
|
const mapStartsWith = ( |
||||||
|
mapping: keyUtils.Key[], |
||||||
|
keys: keyUtils.Key[], |
||||||
|
): boolean => { |
||||||
|
if (mapping.length < keys.length) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
for (let i = 0; i < keys.length; ++i) { |
||||||
|
if (!keyUtils.equals(mapping[i], keys[i])) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
return true; |
||||||
|
}; |
||||||
|
|
||||||
|
export default class KeymapUseCase { |
||||||
|
private repository: KeymapRepository; |
||||||
|
|
||||||
|
private settingRepository: SettingRepository; |
||||||
|
|
||||||
|
private addonEnabledRepository: AddonEnabledRepository; |
||||||
|
|
||||||
|
constructor({ |
||||||
|
repository = new KeymapRepositoryImpl(), |
||||||
|
settingRepository = new SettingRepositoryImpl(), |
||||||
|
addonEnabledRepository = new AddonEnabledRepositoryImpl(), |
||||||
|
} = {}) { |
||||||
|
this.repository = repository; |
||||||
|
this.settingRepository = settingRepository; |
||||||
|
this.addonEnabledRepository = addonEnabledRepository; |
||||||
|
} |
||||||
|
|
||||||
|
nextOp(key: keyUtils.Key): operations.Operation | null { |
||||||
|
let keys = this.repository.enqueueKey(key); |
||||||
|
|
||||||
|
let keymaps = this.keymapEntityMap(); |
||||||
|
let matched = Array.from(keymaps.keys()).filter( |
||||||
|
(mapping: keyUtils.Key[]) => { |
||||||
|
return mapStartsWith(mapping, keys); |
||||||
|
}); |
||||||
|
if (!this.addonEnabledRepository.get()) { |
||||||
|
// available keymaps are only ADDON_ENABLE and ADDON_TOGGLE_ENABLED if
|
||||||
|
// the addon disabled
|
||||||
|
matched = matched.filter((keymap) => { |
||||||
|
let type = (keymaps.get(keymap) as operations.Operation).type; |
||||||
|
return type === operations.ADDON_ENABLE || |
||||||
|
type === operations.ADDON_TOGGLE_ENABLED; |
||||||
|
}); |
||||||
|
} |
||||||
|
if (matched.length === 0) { |
||||||
|
// No operations to match with inputs
|
||||||
|
this.repository.clear(); |
||||||
|
return null; |
||||||
|
} else if (matched.length > 1 || |
||||||
|
matched.length === 1 && keys.length < matched[0].length) { |
||||||
|
// More than one operations are matched
|
||||||
|
return null; |
||||||
|
} |
||||||
|
// Exactly one operation is matched
|
||||||
|
let operation = keymaps.get(matched[0]) as operations.Operation; |
||||||
|
this.repository.clear(); |
||||||
|
return operation; |
||||||
|
} |
||||||
|
|
||||||
|
clear(): void { |
||||||
|
this.repository.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
private keymapEntityMap(): KeymapEntityMap { |
||||||
|
let keymaps = { |
||||||
|
...this.settingRepository.get().keymaps, |
||||||
|
...reservedKeymaps, |
||||||
|
}; |
||||||
|
let entries = Object.entries(keymaps).map((entry) => { |
||||||
|
return [ |
||||||
|
keyUtils.fromMapKeys(entry[0]), |
||||||
|
entry[1], |
||||||
|
]; |
||||||
|
}) as [keyUtils.Key[], operations.Operation][]; |
||||||
|
return new Map<keyUtils.Key[], operations.Operation>(entries); |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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; |
||||||
|
} |
||||||
|
} |
@ -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' })); |
||||||
|
}); |
||||||
|
}); |
@ -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 }); |
|
||||||
}) |
|
||||||
}); |
|
Reference in new issue